java垃圾回收

概述

本文简要介绍java虚拟机垃圾回收相关概念,回收器,算法,gc日志查看等。

下图为HotSpot虚拟机架构图,其中垃圾回收器(Garbage Collector)的主要管理区是堆(Heap)。为什么是堆,而不是所以区域?因为堆是虚拟机中对象数据存储区域,大多数调优主要针对这个区域。

图1-来源

既然java虚拟机自动垃圾回收,为什么我们需要关心:

  1. 不合理的内存分配会导致内存溢出。
  2. 垃圾回收过程中,会使应用程序的线程暂停(STW),直接影响系统性能。

对垃圾回收做优化,涉及到分析、定位、调整等工作,涉及算法、回收器和一些工具的使用,本文一一介绍。

堆内存

既然垃圾回收是针对堆(heap),那我们看看堆内存区域: 下图是常规的对内存划分,适用于serial, parallel, CMS等回收器。

图2-来源

这就是GC分代,为什么需要这么划分,因为有个基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

年轻代

年轻代又分为3个空间,Eden用来分配新创建的对象,如果内存不足,就会触发年轻代GC来释放内存空间。如果GC后还是没有足够空间,会被分到老年代(Old Generation)。对年轻代回收,其实就是标记eden中的存活对象,然后复制到存活区(Survivor)的过程。

存活的对象会在两个存活区(S0,S1)之间复制多次, 直到某些对象的存活时间达到一定的阀值(‐XX:+MaxTenuringThreshold),提到老年区。

图3

如图3,S1和S0是一个from->to的关系,每次Minor GC,from中的存活对象会和eden存活对象一起复制到to区域。下一次又交换from-to,所有S1和S0总有一个是空的。

存活期对象生命周期:

  1. 虚拟机会记录存活期对象复制的次数,如果大于15(-XX:MaxTenuringThreshold=15)次,提升到老年代。
  2. 如果单个存活期空间达到50%(-XX:TargetSurvivorRatio=50),那么,被复制次数较高的对象会被提升到老年代。
  3. 存活期总有一个是空的,意味着如果存活期太大,也浪费空间,配置参数-XX:SurvivorRatio=8。

老年代

前面说了,年轻代存活对象会从存活期晋升到老年代,它的对象大部分是存活的,所以不会采用标记-复制的方式,而是移动对象,实现最小化内存碎片。

永久代

java8之后,PermGen 区已经不存在,改用Metaspace。

根据内存划分,一般来说,Minor GC 清理的就是年轻代。Full GC指全部GC,清理整个堆。**Major GC **可以认为是清理老年代。其实叫什么GC无关紧要,我们需要关心的是,GC导致线程暂停的时间。

GC 算法

标记

垃圾回收,首先要知道什么是垃圾。JVM的做法是,扫描GC-ROOT(还有一种不常用的引用计数法),可访问到的对象标记为存活,其他的即垃圾。GC Root包括:

  1. 当前正在执行的方法里的局部变量和输入参数
  2. 活动线程
  3. 所有类的静态字段
  4. JNI引用

具体参考:eclipse-MAT,另外GC-ROOT为避免每次全部扫描老年代是否对新生代有应用,引入卡表(Card Table)的技术。

扫描标记时,如果引用关系一直变化,就会没法跟踪,所以要暂停线程,这就是概述中提到的STW(Stop The World pause)。以上找到存活对象的过程,就叫标记(Marking)

引用计数法之所以不常用,是因为无法解决循环依赖的问题。

删除

删除垃圾的过程,不同的算法可能不同,大概分为以下三类:

  • 清除(Sweep): 任务不可达对象占用的空间是空闲的,需要一个空闲表(freelist)来维护空闲空间。缺点是空闲空间分散(碎片),不便分配对象,因为申请内存都是一整个区域。
  • 整理(Compact): 相比简单的标记清除,多了一个把存活对象复制到一边的过程。解决了碎片问题,但拷贝和更新对象引用,增加了jvm的暂停时间。
  • 复制(Copy): 需要额外的空间,把对象负责过去。

