实战:内存分配与回收策略
Java技术体系的自动内存管理,最核心的目标就是自动化的解决两个问题:内存分配
和内存回收
,在之前我们讲的都是内存回收,现在我们来看一下内存分配的细节,这里我是使用JDK1.8,HotSpot虚拟机,使用的ParallelGC
这里有一个很细的点:《深入理解Java虚拟机》使用的
Serial+Serial Old
客户端默认收集器组合下的内存分配和回收策略,但咱们的jvm是服务端的,说是只能在32位机jvm.cfg
修改文件使用客户端,所以当我运行书上第一个代码块儿时,就发现结果与书上不对,想不出原因,所以如果你也是ParallelGC,那么书本上本章上许多代码结果都是不一样的。一些VM参数也是无法使用的或者没有效果的。
- java -XX:+PrintCommandLineFlags -version 输出JVM默认垃圾收集器,第四行的
UseParallelGC
和最后一行的Server VM
[root@jeespring ~]# java -XX:+PrintCommandLineFlags -version -XX:InitialHeapSize=132500800 -XX:MaxHeapSize=2120012800 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC java version "1.8.0_211" Java(TM) SE Runtime Environment (build 1.8.0_211-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode
- /usr/libexec/java_home -V 这个是在mac上找我的jvm在哪
- find . -name “jvm.cfg” 如果你找不到jvm.cfg,通过它去找,表示在当前路径下找
- JVM的Server端和Client端区别网上有详细介绍,通俗的讲就是Client端轻量快速,Server端重量性能好(默认)。
对象的内存分配从概念上来讲,应该是在堆上分配,不过实际中也可能通过即时编译后拆分为标量间接存储在栈上(这个在我之前写的
逃逸分析
中有说明)
### 对象优先在Eden分配(书本原题目)
- Serial:当大对象要分配内存时,发现Eden区已经满了,而且Survivor区也不够,会将Eden区原来的对象直接放入老年代,再把新的对象放入Eden区。
- Parallel(我测试的):发现Eden区满了,Survivor区不够,会直接把大对象放到老年代,Eden区对象不变。读者可以自己再测试一遍
/**
* 对象分配
*/
public class no1 {
private static final int _1MB = 1024*1024;
/**
* VM: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* 堆空间分配20m,新生代10m,新生代中的Eden和Survivor为8:1:1
* @param args
*/
public static void main(String[] args) {
byte[] a1,a2,a3,a4;
a1 = new byte[2*_1MB];
a2 = new byte[2*_1MB];
a3 = new byte[2*_1MB];
/**
* 上面已经创建了8m,相当于Eden区已经满了,现在我们再放一个4M的对象
* Eden已经放不下了,会启动一次MinorGC,我们看一下会之前的对象和新来的对象如何分配
*
* PSYoungGen total 9216K, used 8001K
* eden space 8192K, 97% used
* from space 1024K, 0% used
* ParOldGen total 10240K, used 4096K
*/
a4 = new byte[4*_1MB];
}
}
大对象直接进入老年代
我们知道一般对象不会直接到老年区,得现在Eden区和Survivor区熬几轮,但如果有大的对象已经在里面了,那么就很容易触犯minor GC,导致对象进行标记-复制,所以可以可以通过设置XX:PretenureSizeThreshold
来指定超过多大的对象直接去了老年区(注意这个参数Parallel无法使用)
/**
* 对象分配
*/
public class no1 {
private static final int _1MB = 1024*1024;
/**
* VM: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728
* 堆空间分配20m,新生代10m,新生代中的Eden和Survivor为8:1:1
* -XX:PretenureSizeThreshold 表示 需要分配的对象只要大于3M,就直接分配到老年区,注意:这个参数只能Serial使用,ParallelGC无法使用
* @param args
*/
public static void main(String[] args) {
byte[] a4;
a4 = new byte[4*_1MB];
}
}
长期存活的对象进入老年代
意思就是我们给每个对象定义一个对象年龄计数器,存储在对象头中,每经历一次Minor GC后存活,并可以存储到Survivor区时,年龄就+1,当长到系统-MaxTenuringThreshold
设定值时(默认值15),就去老年代.
动态对象年龄判定
这里就是说系统可以动态判断,如果每个对象都得等15轮minor GC,那就会导致频繁的GC,影响性能,如果Survivor区中对低于或者等于某个年龄的对象总和超过了Survivor空间的一半,那这个年龄及其以上的对象直接进入老年区。
空间分配担保
我们在前面就提到,其实老年代就是新生代的担保人,如果新生代放不下的对象,我就把对象放到老年代。根据这第一点我们就可以推断出每次Minor GC
前,虚拟机都应检查老年代最大的连续空间是否有足够的空间装下新生代所有对象,如果空间足够,那我们就放心的Minor GC,如果不够了,虚拟机去查看-XX:HandlePromotionFailur
参数是否允许担保失败,如果允许,继续检查老年代之前进来对象的平均大小是否小于老年代最大连续空间(简单来说:通过以前来的,猜测后面来的应该差不多大),如果足够,进行Minor GC,虽然有风险,如果不够,或者是没有担保,则直接Full GC
,这就很影响性能了。所以一般都会将-XX:HandlePromotionFailur
打开,来避免频繁的FUll GC。这个参数在JDK6 Update24后就不再使用了。
也就是现在只要老年代的连续空间大于新生代对象总大小或者以往对象的平均值大小,就会进行Minor GC,否则将进行Full GC。
总结
垃圾收集器在许多场景都是影响系统停顿时间和吞吐能力的重要因素,虚拟机之所以提供各种不同的收集器和大量的调节参数就是因为各种有各种的适用场景,需要我们不断探索,不断组合。当然我们也需要知道每种收集器的特点以及范围。