JVM内存分配

总的来说,JVM管理的内存包括堆内存和非堆内存。堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常量池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

因此这里所说的内存分配是指堆内存的分配,即我们程序中生成的对象的分配。以下所述针对的是HotSpot虚拟机

1、Java堆结构

以HotSpot为例,如下图:

1.1、年轻代

年轻代(或称新生代,Young、New)通常用来放新生成的对象(但不绝对,如可以通过-XX:PretenureSizeThreadshold配置将大对象直接分配在老年代,仅对Serial和ParNew收集器有效)。年轻代的目标就是尽可能快速地收集掉那些生命周期短的对象。

年轻代分3个区,1个Eden区,2个Survivor区(from 和 to),但每次只使用Eden和一个Survivor区,另一个Survivor区空着。空Survivor区用来放MinorGC时从Eden和在使用的Survivor区中复制来的活着的对象。

针对年轻代的GC为Minor GC或称Young GC,在Eden剩余空间不足以分配新对象时触发

1.2、老年代

老年代(Old、Tenure)通常用来存放从年轻代复制过来的对象(也可直接在老年代分配对象)。老年代中存放的是一些生命周期较长的对象。

针对老年代的GC为Major GC或称Full GC,在老年代剩余空间不足以容纳新对象时触发。Major GC经常会伴随至少一次的Minor GC(但非绝对,如下面所说的HandlePromotionFailure策略,有可能直接进行Major GC;Parallel Scavenge收集器提供了直接进行Major GC的策略选择),Major GC一般比Minor GC慢10倍以上。

 

永久代

通常还可以听到永久代(PermGen space)的概念,其实它并不在Java堆。有时永久代又被称为元空间(Metaspace)或方法区,比较乱。它们的区别是:方法区为JVM的规范,永久代(JDK1.7及之前)、元空间(JDK1.8起)分别是方法区在HotSpot中的一种实现,对于其他类型的虚拟机实现,如 JRockit(BEA)、J9(IBM),其并没有这些概念。

2、堆内存分配过程  

对象的内存分配,绝大部分都是在堆上分配(少数经过JIT编译后被拆散为标量类型并间接在栈上分配),这里介绍在堆上的分配。

生成对象(为对象分配内存)的过程如下:

  1. 首先看Eden剩余空间是否足够分配该对象,若够则直接在Eden分配;
  2. 否则进行MinorGC:将Eden和在使用的Survivor区中活着的对象复制到另一个Survivor区,并回收Eden和使用着的Survivor区,以后另一个Survivor成为使用的Survivor区。MinorGC完成后把对象分配到Eden。
  3. 若另一个Survivor区不能完全容纳复制过来的对象,则能放下的放入该Survivor,把放不下的放到老年代(即进行分配担保);
  4. 若老年代剩余空间不够了则进行Full GC,
  5. 若Full GC后仍不够则抛出OOM异常。

具体可以分为:

  • 分配过程(优先在Eden分配,分配不了则执行MinorGC)
  • 分配担保(MinorGC后若Survivor放不下的存活对象移到老年代,老年代仍不够则进行MajorGC,MajorGC后仍不够则OOM异常)
  • 提前移动的配置(大对象、对象年龄、动态年龄)

以下示例所用JDK为1.8.0_45。GC日志格式含义见理解GC日志

2.1、对象优先在Eden区分配

对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

示例代码:

 1 package cn.edu.buaa.jvmbook;
 2 
 3 /**
 4  * @author zsm
 5  * @date 2017年1月2日 下午10:33:12
 6  */
 7 /*
 8  * 代码3-5
 9  -Xms20M -Xmx20M -Xmn10M
10  -XX:+UseSerialGC(client模式运行,收集器为Serail+SerialOld)
11  -XX:+PrintGCDetails
12  -XX:SurvivorRatio=8
13  *
14  */
15 
16 public class _3_5_YoungGeneration_MinorGC {
17     private static final int _1MB = 1024 * 1024;
18 
19     public static void main(String[] args) {
20         // TODO Auto-generated method stub
21         byte[] allocation1, allocation2, allocation3, allocation4;
22         allocation1 = new byte[2 * _1MB];
23         allocation2 = new byte[2 * _1MB];
24         allocation3 = new byte[2 * _1MB];
25         allocation4 = new byte[4 * _1MB];// 出现一次MinorGC
26     }
27 }
View Code

