JVM垃圾回收算法及收集器

1,071 阅读11分钟

类加载器

类加载器ClassLoader负责加载class文件,且其只负责class文件加载,至于它是否可以运行则由Execution Engine决定。

启动类加载器:该类加载器负责加载放在<JAVA_HOME>\lib目录,或者是被-Xbootclasspath参数所指定的路径中存放的,而且是java虚拟机能够设别的类库加载到虚拟机的内存中。

扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中或者是被java.ext.dirs系统变量所指定的路径中所有的类库。

应用程序类加载器:负责加载用户类路径(ClassParh)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,这个就是程序中默认的类加载器。

自定义加载器:当我们需要防止java代码被反编译或者是从非标准的来源加载代码(比如字节码放在数据库)就需要用到自定义类加载器。 用户可以继承ClassLoader类,重新findClass方法可以自定义类加载器。

双亲委派机制

如果一个类加载器收到了类加载的请求,它首先不会尝试自己去加载这个类,而是把这个请求委任给父类加载器去加载,因此所有的类加载应该都是从顶层开始加载,只有父类加载器反馈无法完成类加载请求时,子加载器才会尝试去自己加载。

JVM内存区域

堆(线程共享)

  • java堆是java虚拟机所管理的内存中最大的一块内存区域,此内存区域的唯一目的就是存放对象的实例。所以堆也是垃圾收集器管理的主要区域。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。堆也可以细分为新生代和老年代,其中新生代又分为Eden空间、From Survivor空间、To Survivor空间等。

方法区(线程共享)

  • 存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

java虚拟机栈(线程私有)

  • 每个方法被执行的时候,java虚拟机都会同步创建一个栈用于存储局部变量、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量存放了编译期可知的各种java虚拟机基本数据类型、对象引用和returnAddress类型。

本地方法栈(线程私有)

  • 用native修饰的方法就是本地方法,这些方法都会放到一个叫做本地方法栈的地方。

程序计数器(线程私有)

  • 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

怎么判断对象是否可以回收

引用计数算法

在对象中添加一个计数器,每当一个地方引用的时候,计数器的值就加一,当引用失效的时候,计数器的值就减一,当计数器为零的时候说明对象是不再被使用了,可以被回收掉。但是这个方案有一个缺陷就是无法解决循环引用的问提(比如A引用了B,然后B又引用了A),这种情况会造成垃圾无法回收。

可达性分析算法

该算法的基本思路:从GC Roots的根对象作为起始点,从起始点开始,根据引用关系向下搜索,如果某个对象到GCRoots间没有任何的引用链,那么可以证明这个对象是不在被使用的。

可以作为GC Roots的对象包含:

  • 在虚拟机栈中引用的对象,比如各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量等。

  • 在方法区中类静态属性引用的对象,比如引用静态变量。

  • 在方法区中常量引用的对象,比如字符串常量池中的引用。

  • 在本地方法栈中JNI(Native方法)引用的对象。

  • java虚拟机内部的引用,比如基本数据类型对应的Class对象,一些常驻的异常对象以及系统加载器。

  • 所有被同步锁(synchronized关键字)持有的对象

  • 反应java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

JVM垃圾回收算法

标记-清除算法

该算法的两个步骤分别为“标记”和“清除”,标记过程就是对象判断是否属于垃圾的一个判定过程,标记完成后统一进行清理。

缺点:执行效率不稳定,如果堆中包含大量对象,这个时间就需要很多的标记和清楚的操作,导致对象越多其执行效率会越低。

标记清除后会产生大量不连续的内存空间的碎片,当空间碎片太多会导致程序运行的时候分配大对象的时候找不到满足条件的连续空间而不得不再提前触发一个垃圾收集动作。

复制算法

它将可用的内存按照容量划分为两个相等的两个区域,每次使用其中一块,当空间用完了后就将其还存活的对象复制到另外一个区域当中,然后把使用过后的空间一次性清理掉。如果内存中的大多数对象是存活的话,那么会消耗大量的时间在内存间的复制,所以这种算法适用于少数存活对象的区域。这种方式分配内存的时候不用考虑有空间碎片的内存情况,只要移动堆顶指针。按照顺序分配即可。

