java的垃圾回收浅谈

news/2024/11/30 2:50:26/

目录

并发标记问题

三色算法问题

浮动垃圾问题

漏标问题

cms的解决方式

g1的解决方式

跨代(区)引用

CMS垃圾回收日志

G1垃圾回收日志


垃圾回收过程其实都包含两步:标记+回收。

标记算法:

  • 引用计数:每个对象都有一个计数器,被别的对象引用时计数器加1,当技术器变成1的时候,这个对象就是垃圾了。这个实现 简单且高效,但是存在循环引用不好处理的问题。
  • 根可达算法:有一个根列表(对于java,比如运行栈、常量区、jni栈等都是根),通过根去遍历,能够遍历的就是存活的对象、不能遍历到的就是垃圾了。

回收算法:

  • 清理:只是清理垃圾对象,不作内存整理。所以这种回收方式会产生内存随便,对应的内存管理也就只能使用链表方式,且回收后对象的内存地址是不变的。
  • 整理:将垃圾对象回收后,会整理内存。正式因为整理过程,停顿时间就比较长。这种方式可以使用指针碰撞方式来管理内存
  • 复制:将内存一分为二,回收的时候是将存活的对象copy到另外一块内存,然后释放原来那块内存整体释放。这种方式没有内存碎片、效率也比整理要高,但是内存的使用率就变低了,有一半的内存都是浪费的,并且当内存很大的时候、存活对象很多的时候,copy过程就会很长,进而停顿时间就会变长

综合来看,复制算法适合在垃圾回收执行时存活对象比较少的场景;整理算法适合在垃圾回收时存活对象较多但是不能有内存碎片的场景;而清理算法因为会产品内存碎片,实际的场景其实并不多,CMS使用该算法,但也诟病比较多。

对于一个java进程中的对象,经过统计发现,大部分对象生命期都是比较短的,活不过一次垃圾回收,而少部分生命期比较长,多次gc后依然存在,甚至和java进程生命期相同。针对这种情况,就对java的内存进行了分区,不同分区使用不同的回收算法,来达到最大的收集效果。

所以在垃圾回收器的发展历史中,出现了两个分区方式(根据分区的粒度不同)

  • 分代内存布局:整个java进程的内存空间分成量大块:新生代和老年代。每一个分代中的内存地址是连续的。新生代里存放生命期端的对象,对应的垃圾回收的时候就用复制算法;而老年代里存放生命期比较长的对象,对应的垃圾回收的时候就使用整理算法。因为新生代使用复制算法,随意新生代需要进一步划分三部分,来满足复制算法:eden区、surviver from区、suviver to区。
  • 分区内存布局:将java进程的内存分成n个region,回收的时候也按region回收。

这是从内存布局上和回收算法上看gc发展的历史,主要就是分这么两个阶段。

从并行的角度看垃圾回收的发展。

  • 单gc线程回收:这个时期,gc线程只有一个,当启动垃圾回收的时候,暂停所有业务线程,然后gc线程开始工作,经过标记、回收后,业务线程才能继续运行。这个垃圾回收过程业务线程是完全暂停的(STW:stop the world)。早期因为java进程内存都比较小,业务也并不复杂,所以垃圾回收都是单线程的,停顿时间也是可接受的,早期的Serial、Serial Old都是单线程的垃圾回收器

  • 多gc线程回收:随着java进程的内存越来越大,单线程回收效率变的很慢了,所以这个时候自然的一个想法就是将gc线程变成多线程的,多线程来并行的标记-回收内存,会更快一些。但是在垃圾回收的过程中,业务线程还是完全暂停的。

  • gc线程和业务线程并行回收:随着技术发展,java的内存需求越来越大、对于业务线程的停顿时间要求也越来越高。要进一步缩短停顿时间,那就是将gc过程进一步系分,在最必要的时候才STW,其他步骤业务线程和gc线程并行运行,从而减少STW时间。

