内存分配和回收策略
3.6 内存分配与回收策略
- Java技术体系中的自动内存管理包括两个方面的问题:给对象分配内存以及回收分配给对象的内存。
- 给对象分配内存,往大方向上来说,就是在堆上分配(也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也会直接分配在老年代。给对象分配内存的规则取决于使用了哪一种组合的收集器还有虚拟机中与内存有关的参数的设置。
3.6.1 对象优先分配在Eden区
- 大多数的情况下,对象的分配优先在Eden区,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象一般是具有朝生夕死的特点,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代(Major GC/Full GC):指发生在老年代的GC,出现了一次Major GC,经常会伴随着至少一次的Minor GC(非绝对),速度会比Minor GC慢10倍以上。
- 收集日志参数:-XX:+PrintGCDetails
- 告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
- 实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析。
3.6.2 大对象直接进入老年代
- 如果是大对象将会直接进入老年代。所谓老年代是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
- 朝生夕死的大对象是导致还有不少内存的时候还要提前触发垃圾收集以获取足够的连续空间来存储它们。
- 虚拟机提供了设置-XX:PretenureSizeThreshold参数,当大于这个设置值的对象直接在老年代分配,目的是为了避免在Eden区以及两个Survivor区之间发生大量的内存复制。
3.6.3 长期存活的对象将进入老年代
- 虚拟机是采用了分代收集的思想来管理内存,所以内存回收必须是要识别哪些对象放置到新生代,哪些对象放在老年代,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden区出生了并经过了一次Minor GC后仍然存在,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区种每经过一次Monir GC,年龄增加1岁。当年龄增到一定程度(默认是15),就会进入老年代。可以通过-XX:MaxTenuringThreshold参数设置。
3.6.4 动态对象年龄判定
- 虚拟机中并不是固定的要求对象的年龄必须要到MaxTenuringshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和占Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
3.6.5 空间分配担保
- 在进行Minor GC之前,虚拟机会首先检查老年代最大可用的连续空间是否大于新生代所有对象空间,如果成立,那么Monir GC可以确保是安全的。如果不成立,虚拟机会检查HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,但有风险;如果小于,将进行Full GC。
- 风险:
- 新生代使用的是复制收集算法,Eden空间和两个Survivor空间,但是只使用Eden空间和其中一个Survivor空间,当Minor GC之后,Survivor空间无法容纳Eden空间中存活的对象,那么这些对象将进入老年代。虚拟机无法提前知道每次Minor GC之后有多少对象存活,所以将记录历次的晋升到老年代的对象容量大小的平均值作为经验值,与剩余老年代空间比较,决定是否进行Full GC来让老年代腾出空间。如果是Minor GC失败了,那么将进行Full GC。
- 虽然会首先用经验值可能会产生担保失败,但还是将HandlePromotionFailure开关打开,避免Full GC过于频繁。