JVM垃圾回收

277 阅读17分钟

JVM垃圾回收

一.JVM分区

image.png

二.判断是否时需要回收

1.四种引用类型

  • 强引用

    垃圾回收器不回收

  • 软引用

    内存空间不足时才回收

  • 弱引用

    垃圾回收器发现就回收

  • 虚引用

    相当于没有引用,随时被回收

    垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。

2.根可达算法

GC Root的对象

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中Native方法引用的对象

三.GC的分类

1.Minor GC(YGC/新生代GC/次收集)

**特点:**发生在新生代的GC,频繁,速度快

**常用收集算法:**标记-复制(复制少量存货对象,其余一块回收)

**触发条件:**eden区分配满。触发YGC后部分存活对象会晋升到老年代,所以YGC后老年代的占用量通常会有所升高

2.full GC(FGC/Major GC/主收集)

**特点:**对整个Java Heap中的对象(不包括永久代/元空间)进行收集。执行速度慢,发生较少

常用收集算法:

​ 对于新生代:复制

​ 对于老年代:标记-清除/标记-整理

触发条件:

  • 老年代空间不足(新生代要晋升到老年代的对象大小比老年代的剩余空间大【包含年龄晋升和对象过大to放不下晋升】)
  • 永久代空间不足(系统要加载的类、反射的类、调用的方法过多)
  • 调用System.gc()
  • 代码中一次获取了大量的对象,内存泄漏

解决:

  • 针对老年代空间不足=>1.让对象在新生代多存活一段时间,尽量做到让对象在Minor GC阶段被回收 2.不要创建过大的对象和数组
  • 针对永久代空间不足=>1.增大永久代空间 2.转为使用CMS GC
  • 针对调用System.gc()=> -XX:+DisableExplicitGC 来禁用 JVM 对显示 GC 的响应。
  • 针对内存泄漏=>通过 Eclipse 的 Mat 工具进行查看,我们基本上就能确定内存中主要是哪个对象比较消耗内存,然后找到该对象的创建位置,进行处理即可。

频繁Full GC排查

查看gc情况 jstat -gcutil 发现full gc频繁jstat -gcutil pid interval(ms)

img

1.看是否是空间不足(上图)

E新生代使用百分比

O老年代使用百分比

2.看是否是内存泄漏(上图)

FullGC 的数量高且不断增长

3.看是否是调用System.gc()

这种情况我们查看 Dump 内存得到的文件即可判断,因为其会打印 GC 原因:

img

比如这里第一次 GC 是由于 System.gc() 的显示调用导致的,而第二次 GC 则是 JVM 主动发起的。

四.垃圾回收算法

1.复制

  • 方法:将内存分为两块,每次使用其中的一块,GC时把还活着的对象复制到另一块内存上,把这块空间一起清理掉
  • 优点:简单高效,不会出现内存碎片
  • 缺点:活对象较多时效率很差
  • 备注:每次使用Eden和其中一块survivor, HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,每次用90%

2.标记清除(Mark-Sweep)

  • 方法:标记所有需要回收的对象,统一回收被标记的对象
  • 优点:利用率100%
  • 缺点:内存碎片、扫描两次

3.标记整理(Mark-compact)

  • 方法:标记所有要回收的对象,让不用回收的的对象想一端挪动,回收垃圾
  • 优点:没有内存碎片,内存利用率100%
  • 缺点:要挪动,效率不高

五.垃圾回收器的一些重要参数

参数描述
UseSerialGC虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收
UseParallelOldGC打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加 1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads设置并行 GC 时进行内存回收的线程数
GCTimeRatio GC时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间,仅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效

六.垃圾回收器

关注点

  • 停顿时间: 停顿时间越短就适合需要与用户交互的程序;提升用户体验
  • 吞吐量:高吞吐量则可以高效率地利用CPU时间 ,尽快完成运算的任务
  • 覆盖区:在达到前面两个目标的情况下,尽量 减少堆的内存空间

Serial(串行):单线程,运行时暂停用户程序(STW),执行垃圾回收程序停止时间较长

ParNew(并发串行):将串行收集器多线程化,STW,只能和CMS配合

Parallel(并行):并行多线程,垃圾收集的多线程同时进行,可以缩短程序停顿时间,不能和CMS配合

Concurrent(并发):一条或多条GC线程,部分时间STW,垃圾收集和用户应用同时进行,回收时系统不会停止运行,响应高的系统

对于新生代(复制算法)

  • Serial
  • ParNew
  • Parallel Scavenge 并行回收GC 吞吐量高

对于老年代(标记清除/整理)

  • Serial Old 标记整理 并行多线程
  • Parallel Old 标记整理 并行多线程
  • CMS(Conc Mark Sweep)标记清除 并行和并发收集
  • G1(Garbage First)新生代和部分老年代 标记整理 并行和并发收集

使用jps -v 可以看到使用的垃圾收集器

以上配套使用

1.Serial+Serial Old

最古老的,单线程,独占式,成熟,适合单 CPU 服务器 -XX:+UseSerialGC 新生代和老年代都用串行收集器

2.Parallel Scavenge+Parallel old

关注吞吐量的垃圾收集器。

即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那有吞吐效率就是 99%

3.ParNew+CMS(Old)GC+Full GC for CMS

关注最快响应时间的垃圾收集器

过程:
  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)
  • 并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
  • 并发清除:不需要停顿
优点:
  • 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的