所以到了现在的垃圾回收器,就是多种回收算法的组合、多gc线程、以及gc过程中某些阶段gc线程和业务线程可并行运行的有机结合,来提高gc的性能的。比如CMS、G1,以及后来的ZGC。

jdk支持的垃圾回收器(连线表示可组合使用):

并发标记问题

当业务线程和gc线程并行运行的时候,就让标记变得非常复杂了,比如gc线程标记到某个对象不是垃圾、但是业务线程下一秒就断开了到该对象的引用、那么这个对象本次gc就不会回收;另外gc线程没有标记认为是垃圾、但是在回收前业务线程又有引用指向了这个对象,这种情况就会更严重,将不是垃圾的对象给回收了,程序就会出现问题。
为了解决这种并发标记的问题,发明了三色着色算法来解决这个问题,其核心思想就是在标记的过程中,给对象的标记情况进行着色区分:

  • 黑色:自己和其字段都被标记,认为是标记结束了的对象,后续的标记都不会再去遍历黑色对象了。而黑色对象也认为是活跃对象,不能进行回收的。
  • 灰色:自己被标记了,但是其成员字段没有被标记。这种属于标记过程中的,所以后续标记处理会继续利用可达性算法来标记改对象的所有字段。
  • 白色:通过根可达算法,遍历不到的对象。这种对象其实就是垃圾,gc过程回收的也就是白对象。但这里需要强调一点:白对象可能是标记结束了缺失不可达没有标记到的、也有可能是标记还没有结束,因为还没有标记到所以是白对象。

ps:当一个对象被标记成灰对象,所谓的后面标记,是指因为操作系统调度,gc线程被暂停了,重新获得cpu时间片后,接着继续标记。这里千万不要和CMS、G1收集器的remark阶段混淆了,这些收集器设计remark阶段就是为了解决着色算法的问题的。所以这里讲的着色算法时的垃圾回收就两个阶段:标记+回收,标记不分阶段哈,不要和remark混为一谈,否则比较不好理解。

三色算法问题

浮动垃圾问题

当gc线程执行标记时,到了如下情况时,gc线程的cpu时间片耗尽,业务线程开始执行。

业务线程将B指向C的引用给解除了:

当gc线程再次获得cpu时间片的时候,再去标记B的字段的时候,已经通过B字段找不到C了,即C已经成为垃圾,但本轮gc就有可能回收不到C。不过下轮gc的时候,C就会被回收了,这种因为业务线程和gc线程并行执行过程中产生的、而本轮gc又不能回收的垃圾称之为浮动垃圾。

浮动垃圾的影响只是会多占用一段时间的内存,并不会导致错误。但是对于不是垃圾而又没有被标记到的,被清理了就会出现问题

为了解决浮动垃圾占用内存的问题,gc的触发时机就不会等到分区(分代)内存全部用光了,才会触发gc,而是对应分区(分代)内存占用达到一定比例的时候,就会处罚gc。

漏标问题

gc线程执行到如下情况,cpu时间片消耗尽了:

 

但是业务线程运行的时候,让A持有了C的引用,但是断开了B对C的引用。这个时候gc线程获得cpu时间片继续运行时,因为A已经是黑色对象,所以gc线程不会再去遍历其字段引用了哪些对象了、而B是灰色对象,但是通过B的字段遍历,已经标记不到C了,那么就会误认为C是一个垃圾,如果本轮gc将C给回收了,那么A对象去访问C的时候,就会出问题。

综上:三色标记法漏标问题的产生有两个充要条件:

1. 业务线程使得一个黑色对象引用指向了白色对象

2. 原本指向这个白色对象引用的灰色对象,被业务线程删除了

cms的解决三色标记的漏标问题(increment update)

cms解决三色标记漏标的问题,就是破坏第一个条件:业务线程使得一个黑色对象引用指向了白色对象。具体的做大就是当黑对象引用别的对象发生变化的时候,就将黑对象变成灰对象(写屏障),那么gc线程再次运行的时候,就会继续标记了。

