码恋 码恋

ALL YOUR SMILES, ALL MY LIFE.

目录
Java垃圾回收与JVM调优
/    

Java垃圾回收与JVM调优

一、JVM 内存与 GC

JVM的内存主要可以划分为堆和堆栈。堆是存储对象的内存空间,堆栈则存储了线程的上下文信息,比如局部变量、方法参数等。我们知道,在JVM中的每个对象都有自己的生命周期,而且大部分对象的生命周期比较短,所以从内存方面考虑,这部分对象应该被快速的回收来释放内存,这个过程就是JVM的分代垃圾回收。如下图:

image.png

这里,我们主要说对于堆的分代垃圾回收。在JVM中,将堆的空间主要分为两部分,年轻代和老年代,同时对于年轻代又划分为Eden区域、From区域和To区域。大家都有一个常识,new的对象都在堆中,那么具体在堆的哪个位置呢?

新建的对象被分配到Eden Space中,当Eden区域空间满时,就触发一次Young GC,已经不被使用的做回收处理,而仍然被使用的则被复制到From区域。经过这个过程,整个Eden区域就是空闲的,如果有新的对象,就 Eden 区域中创建。如果Eden区域的内存再次被用完,就再一次触发了 Young GC ,这时就将 Eden 区域和 From 区域中还在使用的对象复制到 To 区域。下一次 Young GC 则是将 Eden 区域和 To 区域中还在使用的对象全部复制到 From 区域。

如此,经过多次 Young GC 后,会存在某些对象在 From 区域和 To 区域进行多次复制,如果超过某个阈值对象仍然没有释放,则将这些对象复制到 Old Generation。如果Old Generation 区域也用完之后,就会触发 Full GC ,全量回收会对系统的性能造成非常大的影响,所以可以根据各应用的特点和对象的生命周期来设置一个合理的年轻代与老年代的大小值,尽量减少 Full GC。

如此,从减少 Full GC的角度来看,我们可以通过设置一个比较良好的JVM参数来进行性能的调优。

二、垃圾回收算法与典型的垃圾回收器

值得一提的是,在进行 GC 优化之前,要确定应用的架构和代码已经没有或者只有极小的优化空间了,GC 调优是优化程序性能最后的手段。一般来讲,除了对性能要求极高的系统如果 JVM 参数没有特别大的问题,是不需要做所谓的调优的。

GC 调优是一个复杂的工作,需要分析应用的 GC 日志,同时需要深入理解各种垃圾回收器,因此下面从垃圾回收算法开始,介绍两种典型的垃圾回收器。

1、什么是垃圾

我们知道,在 Java 程序的运行过程中创建了很多对象,这其中有很多对象是用了一次就不再使用了,这些就是垃圾对象,需要做清除。那么 JVM 是怎样来确定一个对象是垃圾对象的呢?

为了确定哪些是垃圾对象,常见有两种方式:引用计数法和可达性分析。

  • 引用计数法
    为每个对象提供一个引用计数器,用来统计对象被引用的个数。当一个地方引用了此对象则计数器加 1 ,如果计数器为 0 则认为该对象不存在引用,是一个垃圾对象。

    但是这种方案有一个严重的弊端,就是不能检测循环引用,比如 A 对象的成员变量引用了 B 对象那个,而 B 对象的成员变量又引用了 A 。如此,两个对象的引用计数一直不为 0 ,但是对程序来讲,这两个个对象已经无用了,是垃圾对象。

  • 可达性分析
    将所有引用的对象抽象为一个树,从 GC Roots 的根节点出发,遍历出所有连接的节点对象,这些对象称为“可达”对象。反之,则是不可达对象,需要做垃圾回收。
    image.png

    在 Java 中,可作为 GC Roots 的对象包括 4 种:

a) 虚拟机栈中引用的对象(栈帧中的本地变量表);
b) 方法区中类静态属性引用的对象;
c) 方法区中常量引用的对象;
d) 本地方法栈中 Native 方法引用的对象。

2、GC 算法
  • Mark-Sweep(标记-清除)算法
    标记需要回收的对象,然后清除,会造成许多内存碎片。
  • Copying(复制)算法
    将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。
  • Mark-Compact(标记-整理)算法(压缩法)
    与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。
  • Generational Collection(分代收集)算法
    分为年轻代和老年代,年轻代时比较活跃的对象,使用复制算法做垃圾回收。老年代每次回收只回收少量对象,使用标记整理法。
