JVM【三】——垃圾回收GC
堆空间的基本结构
Java的自动内存管理主要针对的是对象内存的回收和对象内存的分配,由于在JVM中存放对象的位置位于堆,是垃圾收集器管理的主要区域,因此堆也叫作GC堆(Garbage Collected Heap)。
在JDK7版本和JDK7版本以前,堆内存被通常分为以下三部分:
- 新生代内存(Young Generation)
- 老年代内存(Old Generation)
- 永生代内存 (Permanent Generation)
JDK8版本以后,永生代已经被元空间取代,而元空间使用的是直接内存,元空间自动扩展,减少了固定内存分配导致OOM的频率,不过如果内存占用过大依旧会OOM。
新生代内存又分为Eden区(伊甸园区)和Survivor区(幸存者区),由一个Eden区+两个Survivor区组成,默认内存比例为8:1:1,而新生代和老年代的默认内存比例为1:3。
堆内存结构
内存分配和回收原则
1.大多数情况下,新创建的对象会在新生代的Eden区分配内存。当Eden区没有足够的空间进行分配时,会发起一次Minor GC(Young GC)来进行新生代的内存回收。
2.存活下来的对象会被转移到Survivor区,如果发现对象过大,无法存入Survivor空间,就只能通过分配担保机制将新生代对象提前移入老年代中去,如果老年代空间足够,则不会触发Major GC(Old GC),否则会进行一次Major GC清理老年代中不再使用的对象,释放他们的内存。
空间分配担保:为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC(整堆收集,新生代和老年代)。
3.对象首次由Eden区进入Survivor区后初始年龄变为1,然后每经历一次Minor GC,会将Eden区和当前Survivor区的对象移动到另一个Survivor区(至少时刻确保其中一个Survivor区为空),并且年龄+1,当它的年龄增加到一定程度(默认为 15 岁),就会晋升到老生代中,晋升老生代的年龄阈值可以通过-XX:MaxTenuringThreshold=<N>来设置,不过最大值为15(这是因为在对象头中,是用4位比特来对年龄进行存储的,这四位所能表达的最大二进制数是1111,转换为十进制就是15)
4.在老年代,相对悠闲,当老年代内存不足时,则会出发Major GC ,进行老年代的内存清理,若执行后依然无法进行对象保存,就会产生 OOM (OutOfMemory)异常(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx
参数配置)。
主要进行GC的区域
针对HotSpot VM的实现,它里面的GC只有两大类:
部分收集(Partial GC)):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集,所以说的时候要指明是哪种;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集(Full GC):收集整个 Java 堆和方法区。
死亡对象的判断方法
引用计数法
这个方法很简单,就是对象身上有一个引用计数器,每有一个地方引用他,计数器就+1,当引用失效时,就-1,当计数器为0的时候,表示对象不再被任何变量引用,可以被回收。
这种方法实现简单,效率高,但是没有被采用是因为他有一个很大的缺陷,没有办法解决对象之间循环引用的问题。
对象循环引用
除了对象Object A和Object B 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
可达性分析算法
从一组被称为“GC Roots”(垃圾收集根)的对象出发,向下追溯他们直接引用或间接引用的对象,遍历到每个对象的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连(不可达)的话,则证明此对象是不可用的,需要被回收。
可以作为GC Roots的对象主要包括
- 虚拟机栈栈帧的局部变量表中正在引用的对象
- 本地方法栈正在引用的对象
- 静态属性引用的对象
- 方法区常量引用的对象
可达性分析算法
如图所示,Object1~Object3到CG Roots之间是有引用链的,是可达的,而Object4~Object6 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。
引用类型总结
引用类型主要分为强软弱虚四种:
1. 强引用(StrongReference):我们使用的大部分引用都是强引用,这也是最普遍的引用,代码中的普遍赋值,A a = new A() 这种。强关联的对象就好比我们日常生活的必需品,垃圾回收器绝不会回收它,当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2. 软引用(SoftReference):指的是那些有用,但不是必须要的对象,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference):相比于软引用,弱引用的强度还要更低,只有弱引用的对象生命周期更为短暂,不管当前内存空间够不够,只要垃圾回收器发现了它,就会将他回收。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference):"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
package ReferenceType;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class Strong {
public static void main(String[] args) {
A strongReference = new A();
System.out.println("强引用: " + strongReference);
//创建软引用
SoftReference softReference = new SoftReference<>(new A());
System.out.println("软引用: " + softReference);
//创建弱引用
WeakReference weakReference = new WeakReference<>(new A());
System.out.println("弱引用" + weakReference);
// 创建一个引用队列
ReferenceQueue<A> referenceQueue = new ReferenceQueue<>();
// 创建虚引用
PhantomReference<A> phantomReference = new PhantomReference<>(new A(), referenceQueue);
System.out.println("虚引用: " + phantomReference.get()); // 虚引用的get()总是返回null
// GC
System.gc();
// 输出软引用和弱引用在GC后的状态
System.out.println("GC后 - 软引用: " + softReference.get()); // 肯定返回对象内存地址
System.out.println("GC后 - 软引用: " + softReference.get()); // 可能返回对象或null(取决于当前内存状态)
System.out.println("GC后 - 弱引用: " + weakReference.get()); // 返回null,已经被收集
System.out.println("GC后 - 虚引用: " + phantomReference.get()); // 一直为null
// 检查虚引用是否被加入引用队列
if (phantomReference.isEnqueued()) {
System.out.println("虚引用已被加入引用队列");
} else {
System.out.println("虚引用未被加入引用队列");
}
}
}
class A{
public A() {
}
}
虚引用主要用来跟踪对象被垃圾回收的活动
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止OOM等问题的产生。
关于对象判断是否能回收的就聊这么多,接下来简单介绍下常量和类的判断
常量是否可回收的判断方法
运行时常量池主要回收的是废弃的常量,如何判断一个常量是废弃常量呢?
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了
在此再介绍以下方法区,字符串常量池等相关知识
- JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代
- JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
- JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
主要介绍三种常量:
1. 字符串常量池的常量:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建,通常不会被回收。
2. Integer缓存池的常量:Integer类型的常量在 JVM 内部有一个缓存池,范围是 -128~127。这些整数常量通常不会被回收,因为它们在 JVM 启动时会被缓存。
以上两种常量,都会在JVM的生命周期中保留
3. final 关键字修饰的常量,编译期已经确定,它们通常会被嵌入到字节码文件中,而不是像普通的对象一样通过引用来访问。这意味着这些常量通常不依赖于 JVM 的垃圾回收机制。这种常量的生命周期通常和类的生命周期相同,直到类卸载或者程序结束。
类是否可回收的判断方法
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类”:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。这里的知识其实也是类的生命周期卸载类的部分。
垃圾收集算法
“标记”--“清除”算法
标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先根据可达性分析算法标记出所有不需要回收的对象(GC后存活的对象),然后在标记完成后统一回收掉所有没有被标记的对象。
它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:标记清除过后会产生大量不连续的内存碎片,影响使用
标记—清除算法
“复制”算法
为了解决标记-清除算法的效率和内存碎片问题,复制(Copy)收集算法出现了,他将内存分为大小相等的两块,每次仅使用其中的一块用来存储对象,当发生垃圾收集时,就会通过可达性分析算法,遍历每一个对象,判断他们是否存活,然后将还存活的对象复制到另一块内存去,然后再把当前这块使用过的空间全部清理。然后交换立场,这样就使每次的内存回收都是对内存区间的一半进行回收。
你可能会疑惑,这里的寻找存活对象不也是通过可达性分析算法进行的吗?跟标记清除算法的标记阶段不是如出一辙吗?那为何不叫“标记”-“复制”算法,而是叫做“复制”算法
这是因为“复制”算法中并不存在独立标记阶段,显式的进行标记的过程,而是在遍历对象的过程中直接复制到另一块内存区域,不需要进行标记
复制算法
尽管复制算法改进了标记-清除算法,但依旧存在下面这些问题:
- 内存利用率不高:由于每次使用只是用其中一块,可用内存缩小为原来的一半
- 不适合老年代:老年代对象长期存活,复制算法的效率会变得较差,因为需要频繁地复制大量对象。
这种算法思想在JVM中进行了改进:
由于在JVM的新生代中,大部分对象“朝生夕死”,生命周期非常短暂,在发生GC时,需要回收的对象特别多,存活的对象很少,因此需要复制到另一块内存的对象很少,不要按照1:1的比例划分内存空间,这样很容易造成内存空间的浪费。而是而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。
首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。
但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。
老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。
“标记”--“整理”算法
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。这是为了避免内存碎片,保持连续的空闲空间。这一阶段的重点是将存活的对象移动到一起,从而腾出一个大块的连续空闲内存。
标记—整理算法
由于多了整理这一步,然后再进行的清除,所以说白了是三步,“标记”-“整理”-“清除”(但事实上只有标记,整理两阶段),因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法(主要采用)
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
JDK 默认垃圾收集器(使用 java -XX:PrintCommandLineFlags -version 命令查看):
- JDK 8: Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK22: G1
STW
要了解垃圾收集器,首先要了解一个概念——STW
STW是 Stop-The-World 的缩写,是在垃圾回收算法执行过程中,将JVM内存冻结,应用程序暂停的一种状态
可达性分析算法中枚举根节点(GC Roots)会导致所有的Java线程停顿,进入STW状态
- 1.在STW状态下,Java的所有线程都是停止执行的,GC线程除外
- 2.一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
- 3.STW是不可避免的,垃圾回收算法执⾏一定会出现STW,我们要做的只是减少停顿的时间
GC各种算法优化的重点,就是减少STW(暂停),同时这也是JVM调优的重点。
如果系统卡顿很明显,大概率就是频繁执行GC垃圾回收,频繁进入STW状态产生停顿的缘故
记住,目前所有垃圾收集器都会进入STW机制,只能是减少STW暂停
串行-并行-并发
在此介绍下这三个词的概念
- 串行:串行意味着任务一次只能执行一个线程或进程,即单线程工作模式。一个任务必须在另一个任务完成后才能开始,顺序执行,任务之间没有重叠,适用于单核 CPU 或对性能要求不高的场景。
- 并行:并行意味着多个任务同时在多个 CPU 核心上执行。并行要求有多核处理器来真正地同时运行多个任务。每个任务在独立的 CPU 核上执行,任务之间互不干扰,以达到更高的吞吐量。
- 并发:并发意味着任务在同一时间段内交替执行,而不是完全同时执行。它通常会让多个任务在一个 CPU 上轮流执行(在多核环境中也可以是多个核上交替执行)。并发强调任务之间的快速切换,而不一定是真正的同时执行。在并发环境中,任务可能互相等待资源,尤其是在共享资源时。
在垃圾收集器中的应用:
- 串行收集器:单线程执行垃圾回收,通常会暂停应用线程,比如Serial GC。
- 并行收集器:利用多线程并行执行垃圾回收任务,比如 Parallel GC,会停止应用线程但利用多线程提升回收速度。
- 并发收集器:在某些阶段与应用线程并发执行,比如 CMS 和 G1,它们在标记阶段与应用线程并发工作,尽量降低垃圾回收的停顿时间。
常见的的垃圾收集器
收集器 | 作用区域 | 收集算法 | 特点 |
Serial | 新生代 | 复制算法 | 单线程,串行 |
Serial Old | 老年代 | 标记-整理 | Serial的老年代版本 |
Parallel Scavenge | 新生代 | 复制算法 | 多线程,并行,注重吞吐量 |
Parallel Old | 老年代 | 标记-整理 | Parallel Scavenge的老年代版本 |
ParNew | 新生代 | 复制算法 | 与Parallel Scavenge一样,效果做了增强,以便与CMS配合使用 |
CMS | 老年代 | 标记-清除 | 多线程,并发,低停顿,产生内存碎片 |
G1 | 整堆 | 标记-整理 | 多线程,并发,区域优先回收,停顿可控 |
一. Serial收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。
虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
二. Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
三.Parallel Scavenge收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,是能够并行收集的多线程收集器。关注点是吞吐量(高效率的利用 CPU)所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99% 。Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。
Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
四.Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old
五.ParNew收集器
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
六.CMS收集器(重点)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记:
- 并发标记
- 重新标记
- 并发清楚
1.初始标记(触发STW):暂停所有的其他线程,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快 。
2.并发标记(不触发STW):并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。所以要进行一次重新标记。
3.重新标记(触发STW):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,就是并发标记过程中产生的新垃圾,进行标记,或者到重新标记这一阶段,又要用到并发标记已经标记的垃圾,取消垃圾标记。 这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
4.并发清理(不处罚STW):开启用户线程,同时GC线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
解释下缺点1和2:
缺点1是因为CMS是多线程的垃圾收集器,标记阶段和清除阶段会与应用程序线程并发执行这种操作在一定程度上会抢占 CPU 资源;
缺点2是CMS 在标记阶段标记存活对象后,应用线程会继续运行并创建新的对象。这些新创建的对象如果在回收过程之后才被标记到,将无法被清理掉,成为“浮动垃圾”。CMS 的清除阶段不再重新标记,而是直接清理没有标记的对象区域,所以浮动垃圾会积累到下一次 GC 才能回收
七.G1收集器(重点)
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
内存模型:
G1 收集器不采用传统的新生代和老年代物理隔离的布局方式,仅在逻辑上划分新生代和老年代,将整个堆内存划分为2048个大小相等的独立内存块Region,每个Region是逻辑连续的一段内存,具体大小根据堆的实际大小而定,G1不再要求相同类型的 Region 在物理内存上相邻,而是通过Region的动态分配方式实现逻辑上的连续。
G1垃圾收集器运作大致分为以下几个步骤:
初始标记(触发STW):与CMS同名步骤相同
并发标记(不触发STW):与CMS同名步骤相同
最终标记(触发STW):类似CMS的重新标记步骤
筛选回收(部分触发STW)
重点介绍下G1的最后一步——筛选回收
这一步要做的事情比较多 ,包括筛选Region,进行回收,整理对象,空间压缩等,其中筛选Region,整理对象,压缩空间清除碎片这些过程中会触发短暂的STW,而回收过程是与应用线程并发运行的,不触发STW,所以我上边用了 部分触发STW来形容这个步骤。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
原文地址:https://blog.csdn.net/qq_65754164/article/details/142910234
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!