当前位置: 首页 > 科技观察

GC详解,看完这篇同事小勇都惊呆了

时间:2023-03-14 11:35:30 科技观察

GC详解,同事小勇看完这篇文章惊呆了,想着什么时候能和这些小姐姐一起讨论人生,好开心,嘿嘿嘿。收起你的哈拉兹,好吧,小勇这个时候总是发出声音,很悦(fu)开心(ck)。小勇:小农,你现在不是提倡垃圾分类吗?究竟什么是垃圾?和小勇一起散步的时候,小勇总是问这样发人深省的问题!我:什么是垃圾,你不是垃圾吗?小勇:去你的,认真的。我:小勇,答应我,以后散步的时候,我们聊点轻一点的,好吗?什么是垃圾,垃圾是指没有引用的对象就是垃圾?小勇:。...,我们去午休我:来吧,我已经讲到这里了,我给你科普一下,你以后是不是想出现在你的简历上——熟悉常见的GC算法,熟悉常见的垃圾收集器,并有实际的JVM调优经验你有什么经验吗?保证让你豁然开朗。以后去面试的时候,把这些保证告诉面试官。小勇:我对你说的有点兴趣,但如果你不明白,那你就是在浪费我的时间,所以我请你吃饭。我没问题,但是我三粉不会同意你的小勇:你没事,请开始你的表演吧~什么是垃圾什么是垃圾,是一个对象或者多个没有任何引用的对象(循环引用),但它们仍然占用内存空间。GC是一种自动存储管理机制。当不再需要某些占用的内存时,应该将其释放。这种存储资源管理称为垃圾收集。就像我们的衣橱,我们可能会在里面放很多衣服,我们可能几个月、几年都不会穿一次,但是这些我们不穿的衣服一直占据着我们的衣橱(内存),我们把这些会穿的衣服不能穿的就扔掉或者捐赠,这样我们就可以多放一些能穿的衣服,类似于“垃圾回收”。在GC中,只分为可回收和不可回收,如下图所示:1.1Java和C++垃圾回收的区别Java是你随便扔垃圾,Java会自动帮你处理,而C++需要需要人工处理,但容易出现忘记回收或多次回收的问题。JavaGC处理开发效率高,执行效率低的垃圾。C++手动处理垃圾,忘记回收,会导致内存泄漏和多次回收,非法访问开发效率高,执行效率高。怎么找垃圾?上面我们知道了什么是垃圾,那我们怎么找垃圾呢?Java中几乎所有的对象实例都存储在堆中。垃圾回收器在回收堆之前,首先要做的是判断这些对象中哪些还“存活”,哪些需要回收(即不再被引用的对象),有两种算法查找垃圾:referencecount(引用计数算法)RootSearching(根可达算法)1.引用计数方法会添加对对象Counter的引用,只要有地方引用它,counter的值就会+1、当引用失效时,计数器的值为-1,计数器值为0的对象不能使用,此时可以判断这个对象是垃圾,当图中的值变为0时,引用这时候可以使用计数算法来判断是垃圾,但是引用计数的方法解决不了一个问题,就是当对象是循环引用的时候,计数器的值不为0,这个时候引用计数器无法通知这GC收集器来回收它们,如下图:这时候就需要用到我们的根可达算法2.根可达算法根可达算法就是从根开始查找,当一个程序启动后,立即需要的对象称为根对象。所谓根可达算法就是先找到根对象,然后沿着这条线找到那些有用的。比如我们Java程序的main()方法运行时,一个main()方法启动了一个线程。线程栈变量:线程中会有线程栈和主栈帧,从这个main()开始的这些对象就是我们的根对象。静态变量:一个类有一个静态变量,静态变量加载到内存后必须立即初始化,所以静态变量到达的对象称为根对象。常量池:如果你的类使用了其他类的对象,这些就是根对象。JNI:如果我们调用对象图中用C和C++写的本地方法或者object5和object6使用的类,虽然互相引用,但是无法从根找到,所以是垃圾,而object8没有任何参考,自然是垃圾。其他的Object对象可以从根找到,所以有用,不会被垃圾回收。3、区分如何清理垃圾我们找到对应的垃圾后,应该如何清理垃圾呢?GC常用的算法有3种:Mark-Sweep(标记去除)Copying(复制)Mark-Compact(标记压缩)1.Mark-clear该算法正如其名。该算法分为两个阶段:“标记”和“清除”。首先对所有需要回收的对象进行标记,标记完成后统一回收所有标记的对象。这是最基本的收集算法,为什么是最基本的,因为后来的收藏家都是基于这个思路,在它的缺点上改进的。标记和清除算法有其自身的小问题。你可以看到上面的图片。我们从GC的根部找到不可回收的。绿色不可回收,紫色可回收。我们回收之后,它就变成Idle了,这个算法比较简单,在存活对象很多的时候效率更高。它需要经过两次扫描。第一次扫描是为了找到那些有用的,第二次扫描是为了去除那些无用的。找出来清理一下,这里会有两个问题:一个是效率问题,标记和清除两个过程效率不高,另一个是空间问题,标记和清除后,大量将生成不连续的空间碎片。太多会导致以后的程序在运行过程中需要分配更大的对象时,无法找到足够的连续内存,不得不提前触发另一个垃圾回收动作。2.复制算法为了解决效率问题,于是出现了复制(Copying)算法,将可用内存按照容量大小分成两块,每次只使用其中一块.当这块内存用完后,将存活的对象分配给另一块,然后一次性清理掉使用过的内存空间,这样每次都回收了整个半个区域,不需要考虑内存fragmentationwhenallocatedmemory在复杂的情况下,只需要移动堆顶指针即可。这适用于存活对象较少的情况,所以更适合eden区。只扫描一次,提高了效率,不会造成碎片,但是会造成空间浪费,将内存减少到原来的一半有点太高了,移动和复制对象需要调整对象的引用。3、标记压缩算法标记压缩是组织万物的过程,清洗过程同时压缩到头部。在回收之前,往前走,把剩下的清理干净是有用的,但是标记压缩算法还是有问题的。我们都是通过GCRoots找到那些不可回收的对象,然后把不可回收的对象往前移动。这时候我们需要扫描两次,需要移动物体。第一遍扫描出有用的对象,第二遍移动,如果移动是多线程的,需要同步,这样效率会低很多,但是不会产生碎片。分配对象也不会产生内存减半。4.SummaryMark-Sweep(MarkClear):标记为垃圾后,进行清理。其他空间还是固定的,效率也不错,但是容易产生碎片。复制(Copy):将内存一分为二,只使用一半,如果垃圾太多,把有用的复制到另一边,剩下的直接在整个内存中清理,效率更高的Mark-Compact(markcompression):把所有的对象放在一起,清理掉所有的垃圾,剩下的空间还是连续的。当你分配其中的任何内容时,你可以直接分配它。堆内存逻辑分区。JVM中的HotSpot使用分代算法。新生代分为:eden,surviveleden(eden):默认比例为8:就是我们在新创建的对象之后投入的区域。Survivor:默认比例1:回收一次后跑到这个区域。因为安装的对象不同,所以采用的算法也不同。不同的是新生代存活对象很少,死对象很多,所以采用的算法是复制算法。老年代:tenured(终生)老年代有很多活的对象。适用于:标记清除和标记压缩算法对象从生到死对象生成后,首先在栈上分配。如果不能分配栈,就会进入伊甸园区。Eden区经过一次垃圾回收后进入Survivor区,Survivor区经过一次垃圾回收后进入另一个Survivor区。同时Eden区的一些物体也进入另一个survivor,当他们足够大的时候,就会进入old区。这是整个对象的逻辑移动过程。什么时候分配到栈上,什么时候分配到伊甸区?1.堆栈分配。这段代码没人知道支持标量替换:就是用普通的属性,用普通的类型替换对象,称为标量替换。堆栈上的分配将比堆上的分配更快。如果不能在栈上分配,会优先分配本地分配,即线程本地分配TLAB(ThreadlocalAllocationBuffer):很多伊甸区的线程都会给它分配对象,但是在分配对象的时候,我们肯定会征用空间。线程的同步会降低效率,所以TLAB机制设计为占用eden,默认1%,1%的空间用于Eden区。这个空间被称为线程独有的。分配对象时,首先分配给线程。可以将这个独有的空间分配给多个线程,而无需争用eden来申请空间,提高了效率。2老年代的对象什么时候进入老年代?多少次被开垦进入老年?超过XX:MaxTenuringThreshold(YGC)ParallelScavenge指定的次数进入老年代15次。CMS进入老年6次。G1进入老年代15次。网上说可以增加数量。这是不可能的。动态年龄判断为了能够适应不同程序的内存状态,虚拟机并不是永久的,要求对象的年龄必须达到MaxTenuringThreshold才能提升到老年代。如果Survivor空间中所有同龄对象的大小之和大于Survivor空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代,无需等待MaxTenuringThreshold要求的年龄。两个幸存者之间复制的时候,只要超过50%,就把最老的直接放到老区,也就是不一定非要15岁。如果s1中的那么多对象复制到s2中超过50%,s1就会被加入Eden区,一次性将整个对象复制到s2中。经过一次垃圾回收,过去之后,整个add对象的数量好像已经超过了s2的一半。一些最古老的物体直接进入老年区。这称为动态青年判断。大对象直接进入老年代。所谓大对象是指需要大量连续内存空间的Java对象。最典型的大对象就是那些很长的字符串和数组。当经常出现大对象时,在还有大量内存空间获取足够连续的内存空间时,很容易提前触发垃圾回收。在堆栈上分配。如果能在栈上分配,就在栈上分配。会直接出栈,弹出结束。如果无法在栈上分配,则判断该对象是否为大对象。如果是大对象,就直接进入老年代。FGC之后,如果没有,就进入线程本地分配(TLAB),不管怎样,都会去Eden区进行GC清理,如果清理完成就直接结束,如果没有清理,就进入S1,S1继续GC清理,如果age到了,进入Old区,如果age不够进入S2,则S2继续清理GC,要么达到age,要么动态age达到MinorGC/YGC:触发MajorGC/新生代空间耗尽时FullGC:当老年代无法继续分配空间时触发,新生代和老年代同时回收常见垃圾收集器新生代收集器:Serial,ParNew,ParallelScavenge老年代垃圾收集器:SerialOld、CMS、ParallelOld新生代和老年代收集器:G1、ZGC、Shenandoah各垃圾收集器不独立运行。下图表明垃圾收集器之间是有联系的,可以配合使用:新一代垃圾收集器1.串行收集器串行收集器是最基本也是最古老的收集器,是一种单线程收集器。它的“单线程”含义并不意味着它只会使用一个处理器或一个收集线程来完成垃圾收集的工作,更重要的是它强调当它收集垃圾时,会暂停所有其他工作线程,直到结束它的集合。根据上图我们可以知道,Serial收集器在运行时,会暂停所有线程,“StopTheWorld”。GC完成后,应用线程会继续执行。就像你有三个女朋友,她们让你同时逛街,你只能陪其中一个,才能陪另一个,当你陪了其中一个,其他闺蜜要等,但是垃圾回收比这种情况复杂多了!优点:因为采用单线程的方式,所以对于单CPU来说是其他类型收集器中效率最高的缺点之一:在用户不可知不可控的情况下暂停所有线程,风险和体验都不好,并且很难让人接受命令:可以启用Serial作为新生代收集器-XX:+UserSerialGC#selectSerial作为新生代垃圾收集器2.ParNew收集器ParNew收集器本质上是多线程并行版本的串行收集器。Serial收集器除了同时使用多个线程进行垃圾收集外,其余的如Serial收集器可用的控制参数、收集算法、StopTheWorld、对象分配规则等与Serial收集器完全相同。在多核机器上,默认启用的手机线程数与CPU数相同,但可以通过参数修改-XX:ParallelGCThreads#设置JVM垃圾回收的线程数。ParNew收集器除了支持多线程并行收集外,与Serial收集器相比并没有太多的创新点,但它是很多HotSpot虚拟机以服务器方式运行,尤其是之前遗留系统中首选的新生代收集器JDK7.一个与功能和性能无关但其实很重要的原因是:除了Serial收集器,目前唯一能与CMS收集器配合使用的优点:随着CPU的有效利用,有利于GC时系统资源的有效利用缺点:和Serial一样的故障使用场景:ParNew是很多运行在Server模式下的虚拟机的首选新生代收集器,因为CMS它只能与Serial或ParNew一起使用。在当今的多核环境下,多线程并行的ParNew是首选。ParNew收集器是激活CMS后默认的新生代收集器(使用-XX:+UseConcMarkSweepGC选项),也可以使用-XX:+/-UseParNewGC选项强制指定或禁用它3.ParallelScavenge收集器ParallelScavenge收集器也是新一代的收集器,也是基于mark-copy算法。并行收集器的多线程收集器,ParallelScavenge收集器的重点与其他收集器不同的是,CMS等收集器的重点是尽可能缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标是可控的吞吐量。所谓吞吐量,就是处理器运行用户代码所花费的时间与处理器总消耗的时间之比,如下图所示:如果一个虚拟机完成一个任务,用户代码加上垃圾收集一共花费了100分钟,其中垃圾回收用了1分钟,那么吞吐量是99%。停顿时间越短,越适合需要与用户交互或需要保证服务响应质量的程序。良好的响应速度可以提高用户体验。垃圾收集器每100秒收集一次,暂停10秒,垃圾收集器每50秒收集一次,暂停7秒。虽然后面的停顿时间更短,但是整体的吞吐量更低,整体的CPUUtilization也更低。您可以使用-XX:MaxGCPauseMillis来设置收集器尽可能长的时间来完成内存回收,您可以使用-XX:GCTimeRatio来精确控制吞吐量。下面是Parallel收集器和ParallelOld收集器结合进行垃圾收集的示意图。在新生代中,当用户线程全部执行到安全点时,所有线程都被挂起执行。ParNew收集器使用多线程并使用复制算法来执行垃圾收集。收集完成后,用户线程继续执行;在老时代,当用户线程执行到安全点后,所有线程暂停执行,ParallelOld收集器采用多线程,使用标记算法进行垃圾收集。老年代垃圾收集器1.SerialOld收集器SerialOld是Serial收集器的老年代版本。它也是一个使用标记排序算法的单线程收集器。这个收集器的主要意义也是针对客户端模式下的HotSpot。虚拟机使用。如果一个与服务器端的ParallelScavenge收集器配合使用,另一个作为CMS收集器出现故障时的备份计划。Serial收集器和SerialOld收集器运行示意图:适用场景:Client模式;单核服务器;搭配ParallelScavenge收集器;作为CMS收集器的备份方案,使用2.ParallelOldwhenConcurrentModeFailure并发收集器ParallelOld收集器是ParallelScavenge收集器的老版本。支持多线程并发收集,基于标记-排序算法实现。可以充分利用多核CPU的计算能力。考虑ParallelScavenge/ParallelOld收集器的运行图:2.CMS收集器CMS(ConcurrentMarkSweep)收集器是一种旨在获得最短恢复停顿时间的收集器。CMS收集器是基于mark-clear算法实现的。这个收集器的操作比以前的收集器稍微复杂一点。整个过程分为四个步骤:1)初始标记(CMSinitialmark):只标记GCRoots可以直接关联的对象,速度非常快2)并发标记(CMSconcurrentmark):遍历整个过程对象图从GCRoots的直接关联对象开始,这个过程耗时较长但不需要暂停用户线程,可以和垃圾回收线程并发运行3)Re-marking(CMSremark):修正期间并发标记,由于用户程序的持续运行,导致标记产生的对象的标记记录。这个阶段的停顿时间会比初始标记阶段稍微长一些4)ConcurrentCleanup(CMSconcurrentsweep):清理删除标记阶段判断的死对象。由于不需要移动幸存的对象,这个阶段也可以与用户线程并发。初始标记和重新标记这两个步骤仍然需要“StopTheWorld”暂停所有用户线程,因为在整个过程中最长的并发标记和并发清理阶段,垃圾收集器线程可以与用户线程一起工作,一般说起来,CMS收集器的内存回收过程是和用户线程并发执行的,如下图:优点:CMS收集器是一款优秀的收集器,主要体现在:并发收集,低暂停。缺点:CMS收集器对处理器资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但也会因为占用了一部分线程而拖慢应用程序并降低总吞吐量。默认情况下,CMS启动的回收线程数为(处理器核心数+3)/4。也就是说,如果处理器核数大于等于4,垃圾回收线程在并发回收时只占用不超过25%的处理器计算资源。服务器资源会随着CPU数量的增加而减少,但是当CPU数量少于4个时,CMS对用户程序的影响可能会变得非常大。CMS收集器无法处理“浮动垃圾”,可能会出现“ConcurrentModeFailure”故障,从而导致另一次“StopTheWorld”FullGC。在CMS的并发标记和并发清理阶段,用户线程还在随着程序的继续运行,程序运行时自然会不断产生新的垃圾对象,但这部分垃圾对象是在标记过程结束后出现的,并且CMS无法在当前收集中将它们处理掉,所以只好留到下一次垃圾收集时再清理掉。这部分垃圾称为“浮动垃圾”,因为CMS是基于“标记-清除”算法的收集器,所以在收集结束时会产生大量的空间碎片。大对象带来很多麻烦,可能要提前进行FullGC操作,但是通过参数优化新生代和老年代垃圾收集器G1收集器GarbageFirst(简称G1):-XX:+UseCMS-CompactAtFullCollection的收集器是垃圾收集器技术发展史上一个里程碑式的成就。首创了收集器针对partialcollection的设计思想和基于Region的内存布局形式。G1收集器是服务器端应用程序的垃圾收集器。当JDK9发布时,它成为了服务器端模式的默认垃圾收集器,而CMS成为了不被推荐的收集器。特点:出现在G1收集器中对于之前的所有其他收集器来说,目标范围要么是新生代要么是老年代,要么是Java堆,但是G1是综合性的,它可以针对堆内存的任何部分形成一个回收集合对于回收,衡量标准不再是它属于哪一代,而是哪一块内存的垃圾存放量最大,回收收益最大。这就是G1收集器的MixedGC模式。G1首创的Region-based堆内存布局是其能够实现这一目标的关键。虽然G1仍然保留了新生代和老年代的概念,但是新生代和老年代已经不再固定。它们是一系列区域的动态集合。G1可以建立一个可预测的停顿时间模型,因为它将Region作为单次回收的最小单位,G1不再坚持固定大小和固定数量的世代区域划分,而是将连续的Java堆划分为多个大小相等的独立区域,每个Region都可以根据需要充当新生代。在Eden空间、Survivor空间或老年空间中,收集器可以采用不同的策略来处理扮演不同角色的Region。CNOOC区域是一个特殊的Humongous区域,专门用来存放大件物品。G1认为只要对象的大小超过一个Region容量的一半,就可以判断为大对象。G1收集器的运行过程:InitialMarking:标记GCRoots可以直接关联的对象,修改TAMS指针的值,以便用户线程在下一阶段并发运行时,可以正确分配新的可用Region中的对象需要短暂停顿线程,但是在借用MinorGC时是同步完成的,所以这个阶段其实并没有额外的暂停并发标记(ConcurrentMarking):从GCRoots看,堆中的对象是可达的分析,递归扫描整个堆中的对象图,找出需要回收的对象。该阶段耗时较长,但可以与用户程序并发执行。最终标记(FinalMarking):在用户线程上再做一次短暂的停顿,过滤回收用户处理并发阶段后留下的最后几条SATB记录(LiveDataCountingandEvacuation):负责更新Region的统计信息,对每个Region的回收价值和成本进行排序,根据用户锁的预期停顿时间制定回收计划。你只能选择任意数量的Region组成一个回收集合,然后将你决定回收的那部分Region的存活对象分配给空的Region,然后清理掉整个Region的所有空间。小勇,你明白吗?小勇小勇,别睡了,我还没说完呢!小勇醒醒!!!小勇愣了一下:怎么了,下班了吗?....,下班后,你明白我说的GC了吗?小勇:明白了,我明天去面试,你说的太好了!敷衍了事,算了,我都已经记在笔记里了,各位有兴趣的可以看看。本文转载自微信公众号“沐小农”,可通过以下二维码关注。转载本文请联系沐小农公众号。