GC日志:

[GC (Allocation Failure) [DefNew: 7291K->508K(9216K), 0.0035043 secs] 7291K->6652K(19456K), 0.0035385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4687K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  49% used [0x00000000ff500000, 0x00000000ff57f390, 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 2559K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 274K, capacity 386K, committed 512K, reserved 1048576K

说明:新生代可用的空间为9M = 8M(Eden容量) + 1M(一个survivor容量),分配完allocation1、allocation2、allocation3之后,无法再分配allocation4,会发生分配失败,则需要进行一次Minor GC,survivor to区域的容量为1M,无法容纳总量为6M的三个对象,则会通过担保机制将allocation1、allocation2、allocation3转移到老年代,然后再将allocation4分配在Eden区。

2.2、空间分配担保

空间分配担保(Handle Promotion,意译成对象存储位置晋升还差不多,分配担保...什么鬼)即让老年代空间来存放年轻代中的对象,其前提是老年代还有容纳的空间。

在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明Minor GC是安全的,否则,虚拟机需要查看HandlePromotionFailure的值,看是否允许担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC。

以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。

该策略的意义之一在于减少不必要的MinorGC。具体的流程图如下:

示例代码:

 1 /**
 2  * 
 3  */
 4 package cn.edu.buaa.jvmbook;
 5 
 6 /** 
 7 * @author  zsm
 8 * @date 2017年1月5日 下午3:31:38
 9 */
10 public class _3_9_HandlePrpmotion {
11     private static final int _1MB = 1024 * 1024;
12 
13     /*
14      * 代码3-9 -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+HandlePromotionFailure
15      */
16     /*
17     -Xms20M -Xmx20M -Xmn10M 
18     -XX:SurvivorRatio=8 
19     -XX:+PrintGCDetails
20     -XX:+UseSerialGC
21     -XX:-HandlePromotionFailure
22      */
23     public static void main(String[] args) {
24         // TODO Auto-generated method stub
25         byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7, allocation8;
26         allocation1 = new byte[2 * _1MB];
27         allocation2 = new byte[2 * _1MB];
28         allocation3 = new byte[2 * _1MB];
29         allocation1 = null;
30         allocation4 = new byte[2 * _1MB];
31         allocation5 = new byte[2 * _1MB];
32         allocation6 = new byte[2 * _1MB];
33         allocation4 = null;
34         allocation5 = null;
35         allocation6 = null;
36         allocation7 = new byte[2 * _1MB];
37     }
38 
39 }
View Code

GC日志(没在老JDK上跑,可能与期望的有点出入):

[GC (Allocation Failure) [DefNew: 7291K->508K(9216K), 0.0042361 secs] 7291K->4604K(19456K), 0.0042750 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 6811K->508K(9216K), 0.0007200 secs] 10907K->4604K(19456K), 0.0007436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 2638K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,  49% used [0x00000000ff400000, 0x00000000ff47f0e0, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 2560K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 274K, capacity 386K, committed 512K, reserved 1048576K

说明:

发生了两Minor次GC,第一次发生在给allocation4分配内存空间时,由于老年代的连续可用空间大于存活的对象总和,所以allocation2、allocation3将会进入老年代,allocation1的空间将被回收,allocation4分配在新生代;第二次发生在给allocation7分配内存空间时,此次GC将allocation4、allocation5、allocation6所占的内存全部回收。最后,allocation2、allocation3在老年代,allocation7在新生代。 

2.3、大对象直接分配在老年代

需要大量连续内存空间的Java对象称为大对象,大对象的出现会导致提前触发垃圾收集以获取更大的连续的空间来进行大对象的分配。虚拟机提供了-XX:PretenureSizeThreadshold(仅对Serial和ParNew收集器有效)参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。这样做的目的是避免在Eden区和两个 Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

示例代码:

 1 package cn.edu.buaa.jvmbook;
 2 
 3 /**
 4  * @author zsm
 5  * @date 2017年1月2日 下午11:08:58
 6  * @version 1.0
 7  * @parameter
 8  * @return
 9  */
10 /*
11  * 代码3-6 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC(client模式运行,收集器为Serail+SerialOld) -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728(3M,此参数只能以字节为单位)
12  *
13  */
14 // -XX:PretenureSizeThreshold=3145728参数,大对象直接进入老年代,参数只对Serial和ParNew两种收集器有效
15 public class _3_6_BigObjectOldGeneration {
16     private static final int _1MB = 1024 * 1024;
17 
18     public static void main(String[] args) {
19         // TODO Auto-generated method stub
20         byte[] allocation1 = new byte[4 * _1MB];// 出现一次MinorGC
21 
22     }
23 
24 }
View Code

GC日志:

Heap
 def new generation   total 9216K, used 1311K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed47f80, 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 2559K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 274K, capacity 386K, committed 512K, reserved 1048576K

说明:4MB对象超过PretenureSizeThreshold阈值(3MB),直接分配在了老年代。

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

每个对象有一个对象年龄计数器,与对象头标记字中的GC分代年龄对应。对象出生在Eden区、经过一次Minor GC后仍然存活,并能够被Survivor容纳,设置年龄为1,对象在Survivor区每次经过一次Minor GC,年龄就加1,当年龄达到一个阈值(默认15),下次触发Minor GC时就被移到老年代(即配置的是最多被在Survivor间移动几次,达到阈值后下一次GC时不移动而是直接晋升了)。虚拟机提供了-XX:MaxTenuringThreshold来进行设置。

示例代码:

 1 package cn.edu.buaa.jvmbook;
 2 
 3 /**
 4  * @author zsm
 5  * @date 2017年1月2日 下午11:19:41
 6  * @version 1.0
 7  * @parameter
 8  * @return
 9  */
10 /*
11  * 代码3-7 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC(client模式运行,收集器为Serail+SerialOld) -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
12  *
13  */
14 
15 // -XX:MaxTenuringThreshold用来调节对象在Survivor中“熬过”多少次MinorGC后进入老年代
16 public class _3_7_LongLiveOldGeneration {
17     private static final int _1MB = 1024 * 1024;
18     /*
19     -Xms20M -Xmx20M -Xmn10M
20     -XX:+UseSerialGC
21     -XX:+PrintGCDetails
22     -XX:SurvivorRatio=8 
23     -XX:MaxTenuringThreshold=1
24     -XX:+PrintTenuringDistribution
25      */
26     public static void main(String[] args) {
27         // TODO Auto-generated method stub
28         byte[] allocation1, allocation2, allocation3;
29         allocation1 = new byte[_1MB / 4];
30         // 何时进入老年代取决于XX:MaxTenuringThreshold设置
31         allocation2 = new byte[4 * _1MB];
32         allocation3 = new byte[4 * _1MB];
33         allocation3 = null;
34         allocation3 = new byte[4 * _1MB];
35     }
36 
37 }
View Code

GC日志:

[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1)- age   1:     783256 bytes,     783256 total: 5499K->764K(9216K), 0.0024536 secs] 5499K->4860K(19456K), 0.0024878 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1): 4860K->0K(9216K), 0.0006581 secs] 8956K->4860K(19456K), 0.0006864 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4860K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffabf120, 0x00000000ffabf200, 0x0000000100000000)
 Metaspace       used 2560K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 274K, capacity 386K, committed 512K, reserved 1048576K

