JVM:内存区域划分、类加载的过程、垃圾回收机制
目录
内存区域划分
JVM类加载
类加载过程
双亲委派模型
默认类加载器
双亲委派模型的工作流程
双亲委派模型的优点/目的
如何破坏双亲委派模型
垃圾回收机制
垃圾的判断方法
引用计数法
可达性分析法(Java采用)
JVM的四种引用
垃圾回收算法
标记-清除算法
复制算法
标记-整理算法
分代收集算法
垃圾收集器
Serial和Serial Old收集器
ParNew收集器
Parallel Scavenge 和 Parallel Old收集器
CMS收集器(老年代)
G1收集器
总结
JVM是Java运行的基础,也是实现一次编译处处运行的关键。Java程序在执行前首先需要将Java代码转换成字节码(.class文件),JVM首先通过类加载器将字节码文件加载到内存中的运行时数据区,而字节码文件是JVM的一套指令集规范,不能直接将它交给底层操作系统执行,因此需要使用执行引擎这个特定的命令解析器将字节码翻译成底层操作系统指令再交给CPU去执行,这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能。
内存区域划分
此处的内存区域划分即JVM运行时数据区域,由堆、Java虚拟机栈、本地方法栈、程序计数器、元数据区五大部分组成。
· 堆(线程共享):堆是JVM中内存最大的空间,程序中创建(new)的所有对象都保存在堆中。堆中分为两个区域新生代和老生代,新生代存放新建的对象,当经过一定次数GC之后还存活的对象会存储在老生代。
· Java虚拟机栈(线程私有):Java虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。通常所说的堆内存栈内存中的栈内存就是指Java虚拟机栈。
· 本地方法栈(线程私有):本地方法栈和Java虚拟机栈类似,只不过Java虚拟机栈是给JVM使用的,而本地方法栈是给本地方法使用的。
· 程序计数器(线程私有):程序计数器是一个比较小的空间,用于记录当前线程执行的行号的,可以将其看作是当前线程所执行的字节码的行号指示器。如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个本地方法,那么这个计数器的值为空。
· 元数据区(线程共享):元数据区以前也叫做方法区,元数据区存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JDK8元空间的内存属于本地内存,这样元空间的大小就不受JVM最大内存的参数影响了,而是与本地内存有关系;JDK8将字符串常量池移动到了堆中。
在一个Java进程中,元数据区和堆是只有一份的(也就是线程共享),这个进程下的所有线程都共享同一份数据,共用同一个内存空间。而栈和程序计数器是可以有多份的,一个进程下的每一个运行的线程都有自己的程序计数器和栈,也就是线程私有;线程就代表一个“执行流”,每个线程都需要保存自己的程序计数器,也需要记录自己的调用关系,因此每个线程都有自己的程序计数器和栈。
以下列代码为例,说明a,b,c,d,e,f分别在JVM中的哪个区域:
public class Demo1 {private int a;private Demo1 b = new Demo1();private static int c;private static Demo1 d = new Demo1();public static void main(String[] args) {int e = 10;Demo1 f = new Demo1();}
}
首先,先说结论,一个变量处于哪个内存区域和变量的形态有关:①局部变量存储在栈中;②成员变量存储在堆中;③静态成员变量存储在元数据区(方法区)中。
根据上述结论可以得到a,b存储在堆中,因为其实成员变量;c,d存储在元数据区中,因为其实静态成员变量;e,f存储在栈中,因为其实局部变量,属于线程私有的。
但是b,d,f中new Demo1()得到的对象是存储在堆中的,b、d、f只是对于这个对象地址的引用,其是引用类型的变量,存储的是一个对象的地址。
JVM类加载
类加载过程
通常我们编写一个.java后缀的java程序文件,通过javac编译得到.class文件,存储在硬盘中,运行java程序时,JVM需要读取.class文件中的内容,并且执行其中的指令。读取.class文件的内容就称之为类加载,这个过程把类涉及到的字节码从硬盘中读取到内存中(元数据区)。每加载一个.class文件就会创建一个类对象,类加载的输入是.class文件,类加载的结果是内存中对应的类对象。
对于一个类来说,它的生命周期是:加载、验证、准备、解析、初始化、使用、卸载,而类加载的过程其实就是加载、验证、准备、解析、初始化。类加载可以说成五步,也就是刚说的这五种;也可以说成是三步,其中的验证、准备、解析可以统称为连接,按三步来说就是加载、连接、初始化。
· 加载:加载是类加载过程的第一个阶段,它首先通过一个类的全限定名来获取定义此类的二进制字节流;然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。简单一点来说,加载过程就是通过类的名称来找到.class文件,然后打开并读取文件的内容。
· 验证:验证是连接阶段的第一步,其主要功能是验证读取到的.class文件的数据是否正确,是否合法。
· 准备:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,这个设置初始值,是将所有的内容都初始化为0.例如下列代码:
class Test {private static int value = 123;
}
这里的初始化并不是将value的值赋值为123,而是将其初始化为0。
· 解析:解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。以下面这段代码为例:
class Test {String s = "hello";
}
· 初始化:初始化阶段,Java虚拟机真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程:①执行父类构造器,如果该类有父类,首先会调用父类的构造器;②初始化实例变量,按照代码中定义的顺序初始化实例变量;③执行构造器代码,执行构造器中的代码。
双亲委派模型
默认类加载器
JVM的双亲委派模型是一种类加载机制,用于确保Java类加载过程的安全性和一致性。它的主要思想是:每个类加载器在加载类时,首先将请求委派给父类加载器,只有当父类加载器无法完成加载时,才由当前类加载器尝试加载类。
JVM默认有三个类加载器,分别是:BootstrapClassLoader、ExtensionLoader、ApplicationClassLoader。
· BootstrapClassLoader:即启动类加载器,负责加载JDK中lib目录中Java的核心类库(例如java.util.*, java.lang.*等)即$JAVA_HOME/lib⽬录。
· ExtensionLoader:即扩展类加载器,负责加载Java扩展库,即位于JAVA_HOME/lib/ext目录下的类库。
· ApplicationClassLoader:即应用程序类加载器,负责加载应用程序类路径(classpath)上的类,也就是加载我们自己写的程序/第三方库中的类。
双亲委派模型的工作流程
1、当前类加载器收到类加载请求:当一个类加载器收到加载类的请求时,它不会立刻尝试加载这个类;
2、将请求委派给父类加载器:当前类加载器首先将加载请求委派给父类加载器;
3、父类加载器处理请求:如果父类加载器存在,则父类加载器会继续将请求向上委派,直到到达启动类加载器。启动类加载器尝试加载类,如果成功,则返回类的引用;
4、父类加载器无法加载类:如果启动类加载器无法加载该类,加载失败返回到子类加载器;
5、当前类加载器尝试加载类:如果父类加载器无法加载该类,则由当前类加载器尝试加载。通过这种机制可以确保核心类库不会被篡改,避免了类的重复加载和类的冲突问题。
其流程图可以大概画为上述这样,在类加载器开始尝试加载后,只要成功加载到类就直接返回类的引用,进入验证、准备、解析、初始化环节。如果整个流程结束后没有完成该类的加载,就会抛出异常ClassNotFoundException。
此处的父类加载不是那种继承的关系,而是在ClassLoder中有一个parent,用于记录当前类加载器的父类加载器。
双亲委派模型的优点/目的
· 安全性:通过将类加载请求逐级向上委派,可以避免核心类库被篡改或替换,确保系统安全,保证标准库的类,被加载的优先级是最高的,扩展其次,第三方库优先级最低。
· 避免类的重复加载:确保了每个类只被加载一次,避免类的重复加载和类冲突问题。
· 提高加载效率:通过委派机制,可以利用已经加载过的类,提高类加载的效率。
如何破坏双亲委派模型
破坏双亲委派模型可以通过自定义加载器、通过反射机制、OSGi框架、使用SpI这几种方式。
· 自定义类加载器:自定义类加载通过继承java.lang.ClassLoader并重写findClass方法实现,其可以指定父类加载器,也可以继承应用程序类加载器。其破坏双亲委派模型的方法也就是在重写findClass方法时可以实现不同于双亲委派模型的类加载机制。
· 通过反射机制:利用反射机制可以直接操作类加载器的父类加载器,绕过双亲委派机制。例如我们可以通过反射机制获取到系统类加载器的父类加载器(也就是扩展类加载器),然后我们可以通过这个反射机制将系统类加载器(应用程序类加载器)的父类加载器换成我们自定义的类加载器,此时就打破了双亲委派模型了。
· OSGi框架:OSGi 框架提供了一种模块化的类加载机制,允许每个模块(Bundle)有自己的类加载器,从而可以打破双亲委派机制。
· 使用SPI(Service Provider Interface):某些服务提供者接口的实现中,可能需要打破双亲委派机制来加载服务实现类。在META-INF/services目录下创建一个文件,文件名为接口的全限定名,文件内容为实现类的全限定名。通过这种方式,JVM 会使用Thread.contextClassLoader来加载服务实现类,从而可以打破双亲委派机制。
垃圾回收机制
对于Java运行时内存的各个区域:程序计数器、虚拟机栈、本地方法栈、堆、元数据区,其中程序计数器、虚拟机栈、本地方法栈这三部分的生命周期与相关线程有关,随线程而生,随线程而灭,并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存自然就跟着县城回收了。而堆和元数据区是所有线程所共享的,但是元数据区主要存储被加载的类信息,但一个程序中要加载的类都是有上限的,因此不会出现无限增长的情况,所以堆也就是GC的主战场,这里主要存储被new的对象。
垃圾回收也就是回收内存,回收不再使用的对象,这里的垃圾也就是不再使用的对象。GC来进行垃圾回收,首先要找出谁是垃圾(不再使用的对象),然后再对其进行回收,释放垃圾的内存空间。
垃圾的判断方法
垃圾的判断方法其实也就是死亡对象的判断方法,需要针对于每个对象分别判定,看谁是垃圾。在Java中使用一个对象,一般都是通过“引用”来使用的,如果一个对象没有引用指向了就可以认为这个对象是垃圾了。常见的垃圾判断方法有引用计数法和可达性分析法。
引用计数法
引用计数法就是给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”,就可以判定该对象为垃圾了。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。例如python、php就采用了引用计数法进行内存管理。但是在JVM并没有采用引用计数法来管理内存,因为计数器的引入带来了额外内存空间的消耗,最主要的原因就是引用计数法无法解决对象的循环引用问题。
public class Test {Test t = null;public static void main(String[] args) {Test a = new Test();Test b = new Test();a.t = b;b.t = a;a=null;b=null;}
}
例如上述代码,创建了对象a,在堆内存分配内存,创建了对象b,在堆内存分配内存,此时对象a的程序计数器记录的次数为1,b也为1。然后令a.t = b,此时对象b的程序计数器记录的次数为2,再令b.t=a,此时对象a的计数器次数也为2了。我们令a=null,b=null,此时a和b的程序计数器记录的次数都为1,而不为0,但是这两个对象我们已将不再使用了。这就是循环依赖问题。
可达性分析法(Java采用)
可达性分析法的核心思想是通过一系列称为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径称之为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
GC Roots是垃圾回收器确定对象是否可达的起始点。在Java中,GC Roots是一组特殊的对象,GC Roots对象保证了这些对象及其引用链不会被垃圾回收器回收,因为它们是程序的起始点,其他对象通过它们间接可达,它确保了内存中的对象能够正确地被管理和清理,避免内存泄漏和无效引用的问题。在Java中,可以作为GC Roots的对象包含以下几种:
1、虚拟机栈(栈帧中的本地变量表)中引用的对象:每个线程都有一个虚拟机栈,栈帧中的本地变量表包含了方法执行过程中用到的所有局部变量,这些局部变量可能包含对对象的引用。
2、方法区中类静态属性引用的对象:方法区中存储了类的元数据,包括类的静态变量,这些静态变量可能引用对象。
3、方法区中常量引用的对象:方法去还包含运行时常量池,其中可能也有对对象的引用。
4、本地方法栈JNI(Native方法)引用的对象:本地方法栈用于本地方法的调用,本地方法可以通过JNI引用Java对象,这些引用也可以是GC Roots。
5、活动线程:所有正在运行的线程本身也是GC Roots。
6、类加载器:类加载器本身也是 GC Roots,因为它们负责加载类,而类加载器的引用链可以追溯到所有被加载的类及其静态变量。
JVM的四种引用
在Java中引用类型的不同决定了垃圾收集器如何处理对象的生命周期。Java提供了四种引用类型,分别是:强引用、软引用、弱引用、虚引用。
· 强引用:强引用是Java中最常见的引用类型。通过new关键字创建的对象引用就是强引用。只要强引用存在,垃圾收集器就永远不会回收被引用的对象。强引用是默认的引用类型。
String str = new String("hello");
str是一个强引用,只要str不被置为null或超出作用域,对象"hello"就永远不会被垃圾收集器回收。
· 软引用:软引用是一种相对较强但仍允许垃圾收集器回收的引用类型,用于描述一些还有用但不是必须的对象。适用于实现内存敏感的缓存。只有在内存不足的情况下,垃圾收集器才会回收被软引用关联的对象。软引用通常用于实现缓存,当内存充足时对象不会被回收,当内存不足时对象会被回收以释放内存。在JDK1.2之后,提供了SoftReference类来实现软引用。
import java.lang.ref.SoftReferenceclass Test {public static void main(String[] args) {String str = new String("hello");SoftReference<String> softRef = new SoftReference<>(str);}
}
· 弱引用:弱引用也是用来描述非必需对象的。但是它的强度弱于软引用。被弱引用的对象只能生存到下一次垃圾回收之前。当垃圾收集器开始进行工作时,无论当前内存是否充足,都会回收掉只被弱引用关联的对象。JDK1.2之后提供了WeakReference类来实现弱引用。
import java.lang.ref.WeakReferenceclass Test {public static void main(String[] args) {String str = new String("hello");WeakReference<String> weakRef = new WeakReference<>(str);}
}
· 虚引用:虚引用是一种最弱的引用类型,它仅用于跟踪对象被垃圾收集器回收的时间。虚引用本身不会决定对象的生命周期,垃圾收集器回收对象时会将虚引用放入引用队列中。虚引用通常用于实现一些特殊的清理机制,例如管理直接内存。
import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueueclass Test {public static void main(String[] args) {String str = new String("hello");ReferenceQueue<String> refQueue = new ReferenceQueue<>();PhantomReference<String> phantomRef = new PhantomReference<>(str);}
}
在上述代码中,phantomRef是一个虚引用,当垃圾收集器回收对象“hello”时,phantomRef会被放入refQueue中。
垃圾回收算法
通过上面介绍的垃圾判断方法就可以将死亡对象标记出来了,标记出来之后就可以进行垃圾回收操作了,常见的垃圾回收算法包括标记-清除算法、复制算法、标记-整理算法、分代收集算法。
标记-清除算法
标记清除算法是最基础的垃圾收集算法。它首先标记所有从GC Roots可达的对象,然后回收所有未被标记的对象的内存空间。简单直接,不需要移动对象,但是标记和清除的效率都不高,而且会产生内存碎片,可能导致大对象分配失败。
复制算法
复制算法主要是为了解决“标记-清除”算法的效率问题和内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次性清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。但是这种算法会导致可用内存变小,变为原来的一半,如果对象存活数量比较大,复制性能会变差。
标记-整理算法
复制收集算法在对象存活率较高时会进行比较多的赋值操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为“标记-整理”算法。标记过程与“标记-清除”算法过程一致,但后续步骤不是直接对未标记对象进行清除,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。由于多了整理这一步骤,因此效率也不是很高,适用于老年代这种垃圾回收频率不是很高的场景。
分代收集算法
分代算法是通过区域划分实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾收集。当前JVM垃圾收集都采用的是“分代收集”算法,这个算法没有新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是将Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高,没有额外空间对他进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
哪些对象会进入新生代,哪些对象会进入老年代:一般新创建的对象都会进入新生代;大对象和经历了N次(一般默认情况是15次)垃圾回收依然存活的对象会从新生代移动到老年代。
Minor GC 和 Full GC 的区别:
· Minor GC是新生代GC,指的是发生在新生代的垃圾收集,因为Java对象大多都具备朝生息灭的特性,因此Minor GC(采用复制算法)非常频繁,一般速度也比较快。
· Full GC是老年代GC或者Major GC,指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢十倍以上。
垃圾收集器
垃圾收集器是为了保证程序能够正常运行、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
Serial和Serial Old收集器
Serial是JVM最早一批收集器之一,它是一款单线程收集器。在进行垃圾收集时,需要暂停所有的用户线程。Serial和Serial Old收集器都是这种单线程的,但是Serial是新生代收集器,采用复制算法进行垃圾收集。Serial Old是Serial的老年代版本,是老年代收集器,采用标记-整理法进行垃圾收集。
Serial系列收集器的优点是简单高效、资源消耗最少(因为其是单线程方式)、单线程收集。其工作流程为:
· 暂停用户线程:在开始垃圾收集过程之前,Serial垃圾收集器会暂停所有的用户线程,这是为了确保在垃圾收集过程中对象的状态不会被修改,从而保证垃圾收集的准确性。
· 执行垃圾收集:一旦用户线程暂停,Serial垃圾收集器会开启一个单线程来执行垃圾回收操作,这个线程会遍历堆中的对象,标记并清理不再使用的对象,以释放内存空间。
· 等待垃圾收集完成:在垃圾收集过程中,用户线程会被暂停,直到垃圾收集完毕。这意味着用户线程无法在垃圾收集期间执行任何操作。
当垃圾回收完后,Serial垃圾收集器会恢复用户线程的执行。此时,垃圾已清理,堆内存中有更多的空间供应用程序使用。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多个线程进⾏垃圾收集之外,其余⾏为包括Serial收集器可⽤的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全⼀样,在实现上,这两种收集器也共⽤了相当多的代码。ParNew收集器的目标是尽可能缩短垃圾收集时用户线程的停顿时间。
ParNew收集器的一些特点与CMS配合的优势:
①与CMS配合:ParNew垃圾收集器能够与CMS垃圾收集器配合使用,用于处理老年代的垃圾回收。在这种组合中,parNew负责新生代的垃圾收集,而CMS负责老年代的并发垃圾收集,这种分工合作可以有效地减少应用程序的停顿时间,满足对低停顿时间的要求。
②并行收集:ParNew垃圾回收器采用多线程并行收集的方式,类似于Parallel Scavenge收集器。它能够充分利用多核CPU的优势,加块垃圾收集的速度,提高整个程序的性能。
③应对停顿时间要求高的场景:由于ParNew与CMS配合使用,可以针对那些对停顿时间要求较高的应用场景。CMS收集器通过并发执行垃圾回收操作,尽可能减少停顿时间,而ParNew则能够在新生代中高效的执行垃圾回收操作,进一步降低停顿时间。
ParNew采用复制算法进行垃圾收集,是一款新生代的并行收集器,其工作流程为:
· 标记阶段:在垃圾收集时,ParNew收集器会暂停所有的应用线程,然后开始标记所有存活的对象。标记阶段的主要任务是识别哪些对象是存活的,并将它们标出来。
· 复制阶段:在标记阶段完成后,ParNew收集器会将所有的存活对象从Eden区和一个Survicor区复制到另一个Survivor区。如果目标Survior区没有足够的框架容纳所有的存活对象,存活对象将被移到老年代。
· 清理阶段:复制阶段完成后,Eden区和之前Survivor区中的所有对象都被认为是垃圾,并且这些区域的内存可以被清理和重用。
· 应用程序恢复:在清理阶段完成后,应用程序线程将被重新启动,继续执行。
Parallel Scavenge 和 Parallel Old收集器
与ParNew类似,也是一款用于新生代的多线程收集器。但Parallel Scavenge的目标是达到一个可控制的吞吐量,而ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间。
Parallel 收集器的核心特点包括:
· 多线程并行执行:Parallel 收集器利用了多核 CPU 的优势,通过多个线程同时执行垃圾回收操作,加快了垃圾收集的速度。
· 高吞吐量:由于并行执行垃圾收集操作,Parallel 收集器适用于吞吐量要求较高的应用场景。它能够在保证吞吐量的同时,尽可能地减少垃圾收集的停顿时间。
· 适用于大内存堆:随着内存空间的扩大,Parallel 收集器能够更好地应对大内存堆的情况,通过并行执行垃圾收集操作,提高了整个垃圾收集过程的效率。
相比于传统的 Serial 收集器,Parallel 收集器能更好地适应现代应用的需求,特别是大型内存堆和高吞吐量的场景。Parallel Scavenge是一款采用复制算法进行垃圾收集的新生代收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,也是一款多线程的收集器,采用标记整理法进行垃圾收集。其工作流程为:
· 多线程并行执行:Parallel Scavenge 收集器利用了多个线程并行执行新生代的垃圾回收操作。这意味着在进行新生代垃圾回收时,多个线程同时工作,加快了垃圾收集的速度。
· 暂停用户线程:与 Serial 收集器类似,Parallel Scavenge 在进行垃圾收集时会暂停用户线程,以确保垃圾回收的准确性。这一阶段通常称为“Stop the World”。
· 多线程并发清理:Parallel Scavenge 收集器的特点之一是在新生代垃圾收集过程中采用并发清理的方式。这意味着在暂停用户线程期间,多个线程同时清理新生代中的垃圾对象,从而更快地完成垃圾收集过程。
CMS收集器(老年代)
CMS 垃圾收集器的设计初衷是允许垃圾收集器在进行垃圾回收的同时,与应用程序的线程并发执行,不需要长时间暂停应用程序线程。它的工作原理是通过并发标记和清除的方式,先标记所有的存活对象,然后清除未被标记的对象。允许在垃圾收集过程中与应用程序并发执行,从而降低了垃圾收集的停顿时间,提高了系统的响应性和用户体验。
CMS 垃圾收集器主要针对老年代进行垃圾回收,对于新生代则通常使用 ParNew 收集器。这种分代收集的方式能够更好地适应不同内存区域的特点和垃圾回收需求。
CMS使用标记-清除算法来实现的,它的工作过程可以分为以下四步:
· 初始标记:短暂停顿,仅仅只是标记一下GC Roots能直接关联到的对象。这个阶段需要暂停用户线程,因为要确保标记的准确性。
· 并发标记:在这个阶段,CMS 垃圾收集器会与用户线程并发执行,对整个堆进行标记。垃圾回收线程会在后台标记所有存活对象,而用户线程可以继续执行,不受影响。
· 重新标记:在并发标记阶段结束后,CMS 垃圾收集器会进行一次重新标记,来处理在并发标记阶段发生变化的对象。这个阶段需要暂停用户线程,以确保标记的准确性。
· 并发清理:在重新标记完成后,CMS 垃圾收集器会与用户线程并发执行,清理未标记的对象。垃圾回收线程会在后台清理不再使用的对象,而用户线程可以继续执行,不受影响。
通过将垃圾回收过程分为多个阶段,并在其中允许用户线程和垃圾回收线程并发执行,CMS 垃圾收集器成功地减少了用户线程的停顿时间。这种创新的并发垃圾收集策略提高了系统的响应性和用户体验,确保了应用程序的顺畅运行。
G1收集器
CMS 垃圾收集器开创了垃圾收集器的一个新时代,实现了垃圾收集和用户线程同时执行,从而达到了垃圾收集的过程不停止用户线程的目标。这种并发垃圾收集的思路为后续垃圾收集器的发展提供了重要的参考。
G1 垃圾收集器摒弃了传统的物理分区方式,将整个内存分成若干个大小不同的 Region 区域。每个 Region 在逻辑上组合成各个分代,这样做的好处是可以以 Region 为单位进行更细粒度的垃圾回收。G1 垃圾收集器在进行垃圾回收时,可以针对单个或多个 Region 进行回收,从而提高了收集效率和性能。G1 垃圾收集器吸取了 CMS 垃圾收集器的优良思路并通过摒弃物理分区、采用 Region 分区的方式,实现了更细粒度的垃圾回收,从而提高了整个系统的性能和可用性。 G1 垃圾收集器在大内存环境下的表现更加出色,成为了现代 Java 应用中的重要选择。
G1是把整个内存分成了很多块,不同的颜色(字母)表示这一块是新生代(伊甸区/幸存区)、老年代。进行GC的时候不要求一个周期就把所有的内存都回收一遍,而是一轮GC只回收一部分就好,降低STW的影响。
G1 垃圾收集器的回收流程与 CMS 的逻辑大致相同,包括初始标记、并发标记、重新标记和筛选清除等阶段。但是,与 CMS 不同的是,G1 在最后一个阶段不会直接进行整体的清除。相反,它会根据用户设置的停顿时间进行智能的筛选和局部的回收。其工作流程为:
· 初始标记:短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象
· 并发标记:与用户线程并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。
· 重新标记:短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。
· 根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。
通过这种智能的筛选和局部回收方式,G1 垃圾收集器能够更好地平衡垃圾回收的效率和停顿时间,从而提高系统的响应性和用户体验。G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
总结
针对垃圾收集器的总结:
· 新老代:Serial收集器、ParNew收集器、Parallel Scavenge收集器是新生代垃圾收集器;Serial Old收集器、CMS收集器、Parallel Old收集器是老年代垃圾回收器;而G1是全区域的垃圾回收器。
· 按照使用垃圾回收算法区分:
标记-清除:CMS收集器
标记-复制:Serial收集器、ParNew收集器、Parallel Scavenge收集器
标记-整理:Serial Old收集器、Parallel Old收集器
G1收集器从全局上看是标记-整理,从局部看是标记-复制