缺点:
  • CPU 资源敏感:因为并发阶段多线程占据 CPU 资源,如果 CPU 资源不足,效率会明显降低

  • 不整理会出现碎片,很容易出现新生代晋升失败(Promotion failed),导致提前Full GC

  • 由于 CMS 并发清理阶段 用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为 浮动垃圾

  • 由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%),如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

  • 会产生内存碎片:标记-清除算法 会导致产生不连续的内存碎片

解决:
  • Promotion failed(晋升失败)=>1.几次【一次或多次都可】标记清除之进行以此标记整理 2.增加Survivor空间大小

  • Concurrent Mode Failure(并发修改失败)=>调大老年代的空间

4.G1(Young GC+Mixed GC)

针对多核处理器和大内存服务器,能够以很高的概率 满足开发人员对停顿时间的要求, 同时还能保证高吞吐量

G1 把堆划分成多个大小相等的 独立区域(Region),新生代和老年代不再物理隔离

G1 算法将堆划分为若干个独立区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者 Survivor 空间。例如其中一个独立区域如图:

Young GC
  • Eden 空间耗尽时会被触发
  • Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间
  • Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。
Mixed GC
  • 只能回收部分老年代的 Region
  • 如果 mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会使用 serial old GC(full GC)来收集整个 GC heap
  • 不提供full GC
过程:
  1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的 Region 中创建对象,此阶段需要停顿线程(STW),但耗时很短
  2. 并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执 行
  4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
优点:(相较于CMS的优点)
  • 基于标记-整理算法, 不会产生空间碎片,分配大对象时不会无法得到连续的空间而提前触发一次full gc
  • 停顿时间可控: G1可以通过设置预期停顿时间(Pause time)来控制垃圾收集时间,但是这个预期停顿时间G1只能尽量做到,而不是一定能做到

可预测的停顿:

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可高的收集效率

七.JVM调优

年轻代

  • 响应时间优先,年轻代尽可能大(ParNew)
  • 吞吐量优先,尽可能大,达到Gbit,对响应时间没有要求,垃圾收集可以并行( Parallel S)
  • 避免设置过小,过小导致1.YGC次数频繁2.YGC对象直接进入老年代,触发FGC

老年代

  • 响应时间优先,老年代使用并发收集器,大小要小心,过小会导致内存碎片、高回收频率,以及应用暂停使用传统的标记清除。过大需要长时间的收集。
  • 吞吐量优先,较大的年轻代和较小的老年代。,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

其他:

  • 用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
  • XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
  • 使用CMS的好处是用尽量少的新生代,经验值是128M-256M, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间非常的短,2G的内存, 大约20-80ms的应用程序停顿时间
  • 系统停顿的时候可能是GC的问题也可能是程序的问题,多用jmap和jstack()查看,或者killall -3 java,然后查看java控制台日志,能看出很多问题。(相关工具的使用方法将在后面的blog中介绍)(jstack(查看线程)、jmap(查看内存)和jstat(性能分析)
  • 仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定。
  • 采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿
  • JVM参数的设置(特别是 –Xmx –Xms –Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,需要根据PV old区实际数据 YGC次数等多方面来衡量。为了避免promotion faild可能会导致xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力下降等问题。每个参数的调整都需要经过详细的性能测试,才能找到特定应用的最佳配置。

八.故障排查

1.线上功能缓慢排查

简要的说,我们进行线上日志分析时,主要可以分为如下步骤:

①通过 top 命令查看 CPU 情况,如果 CPU 比较高,则通过 top -Hp 命令查看当前进程的各个线程运行情况。

找出 CPU 过高的线程之后,将其线程 id 转换为十六进制的表现形式,然后在 jstack 日志中查看该线程主要在进行的工作。

这里又分为两种情况:

  • 如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗 CPU

  • 如果该线程是 VM Thread,则通过 jstat -gcutil <pid> <period> <times> 命令监控当前系统的 GC 状况

    然后通过 jmap dump:format=b,file=<filepath> <pid> 导出系统当前的内存数据。

    导出之后将内存情况放到 Eclipse 的 Mat 工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码。

②如果通过 top 命令看到 CPU 并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。

具体的可以根据具体情况分析:

  • 如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过 jstack 查看堆栈信息,找到阻塞点。
  • 如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出 jstack 日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程。
  • 如果通过 jstack 可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。

2.导致CPU过高的原因

cpu占用过大的影响:元器件的功耗增大,疯狂发热、使用率道道最大值100%时,未满载状态,如果此时散热不良,会触发cpu的自动保护,降频降温来减低其功耗以及发热量,保证cpu的运行安全。

  • Full GC次数过多
  • 代码中由比较耗时的计算

如果是自己的pc

1.硬件垃圾 2.软件过于臃肿 3.排除病毒恶意大量占用cpu资源,优化开机自启项 ,关闭一些不必要的进程

3.频繁full GC排查

查看gc情况 jstat -gcutil 发现full gc频繁jstat -gcutil pid interval(ms)

img

1.看是否是空间不足(上图)

E新生代使用百分比

O老年代使用百分比

2.看是否是内存泄漏(上图)

FullGC 的数量高且不断增长

3.看是否是调用System.gc()

这种情况我们查看 Dump 内存得到的文件即可判断,因为其会打印 GC 原因:

img

比如这里第一次 GC 是由于 System.gc() 的显示调用导致的,而第二次 GC 则是 JVM 主动发起的。

参考:

1.juejin.cn/post/684490…

2.www.cnblogs.com/redcreen/ar…