但是对于并行的gc标记来说,还是会有问题,比如当gc线程开始标记A对象,所以A对象是灰对象

当gc线程正在通过A对象的A.b字段去标记B对象的时候,这个时候cpu时间片耗尽,业务线程开始运行,这个时候业务线程将B对象对C的引用断开,但是让A对象A.c引用指向了C对象。在写屏障中会将A变成灰对象(本身已经是灰对象了):

当gc线程再次获得cpu时间片的时候,会接着上次没有标记完的地方开始继续标记。当标记完成后,C其实是没有被标记到的,认为是垃圾,就会被回收,但实际上A却是有引用指向了C的,所以C是不能回收的。

在remark阶段,暂停业务线程,来重新扫描所有灰对象(其实是从gc root开始)的所有字段。这样就会将C给标记上了,从而避免问题。所以我们看CMS垃圾回收stw的时候,有的时候remark的时间是比较长的。

当然CMS还有其他的问题,比如CMS是不具内存整理能力的,当老年代内存使用率达到指定的比例的时候,就开始一次老年代gc,而这次gc只是回收了垃圾对象占用内存,但是并不会整理内存,当有对象需要分配在老年代(不管是晋升还是大对象直接分配),没有足够的连续内存分配的时候,就会触发一次内存整理,这个内存整理过程会SWT(和serial old过程一样),那这次gc的STW就会比较旧。

为什么清除算法有碎片问题,但是CMS还是使用了清除算法呢?

就是为了回收阶段不暂停业务线程。因为对于整理算法和复制算法,在回收的时候,对象的内存地址其实是要变化的,所以在回收过程也是需要暂停业务线程的。但是清除算法因为不会改变对象的内存地址,就可以做到业务线程和gc线程线程并行运行。

总结起来CMS量大问题:

1. 解决漏标问题效率太低了,即使remark的时候重新扫描

2. 因为清除算法会有碎片的问题,可能导致回退到serial old收集器来gc,所以可能导致某次耗时特别的长。这就会导致系统的不稳定

参考这个博客R大的回复:

https://hllvm-group.iteye.com/group/topic/44381?page=2

G1的解决方式(SATB:Snapshot-At-Beginning)

G1的解决思路就是破坏第二个条件:原本指向这个白色对象引用的灰色对象,被业务线程删除了。基本思路就是在灰色指向其他对象引用断开时,记录下断开的引用。然后在重新标记的时候,来看是否还有对象指向了断开引用指向的对象。

如上图,G1会记录下B-->C这个断开的引用,在重新标记的时候,就会去看是否还有对应应用了C,如果有,就会标记C。这样就会不会把C漏标了。

G1是如果知道B-->C的引用断开了呢?答案就是SATB,简单理解就是在gc开始的时候记录下了引用的快照,和快照相比,发现断开了B-->C的引用,于是将B-->C的这个引用会放到gc线程的运行栈中去,然后gc再次运行的时候,就会去看是否还有其他对象引用C,如果有,就会将C标记,避免漏标。

所以下一个问题就是SATB这个快照是如何创建的?难道真的是将当前堆里所有对象的引用关系都copy了一份?答案肯定不是的。这里其实可以引申一下,mysql事务开启的时候,也会创建一个快照视图(它肯定是不可能copy整个数据库的数据的,否则会疯的);ES的scroll扫描大量数据的时候,也是在初始化scroll的时候创建了一个快照(它肯定也不是copy整个索引的)。背后的基本思想个人总结就是:记录的其实是一个id水位。比如mysql,事务开启的时候,其快照记录的其实是:当前有哪些正在执行中的事务,且事务id是递增的,所以小于这个集合中最小id的事务一定是已经提交的、大于这个集合最大的id的事务一定是当前事务启动后创建的,所以利用这个水位关系来实现了快照;ES中的道理也打通小异,再回到这里的SATB,其实也是利用了类似的思想来记录快照的。