3、典型的垃圾回收器
  • CMS

    • 简介
      以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是 Mark-Sweep 算法。
    • 场景
      如果你的应用需要更快的响应,不希望有长时间的停顿,同时你的 CPU 资源也比较丰富,就适合适用 CMS 收集器。
    • 垃圾回收步骤
    1. 初始标记 (Stop the World 事件 CPU 停顿, 很短) 初始标记仅标记一下 GC Roots 能直接关联到的对象,速度很快;
    2. 并发标记 (收集垃圾跟用户线程一起执行) 并发标记过程就是进行 GC Roots 查找的过程;
    3. 重新标记 (Stop the World 事件 CPU 停顿,比初始标记稍微长,远比并发标记短) 修正由于并发标记时应用运行产生变化的标记。
    4. 并发清理,标记清除算法;
    • 缺点
      • 并发标记时和应用程序同时进行,占用一部分线程,所以吞吐量有所下降。
      • 并发清除时和应用程序同时进行,这段时间产生的垃圾就要等下一次 GC 再清除。
      • 采用的标记清除算法,产生内存碎片,如果要新建大对象,会提前触发 Full GC 。
  • G1

    • 简介
      是一款面向服务端应用的收集器,它能充分利用多 CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型,即可以设置 STW 的时间。
    • 垃圾回收步骤
      1、初始标记(stop the world 事件 CPU 停顿只处理垃圾);
      2、并发标记(与用户线程并发执行);
      3、最终标记(stop the world 事件 ,CPU 停顿处理垃圾);
      4、筛选回收(stop the world 事件 根据用户期望的 GC 停顿时间回收)
    • 特点
      • 并发与并行
        充分利用多核 CPU ,使用多核来缩短 STW 时间,部分需要停顿应用线程的操作,仍然可以通过并发保证应用程序的执行。
      • 分代回收
        新生代,幸存带,老年代
      • 空间整合
        总体看是采用标记整理算法回收,每个 Region 大小相等,通过复制来回收。
      • 可预测的停顿时间
        使用 -XX:MaxGCPauseMillis=200 设置最长目标暂停值。

三、重要的 JVM 参数

1. 内存
  • -Xms512m:初始堆大小,一开始使用的内存大小,如果超出该大小就会自动扩容
  • -Xmx1024m:最大堆大小,初始化系统就会分配,虽然一开始使用的是初始堆大小,但是空间已经申请,如果超出该大小就会抛出outOfMemoryError
  • -Xmn :设置年轻代大小
  • -XX:SurvivorRatio=4 :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个(from&to)2:4,一个Survivor区占年轻代1/6
  • -XX:NewRatio=3:新生代:老年代 1:3
  • -XX:MetaspaceSize=256m:方法去初始值(jdk1.8移除了永久代取而代之的是元空间,如果用的是JDK7及以前的版本,那么请用-XX:PermSize=256m和-XX:MaxPermSize=512m)
  • -XX:MaxMetaspaceSize=512m:jdk1.8取消了永久代,而是用堆外内存meta space 来存放这些信息,因此它不计入堆大小内存中
  • -Xss1m:设置每个线程的堆栈大小,减小这个值能生成更多的线程
  • -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率
2.垃圾收集器
  • -XX:+UseSerialGC :设置串行收集器
  • -XX:+UseParallelGC :设置并行收集器
  • -XX:+UseParalledlOldGC :设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC :设置并发收集器
3.垃圾回收统计信息
  • -XX:+PrintGC:打印 GC 的简要信息
  • -XX:+PrintGCDetails:打印 GC 日志详细信息
  • -XX:+PrintGCTimeStamps:打印 GC 发生的时间戳
  • -Xloggc:filename:指定 GC 日志文件名
4.并行收集器设置
  • -XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。
  • -XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)

四、GC 调优步骤

基于以上的知识,可以知道我们做 GC 调优的一个目的是:减少GC的频率和Full GC的次数。

文章伊始,就提到了 Full GC,它会对这个堆内存做垃圾回收,会造成巨大的性能开销。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。因此有必要知道 Full GC 出现的几种情况:

  • 老年代内存空间用尽
    调优时尽量让对象在新生代 GC 时被回收、让对象在新生代多存活一段时间和不 要创建过大的对象及数组避免直接在旧生代创建对象 。
  • 持久代Pemanet Generation空间不足
    增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例
  • System.gc() 被显示调用
    垃圾回收不要手动触发,尽量依靠JVM自身的机制

下面说明调优的具体步骤:

1、监控 GC 状态

我们都有这样一个常识:医治病症的前提是,需要知道患者究竟患了什么病,程度如何。故而我们可以使用各种 JVM 工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和 gc 日志,根据实际的各区域内存划分和 GC 执行时间,并结合当前的系统性能否满足业务要求,考虑是否进行优化。

  • 常用命令
    jmap、jstack、jps、jstate、jhat、jinfo等。
  • 可视化工具
    JDK 提供了 Jconsole 和 visualVm 对 JVM 监控,还有第三方提供的 jprofilter,perfino,Yourkit,Perf4j,JProbe,MAT等。
2、分析监控结果,判断是否需要进行优化

如果 JVM 的各项参数设置合理,系统没有出现超时日志,GC 的频率不高,GC 耗时也不多,那么就没有必要进行GC 优化。如果 GC 时间超过 1-3 秒,或者频繁 GC,则必须进行优化。

注:如果满足下面的指标,则一般不需要进行 GC 优化:

  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

一般来说,会有一个推荐的参数分配。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。

活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下:

空间倍数
总大小3-4 倍活跃数据的大小
新生代1-1.5 活跃数据的大小
老年代2-3 倍活跃数据的大小
永久代1.2-1.5 倍Full GC后的永久代空间占用

例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:

*总堆:1200MB = 300MB × 4 新生代:450MB = 300MB × 1.5 老年代: 750MB = 1200MB - 450MB

3、更改垃圾回收器类型或调整内存分配

如果内存分配过小,或者采用的垃圾回收器 GC 较慢,应该首先考虑调整这些参数。

4、测试探索决定最后使用的参数

调整参数后,先灰度部署一台或几台机器。分析参数调整后和调整之前的机器作对比,如果达到预期效果则调整所有的节点,完成优化。

参考:《从实际案例聊聊Java应用的GC优化》



❤ 转载请注明本文地址或来源,谢谢合作 ❤


center