JVM的内存模型:
1.7前:分为年轻代、老年代和永久代
年轻代:分为了三部分,Eden区和两个大小严格相同的Survivor区(默认的大小分配是8:1:1),其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活的对象将被移动到老年代。也就是说年轻代存放的大多是生命周期短的对象。
tip:年轻代进行Minor GC时会STW(Stop The World,停止业务线程)
老年代:存放的大多是生命周期较长的对象。那么哪些对象可以进入老年代呢,通常有两种途径,1.一个对象在年轻代中经过多次垃圾回收(好像是15次吧)仍然存活,说明该对象的生命周期长,会将它复制到老年代。2.因为年轻代中只有一个survivor区可以存放每次GC存活下的对象,但是一个survivor区默认只占年轻代的10%空间小,当存活对象大于这个空间时,会触发内存担保机制将对象复制到老年代。
永久代:主要用于存放类的元数据(类信息、方法等),当一次加载大量类的时候会导致OOM。
Minor GC:对年轻代进行垃圾回收,耗时短;
触发时机:当年轻代内存不足时
Full GC:对年轻代和老年代都进行垃圾回收,耗时长;
触发时机:当老年代内存不足时
在1.7的内存模型中,无论是年轻代、老年代、永久代使用的都是JVM的堆内存。在jdk1.8后,删除了永久代,使用了元空间进行替代,而元空间使用的是本地内存。
元空间:使用本地内存,这样使得内存分配更灵活,可以根据数据量灵活调整。
垃圾回收:
如何判断垃圾:
主要采用了两种算法:引用计数法和可达性分析法。
-
引用计数法(Reference Counting):
-
每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加1;当引用离开作用域或被置为null时,引用计数减1。当引用计数为0时,该对象即可被视为垃圾并回收。
-
优点:实现简单,能够实时地回收内存,不需要暂停应用程序。
-
缺点:无法处理循环引用的情况,即两个对象相互引用但没有其他地方引用它们时,引用计数永远不会为0,导致无法回收。
-
-
可达性分析法(Reachability Analysis):
-
这是Java中最常用的垃圾判断方法。它通过一组称为根对象的引用集合(如栈中的引用、静态字段中的引用等)来开始遍历。任何可以从根对象到达的对象都被认为是存活的,反之则是垃圾。
-
这个过程可以有效地识别哪些对象不再使用,从而进行回收。
-
垃圾处理的算法:
-
标记-清除(Mark-Sweep):
-
标记阶段:垃圾回收器遍历所有可达对象(从根节点开始),并将它们标记为活跃的。
-
清除阶段:清除所有未标记的对象,即那些不再被引用的对象。
-
缺点:这种方法会产生内存碎片,影响内存的连续性。
-
-
标记-整理(Mark-Compact):
-
标记阶段:与标记-清除相同,标记所有可达对象。
-
整理阶段:将所有存活的对象移动到内存的一端,以消除内存碎片。
-
优点:解决了内存碎片问题,但需要更多的处理时间。
-
-
标记-复制(Mark-Copying):
-
将内存分为两个半区,每次只使用其中一个半区。
-
当一个半区满时,将所有存活的对象复制到另一个半区,并清除当前半区。
-
优点:简单且高效,特别适合新生代对象(大多数对象生命周期短)。
-
缺点:浪费内存空间,因为只有一半的内存可用。
-
处理策略:
-
分代收集(Generational Collection):
-
将堆内存分为新生代(Young Generation)和老年代(Old Generation)。
-
新生代:对象生命周期短,使用复制算法。
-
老年代:对象生命周期长,使用标记-清除或标记-整理算法。
-
优点:利用了对象的生命周期特性,提高了垃圾回收的效率。
-
-
增量收集(Incremental Collection):
-
将垃圾回收任务分解为多个小的步骤,逐步执行。
-
优点:减少单次垃圾回收的停顿时间,提高程序的响应性。
-
-
并发收集(Concurrent Collection):
-
垃圾回收器与应用程序线程并发执行,减少停顿时间。
-
优点:适用于长时间运行的应用程序,减少对应用程序性能的影响。
-
-
实时收集(Real-time Collection):
-
保证垃圾回收在特定的时间内完成,适用于实时系统。
-
优点:提供可预测的垃圾回收时间。
-
主要垃圾收集器:
-
Serial GC(年轻代GC):
-
面向单核处理器环境,使用单线程进行垃圾回收,不会使用多个线程来进行垃圾回收,在回收的过程中,会停止所有业务线程(stop-the-world简称:stw),用于Minor-GC。
-
适用于小型应用程序和客户端系统。
-
-
ParNew GC(年轻代GC):
-
是Serial GC的多线程版本,除了引入多线程外其余与Serial GC无大变化,也存在STW问题。
-
-
Parallel GC(年轻代GC)(也称为Throughput Collector,ps):
-
使用多线程进行垃圾回收,以提高吞吐量(业务线程工作时间/(业务线程工作时间+垃圾回收时间))。
-
适用于多核处理器和服务器应用程序。
-
-
Serial-Old GC(老年代GC):
-
Serial-Old GC也是一种单线程的垃圾回收器,它在一个单独的线程上运行,不会使用多个线程来进行垃圾回收,在回收的过程中,会停止所有业务线程(stop-the-world简称:stw),用于Full-GC。
-
-
Parallel-Old GC(老年代GC)(po)(jdk1.8默认ps + po):
-
是parallel GC的老年代版本,由于与ps一样都能控制吞吐量,所以与ps配合使用效果很好
-
-
Concurrent Mark-Sweep (CMS) GC(老年代GC):
-
旨在减少垃圾回收的停顿时间,通过并发执行标记和清除阶段来实现。
-
使用的是三色标记算法(详细解释请往下看)。
-
适用于对响应时间敏感的应用程序。
-
缺点:1.对cpu资源敏感;2.无法处理浮动垃圾(也就是并发清理阶段产生的垃圾,因为已经过了标记阶段了);3.因为使用的是标记-清除算法所以会产生大量的内存碎片。
-
-
G1 (Garbage-First) GC(jdk9后默认的GC处理器,最重要的GC):
-
一种服务器端的垃圾回收器,旨在提供可预测的停顿时间。
-
将堆内存划分为多个区域,并优先回收垃圾最多的区域。
-
使用的是三色标记算法(详细解释请往下看)。
-
适用于大堆内存和多核处理器的环境。
-
G1详细说明:
三色标记算法:
三色标记算法,发生在初始标记,并发标记,重新标记阶段,主要由3种颜色组成:黑,灰,白
黑:对象被标记,且它的所有子对象也被标记完
灰:对象被标记,但是它的子对象还没有被标记或标记完
白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉
工作过程:
-
初始标记阶段:指的是标记 GCRoots 直接引用的节点,将它们标记为灰色,这个阶段需要 「Stop the World」,因为只考虑直接引用的情况,时间花费短所以STW时间短。
-
并发标记阶段:指的是从灰色节点开始,去扫描整个引用链,然后将已经标记过的对象标记为黑色,这个阶段不需要「Stop the World」。
-
重新标记阶段:指的是去校正并发标记阶段的错误,这个阶段需要「Stop the World」。
-
并发清除:指的是将已经确定为垃圾的对象清除掉,这个阶段不需要「Stop the World」。
问题:
由于多线程的特性,在标记过程中可能会产生多标、漏标的问题。
多标:多标问题指的是原本应该回收的对象,被多余地标记为黑色存活对象,从而导致该垃圾对象没有被回收。
最初:
A->B->C
当A被标记为黑色,B被标记为灰色后,GC线程停止标记,而业务线程将A->B的引用断开了(A不再引用B)
A B->C
这时B就变得不可达,B、C应该被当成垃圾回收,但是由于B已经是灰色了,所以在重新标记阶段B还是会被访问,B被标记为黑色,C被标记为灰色,这样B、C就不会被GC回收。缺点:
产出多余垃圾,但是这些垃圾会在接下来的GC中被清除,影响不大。
漏标:漏标问题指的是原本应该被标记为存活的对象,被遗漏标记为黑色,从而导致该垃圾对象被错误回收。
最初:
A->B->C
当A被标记为黑色,B被标记为灰色后,GC线程停止标记,而业务线程将B->C的引用断开了(B不再引用C),而A引用了C
A->B C
| |
|_____>
这时因为并发标记与重新标记阶段都是从灰色对象开始遍历,而C没有与任何一个灰色对象连接,所以C不会被标记,会被当成垃圾回收,这时候问题就很严重了,应为A还引用了C呢,在业务层面需要C对象,这必然会导致严重的事故。G1对于漏标的问题,它采用的解决方案是:原始快照。
原始快照,GC线程在退出运行时,将所有灰色节点下的所有的引用,拍摄快照到Rset中;GC线程恢复后,直接使用快照数据,这样保证了引用对象的完整,缺点就是会产生多余垃圾,但是相比于漏标的危害就不值一提了。
主要特点
-
并行与并发:
-
G1 GC能够充分利用多核处理器的优势,通过并行执行垃圾收集任务来提高效率。同时,它的大部分工作都是与应用线程并发执行的,从而减少了停顿时间。
-
-
分区域收集(分区+逻辑分代):
-
G1 GC将整个堆内存划分为多个大小相等的独立区域(Region),这些区域在逻辑上是连续的,但在物理内存上可能不是连续的。每个Region都可以扮演Eden区、Survivor区或Old区等角色。这种设计使得G1 GC能够更加灵活地进行内存管理和垃圾收集。
-
-
优先回收垃圾最多区域:
-
G1 GC通过跟踪每个Region中的垃圾堆积情况,并根据回收价值和成本进行排序,优先回收垃圾最多的Region。这种策略有助于最大限度地提高垃圾收集的效率。
-
-
可预测的停顿时间:
-
G1 GC通过建立一个可预测的停顿时间模型,允许用户明确指定在一个特定时间片段内,垃圾收集所造成的停顿时间不得超过某个阈值。这使得G1 GC非常适合需要严格控制停顿时间的应用场景。
-
-
空间整合:
-
在整体上,G1 GC使用标记-整理算法来回收内存,以减少内存碎片的产生。但在两个Region之间进行垃圾收集时,它则采用标记-复制算法。这种组合策略有助于兼顾内存利用率和垃圾收集效率。
-
工作原理
-
初始标记
-
通过可达性分析法标记所有可达对象。
-
-
并发标记(Concurrent Marking):
-
在初始标记完成后,G1 GC会进入并发标记阶段。这个阶段与应用程序线程并发执行,通过递归地追踪所有可达的对象,并将它们标记为存活。这个过程是并发的,因此不会阻塞应用程序的执行。
-
-
最终标记(Final Marking):
-
为了处理在并发标记过程中新产生的对象引用关系,G1 GC会执行一次短暂的STW的最终标记。这个阶段确保所有在并发标记阶段漏掉的对象都被正确标记。
-
-
筛选回收(Live Data Counting and Evacuation):
-
在这个阶段,G1 GC会根据每个Region的垃圾堆积情况和回收价值进行排序,并选择性地回收部分Region中的垃圾对象。回收过程包括将存活的对象从一个Region复制或移动到另一个Region,并更新相关的引用。这个过程也是并发的,旨在最大限度地减少停顿时间。同时,这个阶段可能会涉及到对象的整理和压缩,以减少内存碎片。
-
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的 Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,而且 一个大对象如果太大,可能会横跨多个Region来存放。
G1的垃圾回收过程主要包括如下三个环节:
-
年轻代GC(Young GC)
-
老年代并发标记过程(Concurrent Marking)
-
混合回收(Mixed GC)
-
Full GC(如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)
young gc一>young gc+concurrent mark一>Mixed GC)顺序,进行垃圾回收。
1.应用程序分配内存,当年轻代的Ed区用尽时开始年轻代回收过程:G1的年轻代收集阶段是一个并行的(多个回收线程)独占式(STW收集器。在年轻代回收期,G1GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。
2.当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程
3.标记完成马上开始混合回收过程。对于一个混合回收期,G1GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的.
JVM配置:
G1 GC的设计目标是在满足高吞吐量的同时,尽可能缩短垃圾收集造成的停顿时间。它通过并发和并行的垃圾收集阶段来实现这一目标。
tip:图片转载自:Java进阶(垃圾回收GC)——理论篇:JVM内存模型 & 垃圾回收定位清除算法 & JVM中的垃圾回收器_java的内存模型以及gc算法-CSDN博客
和B站黑马jvm垃圾回收机制。