所以总结起来看:

  • CMS解决三色标记漏标是从新增的那个引用入手
  • G1解决三色标记漏标问题是从小时的那个引用入手的

除此之外,G1和CMS在最后回收阶段有个不同:

  • CMS采用的是清除算法,所以在最后的回收阶段,是可以和业务线程并行执行的。
  • 但是G1采用的是复制算法(这里不绝对,也可能是标记整理算法),但不管是哪种,都会该表对象的内存地址,所以G1在回收阶段是会STW的。但是G1回收并不会回收整个堆,而是根据用户设置的stw期望停顿时间,来选择收益最高的region。
  • 另外,使用CMS和G1的堆内存布局是完全不一样的。CMS是分代式的内存管理;G1是分区式的内存管理(逻辑分代)。从这个角度看,jmm设计了内存模型,其最主要的目的就是服务于GC。

跨代(区)引用

这里主要的问题就是:不回收的那个分区引用了当前回收的区域中的对象的时候,会有问题。比如yong gc的时候回收年轻代,但是有老年代的对象引用了年轻代的对象,年轻代的这个对象就不能被回收。

反过来,只是回收老年代的时候,有年轻代对象引用了老年代对象的情况,也是一样的。

解决这方式:全量扫描一遍非收集区,比如yong gc的时候,全量扫描一遍老年代,看看有没有老年代对象引用新生代对象。

对于old区的回收,这么搞是ok的,因为这个时候可以先触发一次yong gc,然后年轻代存活对象也不多了,所以这么扫描不会耗时太久;但是反过来,yong gc的时候,其目的就是只回收年轻代,这个时候去全量扫描老年代,就失去了分区gc的意义了。

对于CMS来说,解决yong gc过程,有old区对象引用年轻代对象的情况,在年轻代中引入了卡表(card table),卡表中记录了有哪些老年代对象引用了年轻代的对象,在引用变更的时候,写屏障中会来更新卡表。

这样在yong gc的时候,不需要全量扫描老年代的对象,看是否引用了新生代对应,只需要扫描年轻代的卡表就可以了。而对于回收old区的时候,会先触发一次yong gc,然后回收old区的时候,扫描yong区就好了(其实这也是cms垃圾回收的一个耗时点)

对于G1来说,分代在G1中,分代只是一个逻辑概念了,其真实的内存布局已经变成了分区(Region)的,gc回收的时候,也是按照Region来回收的,所以这个问题就转换成了跨Region引用的问题了。

G1的解决方式就是每个Region都维护了一个Rset(Remember Set)来记录了其他Region引用了当前Region的对象,在回收对应Region的时候,扫描Rset就可以了。

gc日志

打印gc日志
-XX:+PrintGCDateStamps
​​​​​​-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails 打印gc的详细日志
-XX:+PrintGCCause   打印产生gc的原因
// 如下是指定gc日志输出的方式
-Xloggc:/Users/george/gclog/gc-%t.log 
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M

另外,-Xmx10M,配置成10M,更容易观察到full gc。

CMS垃圾回收日志

parNew和CMS的组合:

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=30
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=1

parNew收集年轻代日志:

GC Allocation Failure就是造成本次gc的原因。Allocation Failure表示的就是新建对象分配内存失败导致的gc。常见的gc cause参考美团的技术博客:Java中9种常见的CMS GC问题分析与解决

