Java教程:9种常见的CMS GC问题详解(一)

开课吧开课吧锤锤2021-03-22 09:42

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

Java

    1.1引言

    自Sun发布Java语言以来,开始使用GC技术来进行内存自动管理,避免了手动管理带来的悬挂指针(DanglingPointer)问题,很大程度上提升了开发效率,从此GC技术也一举成名。GC有着非常悠久的历史,1960年有着“Lisp之父”和“人工智能之父”之称的JohnMcCarthy就在论文中发布了GC算法,60年以来,GC技术的发展也突飞猛进,但不管是多么前沿的收集器也都是基于三种基本算法的组合或应用,也就是说GC要解决的根本问题这么多年一直都没有变过。笔者认为,在不太远的将来,GC技术依然不会过时,比起日新月异的新技术,GC这门古典技术更值得我们学习。

    目前,互联网上Java的GC资料要么是主要讲解理论,要么就是针对单一场景的GC问题进行了剖析,对整个体系总结的资料少之又少。前车之鉴,后事之师,美团的几位工程师搜集了内部各种GC问题的分析文章,并结合个人的理解做了一些总结,希望能起到“抛砖引玉”的作用,文中若有错误之处,还请大家不吝指正。

    GC问题处理能力能不能系统性掌握?一些影响因素都是互为因果的问题该怎么分析?比如一个服务RT突然上涨,有GC耗时增大、线程Block增多、慢查询增多、CPU负载高四个表象,到底哪个是诱因?如何判断GC有没有问题?使用CMS有哪些常见问题?如何判断根因是什么?如何解决或避免这些问题?阅读完本文,相信你将会对CMSGC的问题处理有一个系统性的认知,更能游刃有余地解决这些问题,下面就让我们开始吧!

    1.2概览

    想要系统性地掌握GC问题处理,笔者这里给出一个学习路径,整体文章的框架也是按照这个结构展开,主要分四大步。

Java

    建立知识体系:从JVM的内存结构到垃圾收集的算法和收集器,学习GC的基础知识,掌握一些常用的GC问题分析工具。

    确定评价指标:了解基本GC的评价方法,摸清如何设定独立系统的指标,以及在业务场景中判断GC是否存在问题的手段。

    场景调优实践:运用掌握的知识和系统评价指标,分析与解决九种CMS中常见GC问题场景。

    总结优化经验:对整体过程做总结并提出笔者的几点建议,同时将总结到的经验完善到知识体系之中。

    2.GC基础

    在正式开始前,先做些简要铺垫,介绍下JVM内存划分、收集算法、收集器等常用概念介绍,基础比较好的同学可以直接跳过这部分。

    2.1基础概念

    GC:GC本身有三种语义,下文需要根据具体场景带入不同的语义:

    GarbageCollection:垃圾收集技术,名词。

    GarbageCollector:垃圾收集器,名词。

    GarbageCollecting:垃圾收集动作,动词。

    Mutator:生产垃圾的角色,也就是我们的应用程序,垃圾制造者,通过Allocator进行allocate和free。

    TLAB:ThreadLocalAllocationBuffer的简写,基于CAS的独享线程(MutatorThreads)可以优先将对象分配在Eden中的一块内存,因为是Java线程独享的内存区没有锁竞争,所以分配速度更快,每个TLAB都是一个线程独享的。

    CardTable:中文翻译为卡表,主要是用来标记卡页的状态,每个卡表项对应一个卡页。当卡页中一个对象引用有写操作时,写屏障将会标记对象所在的卡表状态改为dirty,卡表的本质是用来解决跨代引用的问题。具体怎么解决的可以参考StackOverflow上的这个问题how-actually-card-table-and-writer-barrier-works,或者研读一下cardTableRS.app中的源码。

    2.2JVM内存划分

    从JCP(JavaCommunityProcess)的官网中可以看到,目前Java版本最新已经到了Java16,未来的Java17以及现在的Java11和Java8是LTS版本,JVM规范也在随着迭代在变更,由于本文主要讨论CMS,此处还是放Java8的内存结构。

Java

    GC主要工作在Heap区和MetaSpace区(上图蓝色部分),在DirectMemory中,如果使用的是DirectByteBuffer,那么在分配内存不够时则是GC通过Cleaner#clean间接管理。

    任何自动内存管理系统都会面临的步骤:为新对象分配空间,然后收集垃圾对象空间,下面我们就展开介绍一下这些基础知识。

    2.3分配对象

    Java中对象地址操作主要使用Unsafe调用了C的allocate和free两个方法,分配方法有两种:

    空闲链表(freelist):通过额外的存储记录空闲的地址,将随机IO变为顺序IO,但带来了额外的空间消耗。

    碰撞指针(bumppointer):通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。

    2.4收集对象

    2.4.1识别垃圾

    引用计数法(ReferenceCounting):对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象。虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。

    可达性分析,又称引用链法(TracingGC):从GCRoot开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前Java中主流的虚拟机均采用此算法。

    备注:引用计数法是可以处理循环引用问题的,下次面试时不要再这么说啦~~

    2.4.2收集算法

    自从有自动内存管理出现之时就有的一些收集算法,不同的收集器也是在不同场景下进行组合。

    Mark-Sweep(标记-清除):回收过程主要分为两个阶段,第一阶段为追踪(Tracing)阶段,即从GCRoot开始遍历对象图,并标记(Mark)所遇到的每个对象,第二阶段为清除(Sweep)阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(TricolourAbstraction)、位图标记(BitMap)等技术来提高算法的效率,存活对象较多时较高效。

    Mark-Compact(标记-整理):这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与Mark-Sweep类似,第二阶段则会对存活对象按照整理顺序(CompactionOrder)进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(ThreadedCompaction)算法等。

    Copying(复制):将空间分为两个大小相同的From和To两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。有递归(RobertR.Fenichel和JeromeC.Yochelson提出)和迭代(Cheney提出)算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。

    三种算法在是否移动对象、空间和时间方面的一些对比,假设存活对象数量为*L*、堆空间大小为*H*,则:

Java

    把mark、sweep、compaction、copying这几种动作的耗时放在一起看,大致有这样的关系:

Java

    虽然compaction与copying都涉及移动对象,但取决于具体算法,compaction可能要先计算一次对象的目标地址,然后修正指针,最后再移动对象。copying则可以把这几件事情合为一体来做,所以可以快一些。另外,还需要留意GC带来的开销不能只看Collector的耗时,还得看Allocator。如果能保证内存没碎片,分配就可以用pointerbumping方式,只需要挪一个指针就完成了分配,非常快。而如果内存有碎片就得用freelist之类的方式管理,分配速度通常会慢一些。

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享