JVM_8_内存分配与回收策略

来源:互联网 发布:世界核武国家 知乎 编辑:程序博客网 时间:2024/06/11 04:42

内存分配与回收策略


参考资料:

《Java虚拟机垃圾回收(四) 总结:内存分配与回收策略 方法区垃圾回收 以及 JVM垃圾回收的调优方法》


在之前看"分代收集算法"的时候,我们知道目前几乎所有商业虚拟机的垃圾收集器都采用分代收集算法,对于Hotspot虚拟机年代划分,如下图:

对象的内存分配从大体上讲:

在堆上分配,主要在新生代对的Eden区中分配。少数情况下,可能直接分配到年老代中。


Java技术体系中所提倡的自动内存管理最终可以规划为自动解决两个问题:给对象分配内存以及回收分配给对象的内存


下面来看看给对象分配内存的那些事儿。


对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上。少数情况下也肯能会直接分配在年老代中

分配的规则并不是百分百固定的。其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中内存相关的参数配置。


接下来,咱们看看最普通的内存分配规则。

(个人感觉,可以将Eden区,粗暴的理解为年轻代)




对象优先在Eden分配

前面的文章介绍过Hotspot虚拟机新生代内存布局及算法:

a. 将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间。

b. 每次使用Enden和其中一块Survivor。

c. 当回收时,将Eden和使用中的Sruvivor中还存的对象一次性复制到另一块Survivor;

d. 然后清理掉Eden和使用过的Survivor空间;

e. 后面就使用Eden和另一块Survivior空间,重复步骤3。


默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor空间被浪费。


大多数情况下,对象在Eden区中分配;

Eden区没有足够空间进行分配时,JVM将会发起一次MinorGC(新生代GC)

MinorGC时,如果发现存活的对象无法全部放入Servivor空间,只好通过分配机制提前转入熬年老代中




大对象直接进入年老代

所谓的大对象是指:需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组

大对象对虚拟机的内存分配来说就是一个坏消息,比遇到一个大对象更加坏的消息就是:遇到一群"朝生夕灭"的"短命大对象"

经常出现大对象容易导致内存还有不少空间时,就提前触发垃圾收集以获取足够的连续空间来"安置"它们。

所以我们应该避免创建大对象;

"-XX:PretenureSizeThreshold":

可以设置这个阀值,大于这个参数值的对象直接在年老代中分配。

默认为0(无效),且只对Serail 和 ParNew两款收集器有效。

如果需要使用该参数,可以考虑ParNew+CMS组合





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

虚拟机给每个对象定义了一个对象年龄(Age)计数器,其计算流程如下:

a. 在Enden区中分配的对象,经Minor GC之后还存活,就复制移动到Survivor区,年龄为1;

b. 而后每经历一次Minor GC后还存活,在Survivor区复制移动一次,年龄就增加1岁

c. 如果年龄达到一定程度,就晋升到年老代中

"-XX:MaxTenuringThreshold":

设置新生代对象晋升年老代的年龄阀值,默认为15




动态对象年龄判断

JVM为了更好适应不同程序,不是永远要求等到MaxTenuringThreshold参数设置的年龄。

如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入年老代。




空间分配担保

当Survivor空间不够用时,需要依赖其他内存(年老代)进行分配担保(Hanle Promotion)


分配担保流程如下:

a. 在发生Minor GC之前,JVM首先检查年老代最大可用的连续空间是否大于新生所有对象的空间

b. 如果大于,那么可以确保Minor GC是安全的。

c. 如果不大于,则JVM查看HandlePromotionFailure值是否允许担保失败。

d. 如果允许,将尝试进行一次Minor GC,但这是有风险对的;

e. 如果小于或HandlePromotionFailure值不允许冒险,那这时,要改为进行一次Full GC;


尝试Minor GC的风险--担保失败:

因为尝试Minor GC前,无法知道存货的对象大小,所以使用历次晋升到年老代对象的平均大小作为经验值。

加入尝试的Minor GC最终存活的对象远远高于经验值的话,会导致担保失败(Handle Promotion Failure)。

失败后只有重新发起一次Full GC,这绕了一个大圈,代价较高。

但一般还是要开启HandlePromotionFailure,避免Full GC过于频繁,而且担保失败概率还是比较低的。


JDK1.6之后,JVM代码中已经不再使用HandlePromotionFailure参数了...


规则变为:

只要年老代最大可用的连续空间大于新生所有对象的空间或历次晋升到年老代对象的平均大小,就会进行MinorGC,否则进行Full GC

即年老代最大可用的连续空间小于新生所有对象空间时,不在检查HandlePromotionFailure,而是直接检查历次晋升熬年老代对象的平均大小。






回收方法区


参考资料:

