【6】JVM-内存分配与回收策略
JAVA技术体系中的自动内存管理实际上就是自动化的解决了给对象分配内存以及回收给对象分配的内存这两个问题。回收部分通过之前的《GC设计思路分析》和《垃圾收集器》这两篇博文进行了总结,那么接下来主要就是谈谈自己对JVM是如何给对象分配内存这一部分的理解。JVM的内存空间是有限的,并且堆内存是共享的,那么不同线程共用堆内存如何保证线程安全都是需要考虑的问题。
通过之前对JVM中的内存模型的分析以及GC的学习,我们知道JAVA内存分配往大了说就是在JAVA堆上分配内存,对象主要分配在新生代的Eden区;Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁(顺带说一下为什么new一个对象消耗较大就是在堆内存中创建对象需要加锁),因此如果启动了TLAB,JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C语言基本是一样高效的;但如果对象过大的话则仍然是直接使用堆空间分配。对于少数的大对象会被直接分配到老年代中。JVM中的内存分配规则并不是百分百固定的,细节取决于当前使用的垃圾收集器组合(我认为其实就是考虑组合在不同年代使用的是哪种收集算法,不同的收集算法导致了分配细节不一样),还有虚拟机中与内存相关的参数的设置。
对于Client模式下的JVM来说(只有32位的JDK安装才有Client模式的JVM,64位的JDK只有Server模式的JVM),默认的新生代和老年代的垃圾收集器是单线程的Serial(复制算法,并且存在担保机制,默认Eden区和Survivor是8:1)和Serial Old(标记-整理算法)。下面将研究,在这种组合的情况下,JVM的内存分配和回收策略(ParNew和Serial的组合也差不多)。
- 对象优先分配到新生代的Eden区
- 大对象直接进入老年代:所谓的大对象就是指需要大量连续内存空间的JAVA对象,最典型的大对象就是那种很长的字符串和数组。经常产生大对象容易导致额外的GC操作,JVM中提供了一个-XX:PretenureSizeThreshold参数(这个参数只对Serial和ParNew这两个新生代垃圾收集器有效),令大于这个参数的对象直接在老年代中分配,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。为什么这样,就在于Serial使用的是复制算法,如果不了解可以参看之前的介绍JVM垃圾收集机制的博文。
- 长期存活的对象将进入老年代
- 不知道大家在学习GC机制的时候,有没有疑问:对象什么时候才被放到老年代去,具体是怎么做的?
- 我们知道,JVM产生一个对象的时候,首先将其放在新生代的Eden区中,并且随着young gc的产生,大部分的对象都被回收了,那么“熬过”这次GC的对象呢?JVM给了每个对象一个“年龄计数器”,所谓的年龄计数器就是指,这个对象熬过第一次GC,并且进入了Survivor区中,那么就将这个对象的年龄设为1,之后,每熬过一次GC,年龄+1,当这个值到达一个阀值(默认15,可通过-XX:MaxTenuringThreshold来设置)时,这个对象就会被移到老年代中。
- 动态对象年龄判断
- 为了更好的适应不同程序的内存状况,JVM也不是要去一个对象必须达到MaxTenuringThreshold设置的年龄阀值才能进入老年代。如果Survivor中的对象满足同年龄(比如N)对象所占空间达到了Survivor总空间的一半的时候,那么年龄大于或者等于N的对象都可以进入老年代,无需等待阀值。
- 空间分配担保
- 在学习JVM垃圾收集机制的时候,我们就知道了新生代采用复制算法,但是会造成空间的浪费,故而提出了一种“空间担保机制”来提高复制算法的空间利用率,使复制算法的浪费从50%降到了10%。而老年代的内存就充当了这个担保者,并且由于没有其他内存来担保老年代,所以老年代如果不想产生空间内存碎片那么只能使用“标记-整理”算法了。看到这,我们其实心里肯定有疑问——如何保证老年代有足够的空间来执行空间担保机制呢?Full GC,是否触发根据经验值判断,即使不允许担保失败,也有可能发生担保失败。
- 当发生YGC的时候,JVM都会检测之前每次晋升到老年代的对象的平均大小是否大于老年代的剩余内存空间,如果大于,则触发Full GC;如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则不会触发Full GC,反之,触发Full GC,保证老年代有足够的空间支持空间分配担保成功。
- 其实在每次GC发生的时候,我们也不知道到底会有多少对象被回收,又有多少对象能存活。故而只好取之前每次回收晋升到老年代的对象的平均值作为经验值来判断,但是如果某次GC后存活对象激增,任然会导致担保失败,那么只能重新进行Full GC了,虽然这样会绕个圈子,但是大部分情况下还是会将HandlePromotionFailure的值设为true,从而 避免Full GC过于频繁。换句话说,就是大部分情况,允许担保失败。