JVM中最重要的部分之一,就是内存分配和回收策略。
内存回收依靠虚拟机中的垃圾收集器进行完成,而内存分配则分为以下5个方面:
对象优先在Eden分配大对象直接分配在老年代长期存活的对象将进入老年代动态对象年龄判定空间分配担保1 对象优先在Eden上分配
大多数情况下,对象在新生代Eden上分配,当Eden区中没有足够的空间时,虚拟机将触发一次Minor GC
注: Minor GC(新生代GC):指发生在新生代的垃圾收集动作,因为Java对象大多数都是具有朝生夕灭的特性,所以Minor GC会非常频繁,一般回收速度也比较快。Minor GC 采取的是复制算法,因为存活对象较少,所以复制效率高,速度较快,且不存在内存碎片的问题。
Full GC(老年代GC):指的是发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但并非绝对,在Parallel Scavenger收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC采用的是标记-整理算法,所以Major GC的速度一般会比Minor GC慢10倍以上。
2 大对象直接进入老年代
所谓的大对象,就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。
大对象对虚拟机的内存分配来说并不是一个好消息,而更坏的消息,就是遇到一群“朝生夕灭”的短命大对象,所以,编程时应当尽量避免这种情况。否则,在还有足够Eden空间的情况下,将会提前触发垃圾回收动作,以获取足够的连续空间来“安置”这些大对象。
虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个值的对象直接进入老年代,以避免大对象在Eden区和两个Survivor区进行频繁复制。
3 长期存活的对象将进入老年代 因为虚拟机采取分代收集的思想来管理内存,那么就必须能识别哪些对象应该放在新生代,哪些放在老生代。
为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age),如果对象在Eden上出生并经历过一次Minor GC后仍然存活,并且可以被Survivor区容纳(复制算法的特点),该对象会被移入Survivor区,并且Age设置为1。 该对象,在Survivor区中,每“熬过”一次Minor GC,Age自增1,当其年龄增长到一定程度(比如15),则该对象会在下一次Minor GC时被移入老年代中。
注:可通过JVM参数:-XX:MaxTenuringThreshold来自设置。
4 动态对象年龄判定
为了能够更好的适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄到达一定阈值之后,才将对象移入老年代,如果Survivor区相同年龄所有对象的大小大于Survivor空间的一般,则年龄大的对象会直接进入老年代。
具体如下:假设有A、B两个对象 其中Age(A)>Age(B),且Space(A+B)>1/2Space(Survivor),A可以进入老年代。
5 空间分配担保 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象空间总和。如果这个条件成立,则此次Minor GC是安全的。 如果不成立,那么虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,会继续检查老年代最大可用连续空间是否大于历次京畿道老年代对象的平均大小,如果大于,则尝试进行一次Minor GC(存在风险,毕竟这只是大于一个平均值)。如果小于,或者HandlePromotionFailure设置为不允许“冒险”,则会触发一次Full GC为老年代清理出足够的空间。
注:“冒险”是冒了什么风险? 因为新生代使用复制收集算法,但为了内存利用率,会只使用其中一个Survivor区(可参考新生代内存收集算法特点),作为轮换备份,因此,当出现大量对象在Minor GC后仍然存活的情况,就需要老年嗲进行分配担保,把Survivor无法容纳的对象直接放入老年代。而前提就是老年代能够容纳这些对象的剩余空间。
如果老年代无法容纳,则触发一次Full GC来清理老年代。
总结:内存回收额垃圾收集器在很多时候都是英雄系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。
因此,学习虚拟机的内存知识,如果要实践调优阶段,就必须了解每个具体收集器的行为、优劣、调节参数。