《Java虚拟机垃圾回收(四) 总结:内存分配与回收策略 方法区垃圾回收 以及 JVM垃圾回收的调优方法》


在<JVM_1_运行时内存区域>一篇中,曾介绍过方法区相关的回收问题。

虽然JVM规范规定这个区域可以不实现垃圾收集,且针对常量池和类型卸载的回收效果不佳,但方法区实现垃圾回收还是必要的。


方法区(永久代)的主要回收对象

1. 废弃常量

          与回收Java堆中的对象非常类似。

2. 无用的类

          同时满足下面3个条件才能算"无用的类"

          a. 该类所有实例都已经被回收(即Java堆中不存在该类的任何实例);

          b. 加载该类的ClassLoader已经被回收,即通过引导程序加载器加载的类不能被回收。

          c. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的存在。


需要注意方法区回收的应用


大量使用反射、动态代理、经常动态大量类的应用,要注意类的回收;

如运行时动态生成类的应用:

1. GCLib在Spring、hibernate等框架中对类进行增强时会使用;

2. VM的动态语言也会动态创建类来实现语言的动态性;

3. 另外,JSP、基于OSGI频繁自定义ClassLoader的应用等


Hotspot虚拟机的相关调整

JDK1.7:

          使用永久代实现方法区,这样就不用专门实现方法区的内存管理,但这样子容易引起内存溢出问题;

          不在Java堆的永久代中生成分配字符串的常量池,而是在Java堆其他的主要部分(年轻代和年老代)中分配。

          更多请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html


JDK1.8:

          永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并显式管理元数据的空间。

          相关参数:

                    "-XX:MaxMetaspaceSize" (JDK8):指定类元数据区的最大内存大小;

                    "-XX:MetaspaceSize" (JDK8):指定类元数据区的内存阈值--超过将触发垃圾回收;

                    "-Xnolassgc":控制是否对类进行回收;

                    "-verbose:class"、"-XX:TraceClassLoading"、"-XX:TraceClassUnloading":查看类加载和卸载信息;







JVM垃圾回收的调优方法


内存回收与垃圾收集器是影响系统性能、并发能力的主要因素之一,一般都需要进行一些手动的测试、调整优化;


明确期望的目标(关注点)


首先应该明确我们的应用程序调整垃圾回收期望的目标(关注点)是什么?

1. 停顿时间

GC停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验。

与用户交互较多的场景,以给用户带来较好的体验;

如常见Web、B/S系统的服务器上的应用;


2. 吞吐量

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间);

高吞吐量可以高效率的利用CPU时间,尽快完成运算的任务,主要适合在后台计算而不需要太多交互的任务。

应用程序运行在具有多个CPU的机器上,对暂停时间没有特别高的要求。

程序主要在后台进行计算,而不需要与用户进行太多交互。

例如 那些执行批量处理、订单处理、工资支付、科学计算的应用程序。


3. 覆盖区

在达到前面两个目标的前提下,尽量减少堆的内存空间,以获得更好的空间局部性;

可以减少到不男足前两个目标为止,然后再解决未满足的目标;

如果是动态收缩的堆设置,堆的大小将随着垃圾收集器试图满足竞争目标而振荡。


总结一下就是:

低停顿、高吞吐量、少用内存资源。


只是一般这些关注点都是互相影响的,增大堆内存空间获得高吞吐量但会增加停顿时间,反之亦然,做不到完美,有时只能折中。





JVM自适应调整

JVM有自适应选择、调整 相关设置的功能。


一般都会根据平台性能选择好垃圾收集器,并且设置好参数;

在运行中,一些垃圾收集器还会动态监控信息,自动的、动态的调整垃圾收集策略;


所以放我们不知道如何选择垃圾收集器和调整时,应该首先让JVM自适应调整

让后通过输出GC日志进行分析,看能不能满足明确期望的目标(第一步)


如果不能满足,或者通过打印设置的参数信息,发现可以有更好的调优时,可以进行手动指定参数进行设置,并测试。





实践调优:选择垃圾收集器,并进行相关设置

需要明确一个观点:

没有最好的收集器,更没有万能的收集器;

选择的只能是对居图应用最合适的收集器;


我们都知道HotSpot有下面这些组合可以搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;


到了实践调优阶段,那必须要了解每个具体收集器的行为特点、优势和劣势、调节参数等...

然后根据明确期望的目标,选择应用最适合的收集器;


例如:使用Parallel Scavenge/Parallel Old组合,这是一种值得推荐的方式:

1. 只需要设置好内存大小(如"-Xmx"设置最大堆);

2. 然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标。

3. 那些具体细节参数的调节就由JVM自适应完成。


设置调整之后,应该通过在生产环境下进行不断地测试,来分析是否达到到我们的目标。