首页 Java 内存分配策略
文章
取消

Java 内存分配策略

概述

学习完了 Java 内存回收策略,我们在调过头来学习一下内存分配策略。内存分配策略大方向就是向怼上进行分配(但可能经过 JIT 编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的 Eden 区,如果启动了本地线程分配缓冲,将按线程优先分配在 TLAB 上分配。少数情况下也可能会直接分配在老年代中,分配到规则并不是百分之百固定的,其细节取决于当前使用的哪一块垃圾收集器组合,还有虚拟机中与内存相关的参数设置。

规则

对象优先先 Eden 分配

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

Minor GC 与 Full GC 的区别

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

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

大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组(比遇到大对象更加坏的消息是遇到一群“朝生夕死”的“短命大对象”),经常出现大对象融到导致内存还有不少空间时就需要提前出发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值得对象直接在老年代分配。这样做的目的是避免在 Eden 区以及两个 Survivor 区之间发生大量的内存复制。

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

前面说过新生代大部分都是“朝生夕死”的对象,如果某个对象长期存活就不如放入到老年代,减少对于新生代内存占用,减少新生代的 GC 次数。虚拟机会为每个对象都定义一个年龄计数器,新生代中的对象如果每熬过一次 GC,年龄计数器便会加 1,当达到一定年龄(默认 15 岁),便会晋升到老年代中。可以通过参数 -XX:MaxTenuringThreshold 来设置晋升到老年代对象的年龄。

为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代,无需等到 MaxtenuringThreshold 中要求的年龄。

空间分配担保

在发生 Minor GC 之前,虚拟机会检查老年中最大可用连续内存是否大于新生代中所有对象内存空间,如果大于那么这次 Minor GC 便是安全。否则则查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许则会查看老年代中最大可用连续内存是否大于历次晋升到老年代对象的平均大小,如果大于,将会会尝试进行一次 Minor GC,尽管这次是否风险的;如果小于或者 HandlePromotionFailure 不允许冒险,那么便进行一次 Full GC。

前面提到,新生代使用复制收集算法,单位了内存利用率,只是用其中一个 Survivor 空间作为轮换备份,当出现大量对象存活(最极端的情况就是内存回收后新生代中的所有对象都存活),就需要老年代进行担保,将 Survivor 中无法容纳的对象直接进入老年代。老年代担保,前提是老年代中有足够内存容纳剩余的对象,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间作比较,决定是否进行 Full GC 来让老年代腾出更多空间。

虽然担保失败时绕的圈子最大,但大部分情况下都还是将 HandlePromotionFailure 开关打开,避免 Full GC 过于频繁。

本文由作者按照 CC BY 4.0 进行授权

Java Reference & ReferenceQueue

JVM 故障排查常用命令