IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

JVM内存分配与回收策略

本机暂存

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

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

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

   下面是最普遍的内存分配规则,并通过代码去验证这些规则。下面的代码在测试时使用Client模式虚拟机运行,没有手工指定收集器组合,验证的是使用Serial / Serial Old收集器下(ParNew / Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。

   虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。在实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析,不过本实验的日志并不多,直接阅读就能看得很清楚。

   1. 对象优先在Eden分配

   执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、2、3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1、2、3占用)。

   /**对象优先在Eden分配

   * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

   */

   private static final int _1MB = 1024*1024;

   public static void testAlloction() {

   byte[] allocation1, allocation2, allocation3, allocation4;

   allocation1 = new byte[2 * _1MB];

   allocation2 = new byte[2 * _1MB];

   allocation3 = new byte[2 * _1MB];

   allocation4 = new byte[4 * _1MB];

   }

   运行结果:

   [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   Heap

   def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)

   eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)

   from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000)

   to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)

   tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)

   the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)

   compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)

   the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)

   No shared spaces configured.

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

   所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息(遇到一群“朝生夕灭”的“短命大对象”,写程序应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

   虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

   执行testPretenureSizeThreshold()方法后,我们看到Eden空间几乎没有被使用,而老年代10MB的空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728B,这个参数不能与-Xmx之类的参数一样直接写3MB),因此超过3MB的对象都会直接在老年代中进行分配。

   /**大对象直接进入老年代

   * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

   * -XX:PretenureSizeThreshold=3145728

   */

   public static void testPretenureSizeThreshold() {

   byte[] allocation;

   allocation = new byte[4 * _1MB];

   }

   运行结果:

   Heap

   def new generation total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)

   eden space 8192K, 8% used [0x029d0000, 0x02a77e98, 0x031d0000)

   from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)

   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)

   tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)

   the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)

   compacting perm gen total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)

   the space 12288K, 17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)

   No shared spaces configured.

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

   虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

   testTenuringThreshold()方法中allocation1对象需要256KB的内存空间,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后会非常干净地变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时候新生代仍然有404KB的空间被占用。

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

   * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

   * -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=15分别设置为1和15

   */

   public static void testTenuringThreshold() {

   byte[] allocation1, allocation2, allocation3;

   allocation1 = new byte[_1MB / 4];

   allocation2 = new byte[4 * _1MB];

   allocation3 = new byte[4 * _1MB];

   allocation3 = null;

   allocation3 = new byte[4 * _1MB];

   }

   以MaxTenuringThreshold=1的参数设置来运行的结果:

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 1 (max 1)

   - age 1: 414664 bytes, 414664 total

   : 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 1 (max 1)

   : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   Heap

   def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)

   eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)

   from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)

   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)

   tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)

   the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)

   compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)

   the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)

   No shared spaces configured.

   以MaxTenuringThreshold=15的参数设置来运行的结果:

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 15 (max 15)

   - age 1: 414664 bytes, 414664 total

   : 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 15 (max 15)

   - age 2: 414520 bytes, 414520 total

   : 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   Heap

   def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)

   eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)

   from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000)

   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)

   tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)

   the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)

   compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)

   the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)

   No shared spaces configured.

   4. 动态对象年龄判定

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

   执行testTenuringThreshold2()方法,并设置参数-XX: MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已经达到了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。我们只要注释掉其中一个对象的new操作,就会发现另外一个不会晋升到老年代中去了。

   /**动态对象年龄判定

   * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

   * -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=15

   */

   public static void testTenuringThreshold2() {

   byte[] allocation1, allocation2, allocation3, allocation4;

   allocation1 = new byte[_1MB / 4];

   allocation2 = new byte[_1MB / 4];

   allocation3 = new byte[4 * _1MB];

   allocation4 = new byte[4 * _1MB];

   allocation4 = null;

   allocation4 = new byte[4 * _1MB];

   }

   运行结果:

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 1 (max 15)

   - age 1: 676824 bytes, 676824 total

   : 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]

   [GC [DefNew

   Desired Survivor size 524288 bytes, new threshold 15 (max 15)

   : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   Heap

   def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)

   eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)

   from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)

   to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)

   tenured generation total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)

   the space 10240K, 46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)

   compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)

   the space 12288K, 17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)

   No shared spaces configured.

   5. 空间分配担保

   在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

   /**空间分配担保

   * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

   * -XX:-HandlePromotionFailure控制是否允许担保失败

   */

   public static void testHandlePromotion() {

   byte[] allocation1, allocation2, allocation3, allocation4,allocation5,allocation6,allocation7;

   allocation1 = new byte[2 * _1MB];

   allocation2 = new byte[2 * _1MB];

   allocation3 = new byte[2 * _1MB];

   allocation1 = null;

   allocation4 = new byte[2 * _1MB];

   allocation5 = new byte[2 * _1MB];

   allocation6 = new byte[2 * _1MB];

   allocation4 = null;

   allocation5 = null;

   allocation6 = null;

   allocation7 = new byte[2 * _1MB];

   }

   以HandlePromotionFailure = false的参数设置来运行的结果: [GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]

   [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   以MaxTenuringThreshold= true的参数设置来运行的结果:

   [GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

   [GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. Java 6 JVM参数选项大全(中文版) (累计阅读 5,119)
  2. GC与JS内存泄露 (累计阅读 4,739)
  3. Jetty线程“互锁”导致数据传输性能降低问题分析 (累计阅读 4,476)
  4. 深入理解PHP原理之Session Gc的一个小概率Notice (累计阅读 4,329)
  5. JVM内存结构 (累计阅读 4,234)
  6. JVM的GC简介和实例 (累计阅读 3,902)
  7. JVM垃圾收集器 (累计阅读 3,885)
  8. JAVA虚拟机简介 (累计阅读 3,708)
  9. jvm垃圾回收 (累计阅读 3,694)
  10. JVM垃圾收集算法 (累计阅读 3,629)