内存分配和回收策略以及虚拟机参数配置


前言

1、对象优先在Eden分配(如果小于Survivor,先进入Survivor)

复制算法还记得吧,就是说的商业虚拟机关于新生代的垃圾收集就是采用的复制算法 将内存分为3分分别为8:1;1 那么Eden 就代表着8份

1
2
3
4
5
6
7
8
9
10

Java堆大小为20M 不可扩展(Xms表示初始Java堆大小 Xmx为Java堆最大 这里设置相等,就表明不可以扩展)

Xmn 表示分给新生代 (下面表示分给新生到10M,那么剩余的就分配给了老年代)

XX:SurvivorRatio 表示新生代中Eden和Survivor 比为8:1 其实从下面的代码的输出结果也能够看到的


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

1.1、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.hlj.jvm.GC;

/**
* @Description
* @Author HealerJean
* @Date 2018/4/9 下午6:23.
*/
public class GC {
private static final int _1MB = 1024 * 1024;

/**
* 测试JVM内存的分配,新生代和老年代的分区
* 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* 输出gc日志, 堆内存初始化大小20M,堆内存最大20M,新生代大小10M,那么剩余分配给老年代就是10M, 输出GC的详细日志,
* Eden的区域是一个survivor区域的8倍 就是说比为 8:1 也就是说新生代做多能后去到 8M
*/
public static void testAllocation()
{
byte[] allocation1, allocation2, allocation3, allocation4;

allocation1 = new byte[2 * _1MB]; //申请两兆
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
//这里我们再eden已经申请了6M的空间,而实际上新生代大小是EDEN + 一个survivor Eden=8M survivor两块分别1M(因为复制算法的原因)
allocation4 = new byte[4 * _1MB];
}

public static void main(String[] args)
{
testAllocation();

}
}

运行观察GC

关键性解释:当分到allocation4的时候回发生一次Minor GC

当运行到allocation4的时候,我们已经在新生代的Eden中添加了6M的内存,如果再添加4M的话,很明显会超出新生代所给的Eden最大范围8,这个时候,就会提前发生Minor GC(这三个对象是存活的,不是清除哦) ,GC的时候还会发现妈的巴子Survivor才1M,明显放不下,所以只能通过分配担保机制提前转移到老年代中去了。精彩,老子想鼓鼓掌 。哈哈

那么这个时候 allocation4 所需要的4M就放到了Elen中,老年代中放6M(被allocation1、allocation2、allocation3占用) 具体看下面日志就知道了

6651k->148k(9216k)Gc前新生代从6M(Edeb)->GC后148K(Survivor)

下面表示的在发生GC之后的内存分布,一定要注意,这里仅仅是自己触发,并没有进行虚拟机回收的日志,(没有full gc也就是不执行老年代GC,)
8192*51% = 4M,就是说有4M放到了Eden中

1024*14% = 148k,表示最后survivor中放置148K

下面 space 10240k 60% 表示在老年代中放置了6M(没错)

WX20180409-190537@2x

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

所谓的大对象其实就是需要大量连续内存空间的JAVA对象,最典型的就是那种很长的字符串和数组,大对象对于虚拟机来说是一个坏消息,(更要命的是遇到短命大对象,所以写程序的时候要尽量避免) 经常出现大对象,容易导致内存还有很多空间,就提前触发垃圾收集来获取足够的空间(就比如1中的)

JAVA虚拟机提供 XX:PretenureSizeThreshold参数用来设置大于它的直接放到老年代分配,这样的目的是避免了Eden和两个Survivor区直接发送大量的内存复制

1
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  -XX:PretenureSizeThreshold:3145728
1
2
3
4
5
6
7
8
9
10
/**
2、大对象应该直接放到老生代中
*/

public static void testPretenureSizeThreshold(){
byte[] allocation;
allocation = new byte[4 * _1MB];
}


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

虚拟机采用的是分代收集算法,java虚拟机就能够知道哪些在新生代中,哪些在老年代中。其实他对每个对象的年龄都定义了一个计数器,当对象在Ede出生并经历过地第一次Minor GC后能够进入Survivor区,会将它的年龄设置为1.每度过一次Minor GC 它的年龄就会增加1.知道增加到一定程度,默认为15。确实挺老的。

通过参数自行设置年龄大小 -XX:MaxTenuringThreshold=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
// 什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}

4、动态对象年龄判断

为了更好适应不同程序上的内存状态,虚拟机并不是永远要求达到MaxTenuringThreshold,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor的一半,年龄大于它的直接进入老年代。无需等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* 4、动态对象年龄判断
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}


5、空间分配担保

1
HandlePromotionFailure 不再使用

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。

如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。


如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁,参见如下代码,请读者在JDK 6 Update 24之前的版本中运行测试。

在JDK 6 Update 24之后,这个测试结果会有差异,然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。


Author: jony
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source jony !
 Previous
虚拟机性能监控与故障处理工具 虚拟机性能监控与故障处理工具
前言这个章节相当关键,因为前面4篇文章主要介绍了关于虚拟机内存分配和回收技术各方面的内容工具是运用知识处理数据的手段 JDK的命令行工具对于一般的程序员来说,其实我们知道的有两个命令工具java.exe 和javac.exe,但是其他的
2018-04-10
Next 
Java引用 Java引用
前言垃圾回收的机制主要是看对象是否有引用指向该对象。java对象的引用包括
  强引用,软引用,弱引用,虚引用 1、强引用 强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 当运行至
2018-04-09
  TOC