6、JVM中的GC

Java和C++等语言最大的技术区别就是自动化的垃圾回收机制,也就是常说的GC,GC是对内存进行清理、回收

GC一般发生在堆中,同时方法区/元空间这一块也会发生垃圾回收,不过这块的效率比较低,而栈中内存会随着线程的灭亡而释放,不关注

堆中的对象通过引用计数算法可达性分析来判断是否存活、是否可以进行GC

GC中常见的垃圾回收算法有复制算法标记-清除算法标记-整理算法

JVM中常用的垃圾收集器有很多种:Serial/Serial OldParallel Scavenge (ParallerGC )/Parallel OldParNeWConcurrent Mark Sweep (CMS)Garbage First(G1)等,虽然种类繁多,但大都遵循分代收集的理论

程序运行时创建的对象一般分为两种,大部分是朝生夕死的对象和可以熬过多次回收的对象,一般在内存中朝生夕死的对象放在一个区域,熬过多次回收的对象放在一个区域,这就是常说的新生代和老年代,对新生代进行回收的叫做新生代回收,对老年代进行回收叫做老年代回收,对队和方法区/元空间进行回收叫做整堆回收(Full GC)

1、判断对象存活

判断对象是否存活的方式有两种,引用计数法和可达性分析

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1,这种方法Python中使用,JVM中没有使用

可达性分析

通过以GC Roots对象为起点,向下搜索,看是否存在引用,搜索走过的路被称为引用链,当一个对象到GC Roots没有任何引用链,说明此对象是无用可被回收的

GC Roots对象: 虚拟机栈(栈帧中的本地变量表)中引用的对象

                       各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等

                       方法区中类静态属性引用的对象

                       java 类的引用类型静态变量

                       方法区中常量引用的对象,比如:字符串常量池里的引用

                       本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

                       JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)

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

                       JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

                       JVM 实现中的“临时性”对象,跨代引用的对象

即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它将被第一次标记,随后进行一次筛选(如果对象覆盖了 finalize(),我们可以在 finalize()中去拯救),若此次筛选未通过再一次进行标记,两次标记的对象才会被GC

2、垃圾回收算法

复制算法

将内存划分为相等大小的两块,每次只是用其中的一块,当这块用完之后,将还存活的对象复制到另一块上,然后把已使用的那一块内存空间一次清除

这样每次只对半块区域进行内存回收,内存分配时不用考虑内存碎片的情况,按顺序分配就可以,实现简单、运行高效,但是可使用的内存只能有一半,常用于新生代回收,因为新生代中对象都属于朝生夕死的、存活时间短,需要复制的对象少、效率高

Appel式回收

更加优化的复制算法,分配一块较大的 Eden 区和两块较小的 Survivor 空间(也可以叫做做 From 或者 To,也可以叫做 Survivor1 和Survivor2),新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)

标记-清除算法

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代

标记-整理算法

先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新( 直接指针需要调整)

3、垃圾回收器

Serial/Serial Old

最初的垃圾回收器,独占、单线程、适合单CPU,一般用在客户端,适合于小于200左右的堆空间进行垃圾回收

单线程垃圾回收时需要暂停所有的工作线程,直到垃圾回收停止,这叫做STW

Parallel Scavenge (ParallerGC )/Parallel Old

多线程的、关注吞吐量的垃圾回收器,适用于后台运算没有太多交互的场景

ParNeW

多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。和 Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少。(在 JDK9 以后,把 ParNew 合并到了 CMS 了)

Concurrent Mark Sweep (CMS)

第一个多线程垃圾回收器,基于“标记—清除”算法实现的,工作过程分为4步:

初始标记,仅仅标记一下GC roots能直接关联到的对象,速度快

并发标记,和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)

重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短

并发清除,整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作

CMS 收集器的内存回收过程是与用户线程一起并发执行的,但是因为并发处理时工作线程还在运行,所以会不时的出现新的垃圾,CMS无法进行回收,只能下一次再来回收,这样的垃圾叫做浮动垃圾,需要浮动垃圾预留出一定的内存空间,同时因为CMS采用的事标记-清除算法,会产生不连续的空间碎片,还会有对象的挪动,这个操作也会浪费时间,降低效率

Garbage First(G1)

为了解决STW问题而出现的,打破了分代收集的概念,G1垃圾收集器将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果

Region可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象

4、方法区/元空间回收

 方法区/元空间的回收效率很低,主要用来回收废弃的常量和无用的类,类很难进行回收,需要满足以下条件才可以进行回收:

类的实例都进行了回收,虚拟机堆中不存在这个类的所有实例

加载这个类的classLoader都被回收

这个类对应的Class对象没有任何引用,并且这个类的方法无法在任何地方通过反射访问到

posted @ 2020-11-11 18:51  CarBlack  阅读(129)  评论(0编辑  收藏  举报