说明:发生了两次Minor GC,第一次是在给allocation3进行分配的时候会出现一次Minor GC,此时survivor区域不能容纳allocation2,但是可以容纳allocation1,所以allocation1将会进入survivor区域并且年龄为1,达到了阈值,将在下一次GC时晋升到老年代,而allocation2则会通过担保机制进入老年代。第二次发生GC是在第二次给allocation3分配空间时,这时,allocation1的晋升到老年代,此次GC也可以清理出原来allocation3占据的4MB空间,将allocation3分配在Eden区。所以,最后的结果是allocation1、allocation2在老年代,allocation3在Eden区。

2.5、动态判断对象年龄

对象的年龄到达了MaxTenuringThreshold可以进入老年代,同时,如果在survivor区中相同年龄所有对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。

示例代码:

 1 /**
 2  * 
 3  */
 4 package cn.edu.buaa.jvmbook;
 5 
 6 /** 
 7 * @author  zsm
 8 * @date 2017年1月5日 下午3:00:16
 9 */
10 /**
11  * @author zsm
12  *
13  */
14 public class _3_8_DynamicObjectAgeJudge {
15 
16     private static final int _1MB = 1024 * 1024;
17 
18     /*
19      * 代码3-8 -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
20      */
21     /*
22     -Xms20M -Xmx20M -Xmn10M
23     -XX:SurvivorRatio=8 
24     -XX:+PrintGCDetails
25     -XX:+UseSerialGC
26     -XX:MaxTenuringThreshold=15
27     -XX:+PrintTenuringDistribution
28      */
29     public static void main(String[] args) {
30         // TODO Auto-generated method stub
31         byte[] allocation1, allocation2, allocation3, allocation4;
32         allocation1 = new byte[_1MB / 4];
33         allocation2 = new byte[_1MB / 4];
34         allocation3 = new byte[4 * _1MB];
35         allocation4 = new byte[4 * _1MB];
36         allocation4 = null;
37         allocation4 = new byte[4 * _1MB];
38     }
39 
40 }
View Code