cms收集老年代老年代:

  •  CMS Initial Mark(初始标记 STW):
  • CMS-concurrent-mark(并发标记):
  • CMS-concurrent-preclean(并发预清理)
  • CMS-concurrent-abortable(并发可中断预清理)
  • CMS Final Remark(最终标记STW)
    • YG occupancy: 348 K (3072 K) -- 表示年轻代占用空间为 348 K,年轻代总空间为 3072K
    • Rescan (parallel) , 0.0001908 secs -- 老年代重新扫描耗时 0.0001908 秒
    • weak refs processing, 0.0000335 secs -- 弱引用处理耗时
    • class unloading, 0.0002128 secs -- 类卸载处理0.0002128秒
    • scrub symbol table, 0.0002739 secs -- 符号表处理耗时0.0002739秒
    • scrub string table, 0.0001446 secs -- 字符表处理耗时0.0001446秒
    • CMS-remark: 2631K(6848K)] 2980K(389920K), 0.0009399 secs [Times: user=0.00 sys=0.00, real=0.00 secs]  -- CMS 重新标记后,老年代占用 2631K,老年代总空间 6848K,堆占用空间 2980K,堆总空间 389920K,以及最终标记的总耗时为0.0009399秒
  • CMS-concurrent-sweep(并发清除)

  • CMS-concurrent-reset(并发重置)

G1垃圾回收日志

-XX:+UseG1GC

G1相关的参数

-XX:G1HeapRegionSize=n

Region的大小。但是这不是最终值,Region大小会根据实际情况自动调整的

-XX:MaxGCPauseMillis

一次gc回收期望的STW时间,默认为200ms。G1会尽量在指定的这个时间内完成gc。

-XX:G1NewSizePercent

新生代最小值,默认值5%

-XX:G1MaxNewSizePercent

新生代最大值,默认值60%

-XX:ParallelGCThreads

STW期间,并行GC线程数

-XX:ConcGCThreads=n

并发标记阶段,并行执行的线程数

-XX:InitiatingHeapOccupancyPercent

设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包old+humongous

ps:查看java的参数:

  • java -X     会打印出hotspot的可选参数。
  • java -XX:+PrintFlagsFinal -version     会打印出当前jvm版本所有-XX开头的那些参数

http://www.ppmy.cn/news/3935.html

相关文章

所谓工作能力强,其实就这五点

博客主页:https://tomcat.blog.csdn.net 博主昵称:农民工老王 主要领域:Java、Linux、K8S 期待大家的关注💖点赞👍收藏⭐留言💬 #mermaid-svg-YapmQUqJ0V32EFv6 {font-family:"trebuchet ms",ve…

【大数据技术Hadoop+Spark】MapReduce之单词计数和倒排索引实战(附源码和数据集 超详细)

源码和数据集请点赞关注收藏后评论区留言私信~~~ 一、统计单词出现次数 单词计数是最简单也是最能体现MapReduce思想的程序之一,可以称为MapReduce版“Hello World。其主要功能是统计一系列文本文件中每个单词出现的次数 程序解析 首先MapReduce将文件拆分成spli…

计算机毕设Python+Vue校园失物招领平台(程序+LW+部署)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

十大编程语言黑客向,学会一个不怕没工作,全部学会随便秀操作

首先文章并不是鼓励大家去成为黑客,毕竟这个用在错误的地方,您最终可能需要尝试牢狱之灾。因为有很多的编程语言我也不是很懂,所以借鉴了一些专业人员的看法。当然他们不是黑客。然后下面给大家大概的介绍下其中十个吧。下期为您介绍剩下的几…

使用transformers框架导入bert模型提取中文词向量

导言 在笔者的上一篇文章大白话讲懂word2vec原理和如何使用中提到了如何将词语转变成计算机能够识别的语言,即将文本数据转换成计算机能够运算的数字或者向量这个概念,并详细阐述了word2vec这个模型的原理,如何在gensim框架下使用word2vec将…

蓝桥杯备赛(Day1)

目录 python基础知识点拾遗 将计算所得结果分别赋值给多个变量 两变量数值进行交换 多个变量同时进行赋值 取出三位数中的百位数十位数个位数(水仙花数) 序列 列表 复制与排序 赋值 排序 元组(tuple) 集合(set) 函数 …

[附源码]Python计算机毕业设计个人资金账户管理Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等…

lerna+rollup搭建vue组件库并发布到npm

一、框架介绍 1. lerna Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。 目的是将大型代码仓库分割成多个独立版本化的软件包。 官网点这里 2. rollup Rollup 是一个 JavaScript 模块打包工具,可以将…