上述介绍的只是GC算法的基础,真正的回收器可能会结合几个算法一起使用。因为不同区域适合不同算法。

垃圾回收器

大多数回收器,都会选择两种不同的算法,分别用来清理年轻代和老年代。主要有Serial,Parallel GC,CMS,G1等回收器。查询使用了什么回收器可以通过命令:

jmap -heap  <pid>

Serial

串行GC,因此这种GC算法不能充分利用多核CPU,一般也不会用,就不介绍了。

Parallel GC

并行GC,有效利用多核,减少GC时程序暂停时间,提高系统吞吐量。 参数配置:

‐XX:+UseParallelGC

CMS

Concurrent Mark and Sweep ,对年轻代采用并行 STW方式的标记-复制,对老年代采用标记清除。

CMS的设计目标是避免在老年代垃圾收集时出现长时间的卡顿。不对老年代进行整理, 而是使用空闲列表(freelists)来管理内存空间的回收。 参数配置:

‐XX:+UseConcMarkSweepGC

G1 – Garbage First

G1的设计目标是多核机器和大内存应用,替代CMS是它的长期目标。G1也分年轻代和老年代,但是不再是连续的物理空间。一个内存区域会被划分为一个个固定大小的region,eden、survivor、old只是这些region的标签,region大小从1-32M不等。

如上图,除了Eden、Survivor、Old,还有humongous(用来存储比标准region大50%甚至更大的对象)。Regions设计目的是为了并行回收而不停止其他应用线程。整体来说,G1做了以下优化:

  1. 年轻代回收期间,可以动态调整区域百分比,内存使用更灵活。
  2. 可以只将一组或者多组region并行复制实现压缩,减少碎片。
  3. 可以设置预期暂停时间避免因为暂停过长引起雪崩效应。

可以通过下面参数使用G1回收器,其他参数和最佳实践见:Command Line Options and Best Practices

-XX:+UseG1GC

GC日志

可以通过启动参数配置是否打印gc日志和gc日志文件,也可以通过jstat等工具查看,不过生产环境一般都配置了gc日志,发布定位历史问题。

java -XX:+PrintGCDetails -Xloggc:./log/gc.log -jar app.jar
3.211: [GC (Allocation Failure) [PSYoungGen: 267583K->3013K(427008K)] 271459K->7544K(514560K), 0.0059950 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
3.836: [GC (Metadata GC Threshold) [PSYoungGen: 264562K->3108K(427008K)] 269094K->8606K(514560K), 0.0073634 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
3.843: [Full GC (Metadata GC Threshold) [PSYoungGen: 3108K->0K(427008K)] [ParOldGen: 5497K->7536K(60416K)] 8606K->7536K(487424K), [Metaspace: 20813K->20813K(1069056K)], 0.0604006 secs] [Times: user=0.15 sys=0.01, real=0.06 secs]

日志说明:

  • 3.211:GC事件开始时,相对于JVM启动时的间隔时间,单位是秒
  • GC: 用来区分 Minor GC 还是 Full GC 的标志。GC 表明这是一次小型GC(
  • Allocation Failure:触发垃圾收集的原因。年轻代中没有适当的空间存放新的数据结构引起的。
  • PSYoungGen: 垃圾收集器的名称
  • 267583K->3013K(427008K):年轻代内存回收前,回收后和总内存
  • 271459K->7544K(514560K):整个堆内存回收前,回收后和总内存
  • 0.0059950 :gc时间,单位秒
  • user – 在此次垃圾回收过程中, 由GC线程所消耗的总的CPU时间
  • sys – GC过程中中操作系统调用和系统等待事件所消耗的时间
  • real – 应用程序暂停的时间。在 Parallel GC 中, 这个数字约等于: (user time + system time)/GC线程数

gc日志可视化分析工具


引用和参考: https://coolshell.cn/articles/11541.html https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html https://www.oracle.com/technetwork/cn/articles/java/g1gc-1984535-zhs.html https://plumbr.io/java-garbage-collection-handbook

CONTENTS