GC(垃圾收集)
1、GC 是什么? 为什么要有 GC?
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,但Java 语言并没有提供释放已分配内存的操作方法。
2、 Java 垃圾回收机制
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,在虚拟机空闲或者当前堆内存不足时,就会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。(也可以通过代码System.gc()或者Runtime.gc(),通知 GC 运行,但执行时间与目标并不保证)。
3、如何判断一个对象是否存活?
判断一个对象是否存活有两种方法:
3.1、 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器自增加一,一个引用失效时,计数器就自减一。在GC扫描时判断该对象的引用计数器是否为零,如果为零就说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。
但是引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A、B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
3.2、可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 Java 中可以作为 GC Roots 的对象有以下几种:
• 虚拟机栈中引用的对象
• 方法区类静态属性引用的对象
• 方法区常量池引用的对象
• 本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象并不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize() 方法。当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没必要的。 如果该对象有必要执行finalize() 方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize() 执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除” 即将回收”集合,等待回收。
4、垃圾回收算法
- Mark-Sweep 标记清除算法:空间不连续,产生空间碎片。
- Copying 复制算法 : 没有碎片,浪费空间。
- Mark-Compact 标记整理算法:没有碎片,效率偏低。
4.1、Mark-Sweep (MS)
- 扫描整个堆,标记存活的对象;
- 执行 sweep 操作回收未被标记的对象。
4.2、Copying
复制算法会先将堆分成A,B两份,而真正使用的是一半的堆内存
- 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象复制到B块。
- 清理A块所有对象。
- 新生对象被分配到B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象复制到A块。
- 清理B块所有对象。
- 如此重复。
4.3、Mark-Compact (MC)
- 扫描整个堆,标记存活的对象;
- 移动存活对象,同时更新存活对象中所有指向被移动对象的指针。
4.4、分代收集算法
前面三种算法各有优缺点
- 把堆内存分为新生代以及年老代,并对不同代的区域使用不同的收集算法,以提高回收效率。
- 年轻代使用复制算法;老年代使用标记-清除算法或者标记-整理算法,或者两者结合算法。
4.5、增量收集算法
同样是基于前面三种算法的合并使用。
因为每次垃圾回收时,程序都会处于一种Stop the World的状态,这种状态下,应用程序会被挂起,暂停一切正常的工作。这样一来,将严重影响用户体验或者系统稳定性。
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替进行。每次,垃圾回收线程只收集一小片区域的内存空间,接着切换到用户线程继续执行。依次反复,直到垃圾收集完成。
增量收集算法就是通过处理线程间的冲突,允许垃圾收集线程以分阶段的方式完成标记、复制以及清除任务。
- 这种分阶段的方式意味着线程中断,虽然看似减少了应用程序停顿的时间;但是总体上线程切换和上下文切换的次数增多,使得总体消耗增大,造成系统吞吐率的下降。
4.6、分区收集算法
一般来说,在相同的条件下,堆的空间越大,在一次GC任务时需要消耗的时间就越长,对应用线程的中断就越长。为了更好的控制每次GC的时间,将一块大的堆内存分割成多个小块,进行独立回收小块内存,从而减少每次GC的时间,同样存在线程切换和上下文切换的次数增多的问题。
5、垃圾回收器
基于垃圾回收机制以及判断一个对象是否存活的概念,前辈们开创出并迭代出很多种垃圾回收器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:CMS、Serial Old、Parallel Old
- 整堆收集器: G1、ZGC
有线连接表示可搭配使用
分代思想是在JDK1.8以及以前的版本使用,后面出现的分区思想:JDK9出G1,JDK11出ZGC,将随着版本迭代会设计出更优的回收器。
从多线程角度观察又分为:
串行收集: Serial、Serial Old 指只有一条垃圾收集线程串行工作,在垃圾收集时所有线程必须阻塞等待。
**并行收集:**ParNew、Parallel Scavenge、Parallel Old 指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集: CMS 、G1、ZGC 、Shenandoah 指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
吞吐量: 即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%