内存分配与回收策略

    我们经常听到“jvm调优”,但是对于0经验的我来说,真是一头雾水,所以打算从基础抓起,首先了解一些内存分配与回收策略。

    以jvm为例,内存主要分为堆(heap)和栈(stack)。栈用于存储线程上下文信息,如方法参数、局部变量等。堆则是存储对象的内存空间,对象的创建和释放、垃圾回收就是在堆中进行的。对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB(ThreadLocalAllocBuffer,是线程私有的一块内存,如果设置了-XX:UseTLAB,在线程初始化时,也会申请一块指定大小的内存,只供当前线程使用,每个线程都有一个单独的Buffer,如果要分配内存,就在自己的Buffer上分配,不存在竞争的情况,能够提高分配的效率,但是当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用)上分配。少数情况也可能会直接分配在老年代中,分配的规则不是百分百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置, 下面介绍的都是最普遍的内存分配规则。

 1.对象优先在Eden分配

      大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配,虚拟机会发起一次Minor GC。

内存分配与回收策略_第1张图片

内存分配与回收策略_第2张图片

分析: 代码尝试分配3个2MB大小和一个4MB大小的对象,因为配置的-XX:SurvivorRation=8,新生代总可用空间为Eden区+一个Survivor区的总容量(为什么不是Eden+两个Survivor区的空间?因为要留一个Survivor区来接收MinorGC后还存活的对象,毕竟新生代采用的复制算法),分配allocation4的时候会触发一次MinorGC,因为Eden区已经占用了6MB,剩余空间已经不够分配allocation4需要的4MB了,并且在GC期间虚拟机发现已经存在的三个2MB大小的对象都不能放入Survivor空间(Survivor空间只有1MB大小),所以通过分配担保机制提前放到老年代中。GC结束后,4MB的allocation4对象分配到Eden中。

下面是我自己测试的,和书中的出入有点大,不清楚问题出在了哪里,有了解的大佬帮忙指点一下。

   内存分配与回收策略_第3张图片

   对应的gc参数

内存分配与回收策略_第4张图片


2.大对象直接进入老年代

    大对象就是需要大量的连续内存空间的对象,最典型的大对象是很长的字符串以及数组,虚拟机的内存分配不喜欢大对象,所以在平时写代码的时候,尽量避免“朝生夕灭”的“短命大对象”。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的是为了避免在Eden区和两个Survivor区之间发生大量的内存复制(新生代采用复制算法)。

内存分配与回收策略_第5张图片

从gc日志中,可以看到老年代中并没有对象,难道PretenureSizeTHreshold这个参数没有起作用吗?原来是因为PretenureSizeTHreshold这个参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

然后从gc日志中我们可以看出新生代用的是Parallel Scavenge收集器,老年代采用的是Parallel Old收集器,所以参数没有起作用。

通过命令:java -XX:+PrintCommandLineFlags -version

内存分配与回收策略_第6张图片

看到使用的是ParallelGC,所以PretenureSizeThreshold参数没有起作用,所以我们采用PraNew+CMS的组合,配置参数-XX:+UseConcMarkSweepGC,老年代采用CMS收集器,而新生代默认采用ParNew收集器。加上后,再看gc日志。

内存分配与回收策略_第7张图片

可以看到采用ParNew+CMS的组合后,PretenureSizeThreshold参数起作用了,大于3MB的对象直接分配到了老年代。


3.长期存活的对象将进入老年代

     对象在Eden出生并经过一次Minor GC后仍存活,并且能够被Survivor容纳,将被移动到Survivor空间中,并且对象的年龄设置为1。对象在Survivor区中每经历一次Minor GC,年龄就+1,当年龄达到一定程度(默认15,对象头里分配了4bit来存储age属性,所以最大1111,即15),就会被晋升到老年代中,关于这个对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

内存分配与回收策略_第8张图片

可见,当参数设置为1的时候,allocation1在第二次gc发生时进入老年代,新生代已经使用的内存变为0KB。 

分析过程如下:

内存分配与回收策略_第9张图片

上面的分析是我根据gc日志推断的,有两个点不是很清楚,希望了解的大佬帮忙解答一下:

①当Eden区中存在的对象的大小+将要分配的对象的大小=Eden区大小的时候,会触发YoungGC吗?从gc日志中,我推断是会的。

②将对象的引用设置为null后,当新分配的对象大小+没有被引用对象内存的大小=Eden区大小,是把新分配的放到老年代,并且把没有被引用的对象收集掉。还是将没有被引用的对象的内存收集掉,然后把新分配的对象放到Eden区中,从gc日志中,我推断是后者。


当参数设置为15的时候,allocation1在第二次gc发生时就不会进入老年代。

内存分配与回收策略_第10张图片

上面的图是《深入理解Java虚拟机》里的,我自己打印的和书中不一样,会出现两种情况

①第二次gc后新生代占用内存为0

内存分配与回收策略_第11张图片

②第二次gc后,新生代大小不为0,但是小于allocation1的大小

内存分配与回收策略_第12张图片

对应的jvm参数

内存分配与回收策略_第13张图片

不清楚为何会出现这种情况,把新生代晋升老年代的年龄改成了15,第二次gc后,新生代中应该还有allocatioin1的对象的,可是自己尝试的就没有了,还请了解的大佬给指点迷津。


4.动态年龄判断

    在前面介绍过对象的年龄达到MaxTenuringThreshold才能晋升到老年代,为了更好的适应不同程序的内存状态,如果在Survivor空间中相同年龄所有对象那个大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入到老年代,不用非得等达到MaxTenuringThreshold中要求的年龄。

内存分配与回收策略_第14张图片

分析:通过gc日志,我们可以看到Suvivor空间占用为0%,老年代空间比预期的大,就是allocation1和allocation2对象直接进入了老年代,因为这两个对象是同年龄的,并且加起来达到了512KB,满足了相同年龄对象达到Survivor空间的一半规则。不过这里我有个疑惑,为何老年代中的使用内存不是 4178+512=4690附近的数字?


5.空间分配担保

    发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,Minor GC是肯定安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。若允许,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均水平,如果大于,尝试进行一次Minor GC;如果小于或HandlePromotionFailure设置不允许,就要进行一次Full GC。

    关于“冒险”,新生代使用复制算法,但为了内存利用率,只使用其中一个Survivor空间作为轮换备份,所以当出现大量大对象在Minor GC后存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接放到老年代中。老年代要进行这样的担保,前提是老年代的可用内存可以放得下这些对象,会有多少对象存活在实际完成内存回收之前是不能确定的,所以就根据之前每次回收晋升到老年代对象容量的平均值作为参考,与老年代剩余空间进行对比,从而来决定是否进行Full GC来让老年代腾出空间。取平均值进行比较是一个动态概率的手段,如果某次Minor GC存活的对象突增,远远高于平均水平,就会担保失败(Handle Promotion Failure),如果担保失败,就会发起Full GC,虽说这样做了无用功,但还是建议把HandlePromotionFailure开关打开,避免Full GC过于频繁。

注意:在JDK 6 Update24后,规则变为只要老年代的连续空间大于新生代对象的总大小或历次晋升的平均值大小就会进行Minor GC,否则进行Full GC。


备注:

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也很快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。

后记:上面都是参考《深入理解Java虚拟机》书中的资料,自己实验的gc日志和书中有的地方有出入,后续会继续深入研究,欢迎各位大佬指出问题。

你可能感兴趣的:(jvm)