GC日志:

[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)- age   1:    1045416 bytes,    1045416 total: 5755K->1020K(9216K), 0.0027553 secs] 5755K->5116K(19456K), 0.0028017 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
[GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15): 5116K->0K(9216K), 0.0009216 secs] 9212K->5116K(19456K), 0.0009399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5116K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffaff130, 0x00000000ffaff200, 0x0000000100000000)
 Metaspace       used 2559K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 274K, capacity 386K, committed 512K, reserved 1048576K

说明:发生了两次Minor GC,第一次发生在给allocation4分配内存时,此时allocation1、allocation2将会进入survivor区,而allocation3通过担保机制将会进入老年代。第二次发生在第二次给allocation4分配内存时,此时,survivor区的allocation1、allocation2达到了survivor区容量的一半,将会进入老年代,此次GC可以清理出allocation4原来的4MB空间,并将allocation4分配在Eden区。最终,allocation1、allocation2、allocation3在老年代,allocation4在Eden区。、

3、堆内存分配参数设置

见 HotSpot JVM各区域内存分配参数设置-MarchOn

  • -Xmx3550m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为3550M。
  • -Xms3550m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
  • -XX:NewSize=1024m:设置年轻代初始值为1024M。
  • -XX:MaxNewSize=1024m:设置年轻代最大值为1024M。
  • -Xmn2g:设置年轻代大小为2G,相当于将上面两个都设为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。
  • -XX:SurvivorRatio=4:设置年轻代中Eden区与一个Survivor区的比值。表示Edgen为一个Survivor的4倍,即1个Survivor区占整个年轻代大小的1/6。默认为8。
  • -XX:NewRatio=4:设置老年代与年轻代(包括1个Eden和2个Survivor区)的比值。表示老年代是年轻代的4倍。若按官方推荐年轻代占整个堆空间3/8,则对应这里的值为5/3
  • -XX:PretenureSizeThreadshold=1024:设置让大于此阈值的对象直接分配在老年代(只对Serial、ParNew收集器有效),单位为字节
  • -XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区移动了7次那下次MinorGC时就进入老年代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,可以在某种程度上提高服务稳定性。

4、参考资料

[1]《深入理解Java虚拟机——JVM高级特性与最佳实践》

[2]http://www.cnblogs.com/leesf456/p/5218594.html

[3]Java堆内存设置原理

 

posted @ 2017-01-05 14:31  March On  阅读(1045)  评论(0编辑  收藏  举报
top last
Welcome user from
(since 2020.6.1)