缺点:复制算法将可用的内存空间缩小了一半。

标记-整理

标记整理算法就将标记的可存活对象向内存空间的一端移动,然后直接清理掉边界外的内存。

分代收集算法

根据对象的生存周期,根据新生代和老年代对象不同的特性选择不同的垃圾回收算法,比如新生代采用复制算法,老年代使用标记清除或者标记整理算法。

收集器类型

Serial收集器

是最基本、发展历史最悠久的收集器,一个单线程的收集器。在进行垃圾收集时必须暂停其他所有的工作线程,直到收集结束为止。

                                       

ParNew收集器

其实就是Serial收集器的多线程并行版本。

                          

Parallel Scavenge收集器

新生代收集器,使用的也是复制算法的收集器,又是并行的多线程收集器,此收集器的目标是达到一个可控制的吞吐量。所谓的吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值,例如虚拟机总共运行了100分钟,其中垃圾收集花掉了1分钟,那么吞吐量就是99%。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Serial Old收集器

Serial收集器的老年代版本,也同样是一个单线程收集器,使用的是标记-整理算法。

Parallel Old收集器

Parallel Old收集器的老年版本,使用多线程和标记-整理算法。

CMS收集器

CMS是一个老年代收集器,是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基

于标记-清除算法实现的,它从总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。但是需要注意CMS不能和Parallel Scavenge收集器配合工作。

CMS收集器的流程分为以下四步:

初始标记:该阶段仅仅只标记以下GC Roots能直接关联的对象,时间非常短。

并发标记:该阶段从GC Roots的直接关联对象开始遍历整个对象图的过程,这个是和用户线程并发运行的。

重新标记:该阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的时间通常是大于初始标记而小于并发标记。

并发清除:该阶段清理删除掉被标记的已经判断死亡的对象,这个阶段也是和用户线程并发进行的。

CMS是一款非常优秀的垃圾收集器,但是还是会有下面所示的三个缺点:

  • CMS收集器对CPU资源非常敏感;

  • CMS收集器无法处理浮动垃圾;

  • CMS是基于标记-清除算法实现的收集器,会产生大量空间碎片,不过CMS收集器提供了一个参数-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,但是JDK开始废弃掉),用于CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,虽然上述方式解决了碎片的问题,但是停顿时间会变长,所以设计者又提供了一个参数-XX:CMSFullGCsBefore-Compaction(也是jdk9开始废弃),这个参数的作用是要求CMS收集器在进行若干次不整理空间的Full GC后,下次进入Full GC前会先进行碎片整理(默认是0,表示每次进入Full GC的时候都会进行碎片的整理)。

G1收集器

虽然G1也仍是遵循分代理论设计的,但是G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的堆划分为多个大小相等的独立区域,每个区域都可以根据需要扮演Eden区、Survivor区或者是老年代空间的角色,收集器能根据不同的角色的区域采用不同的策略处理。

Region中还有一类特殊的Humongous区域,专门用来存储大文件,G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,每个Region的大小可以通过-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,而对于那些大对象,将会被存在N个连续的HumongousRegion中。G1的大多数行为都把Humongous Region作为老年代的一部分进行看待。

G1垃圾收集器运行过程

初始标记:仅仅只是标记一个GC Roots能直接关联到的对象,这个阶段需要停掉线程,但是这个时间非常短,而且是借用进行Minor GC的时候同步完成的,所以这个阶段并没有额外的停顿。

并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段比较耗时,但是由于其是并发的,可以与用户程序同时进行而无需停顿。

最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录

筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户的期望停顿时间来定制回收计划,可以自由选择任意多个Region构成收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。这里涉及到存活对象的移动,是必须暂停用户线程,由多条收集器线程并行执行的。

可以作为了解的垃圾收集器

Shenandoah收集器:并发标记、并发回收、并发引用更新。

ZGC收集器:并发标记、并发预备重分配、并发重分配、并发重映射。

本章知识点主要参考《深入理解JVM虚拟机》

如果觉得本篇文章对你有帮助的请给作者点个关注每周都会有干货输出哦!