【Java 虚拟机笔记】内存分配策略相关整理
2019-02-27 本文已影响22人
58bc06151329
文前说明
作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
1. 概述
- Java 技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题。
- 给对象 分配 内存以及 回收 分配给对象的内存。
Minor GC
- 从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。
- 当虚拟机无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。
- 分配率越高,越频繁执行 Minor GC。
- 内存池被填满的时候,其中的内容全部会被复制,指针会从 0 开始跟踪空闲内存。
- Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。
- Eden 和 Survivor 区不存在内存碎片。
- 写指针总是停留在所使用内存池的顶部。
- 执行 Minor GC 操作时,不会影响到永久代。
- 从永久代到新生代的引用被当成 GC Roots。
- 从新生代到永久代的引用在标记阶段被直接忽略掉。
- 当虚拟机无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。
Major GC / Full GC
- 从老年代空间回收内存被称为 Major GC。
- 出现 Major GC,经常会伴随至少一次的 Minor GC(但非绝对,在 Parallel Scavenge 收集器的收集策略里有直接进行 Major GC 的策略选择)。
- MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。
- Full GC 的触发机制。
- 调用
System.gc()
,系统建议执行 Full GC,但是不必然执行。 - 老年代空间不足。
- 方法区空间不足。
- 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存。
- 由 Eden 区、From Survivor 区向 To Survivor 区复制时,对象大小大于 To Survivor 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- 调用
HotSpot 虚拟机 GC 的过程
- 初始阶段,新创建的对象被分配到 Eden 区,From/To Survivor 两块区域都为空。
- 当 Eden 区满 Minor GC 被触发。
- 经过扫描与标记,存活的对象被复制到 S0(From Survivor)区,不存活的对象被回收。
- 再一次 Minor GC,Eden 区和 S0 中没有引用的对象被回收,存活的对象被复制到 S1(To Survivor)区。
- 在上次 Minor GC 过程中移动到 S0 中的对象再复制到 S1 后其年龄要加 1。
- Eden 区 S0 区被清空,所有存活的数据都复制到了 S1 区,并且 S1 区存在着年龄不一样的对象。
- 再一次 MinorGC 则重复这个过程,Eden 区和 S1(To Survivor)区被清空。
- 再经过几次 Minor GC 之后,当存活对象的年龄达到一个阈值之后(可通过参数配置,这里设置为 8),就会被从新生代 Promotion 到老年代。
- 随着 Minor GC 一次又一次的进行,不断会有新的对象被 Promote 到老年代。
- 最终,Major GC 将会在老年代发生,老年代的空间将会被清除和压缩。
2. 对象优先在 Eden 分配
- 大多数情况下,对象在新生代 Eden 区中分配内存,但 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
- 通过 -XX:PrintGCDetails 参数打印 GC 日志。
- 例如使用 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
* -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
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 (Allocation Failure) [DefNew: 7307K->375K(9216K), 0.0023729 secs] 7307K->6519K(19456K), 0.0024046 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
//Heap
// def new generation total 9216K, used 4635K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// eden space 8192K, 52% used [0x00000000fec00000, 0x00000000ff029140, 0x00000000ff400000)
// from space 1024K, 36% used [0x00000000ff500000, 0x00000000ff55dd40, 0x00000000ff600000)
// to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
// tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
// the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
// Metaspace used 3357K, capacity 4494K, committed 4864K, reserved 1056768K
// class space used 325K, capacity 386K, committed 512K, reserved 1048576K
- 上例代码中设定最大内存空间为 20M,新生代 10M,老年代 10M。
- DefNew 表示为 Serial 收集器。
- 创建 allocation1、allocation2、allocation3 对象定义为各 2M,一共 6M。allocation4 对象为 4M。
- 创建 allocation4 对象所需内存相加已经大于 Eden 区空间,产生一次 Minor GC。
- 将 allocation1、allocation2、allocation3 移至老年代,tenured generation total 10240K, used 6144K,老年代使用了 6M。
- Eden 区空闲出来,再将 allocation4 放入新生代,def new generation total 9216K, used 4635K,新生代使用了 4M。
3. 大对象直接进入老年代
- 大对象是指需要大量连续内存空间的 Java 对象,最典型的大对象就是很长的字符串以及数组。
- 大对象对虚拟机内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来 " 安置 " 它们。
- 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数来设置大对象的界限,大于此值则直接分配至老年代。
- 例如使用 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)。
/**
* -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1;
allocation1 = new byte[4 * _1MB];
}
}
//Heap
// def new generation total 9216K, used 5423K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// eden space 8192K, 66% used [0x00000000fec00000, 0x00000000ff14bf48, 0x00000000ff400000)
// from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
// to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
// tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
// the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
// Metaspace used 3340K, capacity 4494K, committed 4864K, reserved 1056768K
// class space used 322K, capacity 386K, committed 512K, reserved 1048576K
//-XX:PretenureSizeThreshold=3145728
//Heap
// def new generation total 9216K, used 1327K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// eden space 8192K, 16% used [0x00000000fec00000, 0x00000000fed4bf38, 0x00000000ff400000)
// from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
// to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
// tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
// the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
// Metaspace used 3333K, capacity 4494K, committed 4864K, reserved 1056768K
// class space used 319K, capacity 386K, committed 512K, reserved 1048576K
- 上例代码中设定最大内存空间为 20M,新生代 10M,老年代 10M。
- 未设置 -XX:PretenureSizeThreshold=3145728 前,def new generation total 9216K, used 1327K,内存使用了新生代空间。tenured generation total 10240K, used 0K,老年代为 0K。
- 设置 -XX:PretenureSizeThreshold=3145728 后,tenured generation total 10240K, used 4096K,内存占用直接分配到了老年代空间。
4. 长期存活的对象将进入老年代
- Minor 的主要对象是新生代,对象在 Minor 后并不都会直接进入老年代,除非 Survivor 空间不够,否则此存活对象会经过多次 Minor GC 后还生存的话才进入老年代,而虚拟机默认的 Minor GC 次数为 15 次,可通过 -XX:MaxTenuringThreshold 进行次数设置。
- 例如使用 JDK 1.6 环境,Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)。
/**
* -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[1 * _1MB / 4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];// 第一次 Minor GC
allocation3 = null;
allocation3 = new byte[4 * _1MB];// 第二次 Minor GC
}
}
//-XX:MaxTenuringThreshold=15
//[GC [DefNew: 5463K->456K(9216K), 0.0053475 secs] 5463K->4552K(19456K), 0.0053895 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
//[GC [DefNew: 4964K->456K(9216K), 0.0010450 secs] 9060K->4552K(19456K), 0.0010711 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Heap
// def new generation total 9216K, used 4775K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
// eden space 8192K, 52% used [0x00000000f9a00000, 0x00000000f9e37be0, 0x00000000fa200000)
// from space 1024K, 44% used [0x00000000fa200000, 0x00000000fa272170, 0x00000000fa300000)
// to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
// the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
// compacting perm gen total 21248K, used 3497K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
// the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb16ab58, 0x00000000fb16ac00, 0x00000000fc2c0000)
//-XX:MaxTenuringThreshold=1
//[GC [DefNew: 5463K->458K(9216K), 0.0038139 secs] 5463K->4554K(19456K), 0.0038559 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[GC [DefNew: 4967K->0K(9216K), 0.0007399 secs] 9063K->4554K(19456K), 0.0007592 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Heap
// def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
// eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e227f0, 0x00000000fa200000)
// from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa2001e0, 0x00000000fa300000)
// to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation total 10240K, used 4554K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
// the space 10240K, 44% used [0x00000000fa400000, 0x00000000fa8729a8, 0x00000000fa872a00, 0x00000000fae00000)
// compacting perm gen total 21248K, used 3598K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
// the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb1840b0, 0x00000000fb184200, 0x00000000fc2c0000)
- 上例代码中设定最大内存空间为 20M,新生代 10M,老年代 10M。
- 设置 -XX:MaxTenuringThreshold=15,必须经过 15 次 Minor GC 才能晋升到老年代,因此经过两次 Minor GC 后,新生代仍有 4775K,即 allocation1 对象的仍在 From Survivor 区域。
- 设置 -XX:MaxTenuringThreshold=1,第二次 Minor GC 时,新生代已经清空,allocation1 对象因为年龄为 1 进入老年代。
5. 动态对象年龄判定
- 为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果 在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或者等于该年龄的对象直接可以进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
- 例如使用 JDK 1.6 环境,Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)。
/**
* -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[1 * _1MB / 4];
allocation2 = new byte[2 * _1MB / 4];//注释前后对比(不注释,则 allocation1 + allocation2 大于 Survivor 空间的一半)
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];// 第一次 Minor GC
allocation4 = new byte[4 * _1MB];// 第二次 Minor GC
}
}
//allocation2 被注释的情况下
//[GC [DefNew: 5463K->456K(9216K), 0.0040297 secs] 5463K->4552K(19456K), 0.0040705 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[GC [DefNew: 4964K->456K(9216K), 0.0035523 secs] 9060K->8648K(19456K), 0.0035859 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Heap
// def new generation total 9216K, used 4690K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
// eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e227e8, 0x00000000fa200000)
// from space 1024K, 44% used [0x00000000fa200000, 0x00000000fa2721b8, 0x00000000fa300000)
// to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation total 10240K, used 8192K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
// the space 10240K, 80% used [0x00000000fa400000, 0x00000000fac00020, 0x00000000fac00200, 0x00000000fae00000)
// compacting perm gen total 21248K, used 3507K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
// the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb16ce88, 0x00000000fb16d000, 0x00000000fc2c0000)
//allocation2 没有注释的情况下
//[GC [DefNew: 5975K->968K(9216K), 0.0042617 secs] 5975K->5064K(19456K), 0.0043078 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//[GC [DefNew: 5476K->0K(9216K), 0.0030553 secs] 9572K->9160K(19456K), 0.0030796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Heap
// def new generation total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
// eden space 8192K, 51% used [0x00000000f9a00000, 0x00000000f9e22750, 0x00000000fa200000)
// from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200330, 0x00000000fa300000)
// to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation total 10240K, used 9159K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
// the space 10240K, 89% used [0x00000000fa400000, 0x00000000facf1f20, 0x00000000facf2000, 0x00000000fae00000)
// compacting perm gen total 21248K, used 3524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
// the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb171940, 0x00000000fb171a00, 0x00000000fc2c0000)
- 上例代码中设定最大内存空间为 20M,新生代 10M,老年代 10M。
- 当只有 allocation1 占据 Survivor 时,还不到一半空间,所以还停留在 Survivor 空间。
- 当 allocation2 也存在时,执行第一次 Minor GC 的时候 allocation1 和 allocation2 同时被移动到 Survivor 区域,但是因为 allocation1 和 allocation2 的总和已经达到了 Survivor 的一半,所以立刻被移动到老年代。
6. 空间分配担保
- 在 JDK 1.6 Update 24 之前,可以通过 HandlePromotionFailure 设置是否允许担保失败。
- 如果允许,那么会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试再进行一次 Minor GC,尽管这次 Minor GC 是有风险的(当出现大量对象在 Minor GC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接进入老年代)。
- 如果小于或者 HandlePromotionFailure 设置不允许冒险,那么改为进行一次 Full GC。
- JDK 1.6 Update 24 之后,在发生 Minor GC 之前,虚拟机会先检查老年代的连续空间大小是否大于新生代对象总大小或者历次晋升的平均大小,如果是则进行 Minor GC,否则将进行 Full GC。
- Parallel Scavenge 收集器与其他收集器在空间分配担保上有一点差别,正常是在 Minor GC 前进行检查, 而 Parallel Scavenge 收集器在 Minor GC 后也会进行检查。
- 例如使用 JDK 1.6 环境 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)。
/**
* -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
// Minor GC allocation1、allocation2 和 allocation3 进入老年代
allocation4 = new byte[2 * _1MB];// 第一次 Minor GC
allocation5 = new byte[2 * _1MB];
allocation6 = new byte[2 * _1MB];
allocation1 = null;
allocation2 = null;
allocation3 = null;
allocation7 = new byte[2 * _1MB];// 第二次 Minor GC
}
}
//[GC [DefNew: 7255K->202K(9216K), 0.0047298 secs] 7255K->6346K(19456K), 0.0047709 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
//[GC [DefNew: 6849K->6849K(9216K), 0.0000174 secs][Tenured: 6144K->6346K(10240K), 0.0046928 secs] 12993K->6346K(19456K), [Perm : 3568K->3568K(21248K)], 0.0047640 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//Heap
// def new generation total 9216K, used 2185K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
// eden space 8192K, 26% used [0x00000000f9a00000, 0x00000000f9c224e8, 0x00000000fa200000)
// from space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
// tenured generation total 10240K, used 6346K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
// the space 10240K, 61% used [0x00000000fa400000, 0x00000000faa32b98, 0x00000000faa32c00, 0x00000000fae00000)
// compacting perm gen total 21248K, used 3598K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
// the space 21248K, 16% used [0x00000000fae00000, 0x00000000fb183b78, 0x00000000fb183c00, 0x00000000fc2c0000)
- 上例代码中设定最大内存空间为 20M,新生代 10M,老年代 10M。
- allocation1、allocation2、allocation3 占用 Eden 区域的 6M 空间。
- allocation4 分配内存时,Eden 区域空间不足,触发第一次 Minor GC。
- Minor GC 结束后,allocation1、allocation2 和 allocation3 进入老年代,此时老年代剩余空间为 4M。
- allocation7 分配内存时,此时新生代已经放入 allocation4、allocation5 和 allocation6。
- 无法放入 allocation7 即将触发第二次 Minor GC。
- 触发前进行检查,此时老年代剩余空间为不足 4M,新生代对象总大小为 6M,历次晋升的平均大小为 6M,因此改为进行一次 Full GC。
- Full GC 后,新生代对象全部晋升,老年代因为 allocation1、allocation2、allocation3 被赋空,内存得到回收,因为晋升的 allocation4、allocation5 和 allocation6 ,内存大小还是 6M 左右。
- 堆总量由 12M 降低至 6M。
参考资料
https://blog.csdn.net/weixin_39788856/article/details/80388002
https://www.cnblogs.com/wcd144140/p/5649553.html
https://blog.csdn.net/v123411739/article/details/78941793