在线咨询
QQ咨询
服务热线
服务热线:13125520620
TOP

JVM进阶之GC(五)对象的内存分配和回收策略

发布时间:2018-5-23 浏览:3012

JVM内存结构
 
JVM内存主要由新生代、老年代、永久代构成。
 
新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor 
GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
 
新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
 
老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
 
永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
常见垃圾回收器
 
不同的垃圾回收器,适用于不同的场景。常用的垃圾回收器:
 
串行(Serial)回收器是单线程的一个回收器,简单、易实现、效率高。
并行(ParNew)回收器是Serial的多线程版,可以充分的利用CPU资源,减少回收的时间。
吞吐量优先(Parallel Scavenge)回收器,侧重于吞吐量的控制。
并发标记清除(CMS,Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,该回收器是基于“标记-清除”算法实现的。
对象内存分配策略
 
对象的内存分配,就是在堆上分配(如果经过JIT编译器逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配),对象主要分配在eden区,少数情况下也可能直接分配至老年代中,分配的规则视当前使用的垃圾收集器组合和内存参数规则决定。
 
1. 对象优先在eden分配
 
对象在绝大多数情况下,在新生代eden区分配,当eden区没有足够空间进行分配的时候,JVM会发起一次Minor GC。
 
相关内存参数如下:
 
-Xms:最小堆内存值
-Xmx:最大堆内存值
-Xmn:新生代内存值
-XX:SurvivorRatio:新生代中eden区与一个survivor区的空间比
比如,设置的参数是-Xms20M、-Xmx20M、-Xmn10M、-XX:SurvivorRatio=8,可得知,最小堆和最大堆内存一致,即堆内存固定为20MB,新生代为10MB,而老年代=堆内存-新生代,得知老年代为10MB,eden区与survivor区的比例是8:1,eden区=新生代 * SurvivorRatio / 10,eden区的大小为8MB,survivor区为2MB,s0和s1区都为1MB,那么新生代的总可用空间为9MB(eden区 + 1个survivor区)。来看测试代码:
 
public class Test1 {
    private static final int _2MB = 2 * 1024 * 1024;
 
    public static void main(String[] args) {
        byte[] object1 = new byte[_2MB];
        byte[] object2 = new byte[_2MB];
        byte[] object3 = new byte[_2MB];
        byte[] object4 = new byte[2* _2MB];
    }
 
}
 
不懂GC日志的请移步GC 日志。
 
从日志中得知,上述代码发生了一次Minor GC,结果是新生代由7651K变为了529K,而总内存占用量基本没变。这次GC的原因是给object4分配内存时,object1、object2、object3已存在eden区,并占用了6MB的内存,剩余的空间不足以分配给object4的4MB,所以发生了Minor GC。
 
GC的时候JVM又发现3个2MB大小的对象都无法放入survivor区,所以只能通过分配担保机制提前转移到了老年代。GC结束后,从日志中可看出tenured generation(老年代)used了6144K,def new generation(新生代)used 4791K,即object4所占用的内存。
 
2. 大对象直接进入老年代
 
大对象,即需要大量连续内存空间的对象,比如上述测试代码中的byte数组就是典型的大对象。经常出现大对象就容易导致内存还有不少空间时就提前触发了GC,以便获取更大的连续空间来分配。
 
虚拟机提供了一个参数-XX:PretenureSizeThreshold,大于此设置值的对象将直接进入老年代分配内存,这样做的目的是避免在eden区和两个survivor区之间发生大量的内存复制。
 
3. 长期存活的对象进入老年代
 
与大对象相对应,小对象在GC过程中通常不会因为内存空间不够分配而直接进入老年代,而是通过给每个对象定义一个对象年龄计数器的方式。对象在eden区出生,经过第一次Minor GC后仍然能存活,并且能被survivor区容纳,将被移动到survivor区中,并且对象的年龄设为1。对象在survivor区每经过一次Minor GC,对象的年龄就加1岁,当它的年龄增加到一定程度时(默认为15岁),就会晋升到老年代中去。
 
对象晋升老年代的年龄阈值,可通过参数-XX:MaxTenuringThreshold调整。
 
4. 动态对象年龄判定
 
为了能更好地适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,而是有个机智的策略:如果在survivor区中相同年龄的所有对象大小的总和大于survivor区空间的一半时,年龄大于或等于该年龄的对象就可以直接进入老年代,无须达到MaxTenuringThreshold中要求的年龄。
 
5. 空间分配担保
 
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果条件满足,那么Minor GC就是安全的,否则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则“尝试”进行一次Minor GC,如果小于,则要进行Full GC。
 
为什么是尝试进行Minor GC呢?因为新生代采用复制收集算法,只使用其中一个survivor空间来作为轮换备份,因此出现大量对象在Minor GC后仍然存活的情况下(最极端的就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把survivor区无法容纳的对象直接移至老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的空间,然而一共会有多少对象存活下来,在实际完成内存回收的过程中是无法明确知晓的,所以只好取之前每一次回收晋升到老年代的对象容量的平均大小值作为参考值,与老年代的剩余空间比较,来决定是否进行Full GC来让老年代腾出更多空间。
 
总结
 
对象优先在eden区分配内存,如果eden没有足够的空间,则会触发Minor GC,清理空间
对象达到了MaxTenuringThreshold设定的年龄,或survivor区中相同年龄的所有对象大小的总和大于survivor区空间的一半时,年龄大于或等于该年龄的对象,就可以直接进入老年代
新生代对象的总大小或者历次晋升的平均大小大于老年代的连续空间时,就会进行Full GC,反之进行Minor GC
最后补充几点会触发Full GC的方式:
 
Perm(永久代)空间不足;
CMS GC时出现promotion failed和concurrent mode failure(concurrent mode 
failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止CMS,直接进行Serial 
Old GC);
统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间;
主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题。
 
 
 
 

TAG
软件定制,软件开发,瀚森HANSEN
0
该内容对我有帮助