Java 垃圾回收
什么是垃圾回收
简述
简单来说,寻找和释放垃圾占用的内存空间的过程被称为垃圾回收,简称gc。(Garbage Gollection)
在Java体系内,垃圾回收算法和垃圾回收器是并存的,是一体的。
但其实垃圾回收算法的历史要比Java久远,刚开始的出现也并非应用在JVM上。
作为Java程序员,我们需要明白,垃圾回收给我们的编码带来了一些好处,同时有好处就有坏处。
优点:
- 我们日常编码中,几乎可以不考虑内存管理。
- jvm可以有效的防止内存泄漏,有效的利用可使用的内存。
缺点:
jvm对内存的管理,程序员几乎并不参与,降低了我们接续内存相关问题的能力。
垃圾回收算法
设想,现实世界中我们是一位街道垃圾清理工,任务是把一条街打扫干净,首先需要把垃圾先找出来堆在路边,一段路可能一个小垃圾堆,最后垃圾车过来一堆一堆的收走…
垃圾回收算法就其实就是定义了几种垃圾收集的办法。
标记-清除算法(Mark-Sweep)
在Java中,垃圾收集器的一种常见算法是标记清除算法(Mark and Sweep),该算法的核心思想是,首先标记出所有仍然在使用中的对象,在标记完成后,在清除掉未被标记的对象。
- 在执行标记阶段,垃圾收集器会从GC ROOT 根节点开始遍历对象,将所有可达的对象进行标记,这些节点可以是程序中的静态变量、本地方法栈中的引用等。
- 在标记完成后,垃圾收集器会扫描整个堆内存,将未被标记的对象进行回收。由于标记和清除过程需要暂停应用程序的执行,因为在现实中通常使用了并发标记清除算法来减少暂停时间。
特点 算法简单,但会产生内存碎片,其中一个主要问题是,清除操作之后会产生大量的不连续空间,这可能导致堆碎片严重,影响程序性能。
此外,由于该算法无法处理循环引用的情况,因为在处理包含循环引用的数据结构时,需要使用其他垃圾回收算法来处理这种情况 。
复制算法(Copying)
- 将内存划分为两个或多个相等的部分,只使用其中一部分来分配对象。
- 当这部分内存填满时,进行垃圾回收,将存活的对象复制到另一部分内存中,然后清除已使用的部分。
- 不存产生内存碎片,但会浪费一半的内存空间。
标记-压缩(Mark-Compact)或标记-整理(Mark-Sweep-compact)算法
- 类似于标记-清除算法,但在标记阶段之后,会对存活的对象进行压缩或整理,将其移动到一起,从而消除内存碎片。
分代收集算法(Generational Collection)
- 基于对象生命周期的观察,将内存划分为年轻代和老年代。
- 年轻代中的对象通常具有较短的生命周期,采用效率较高的垃圾回收策略,如复制算法。(年轻代被划分为Eden空间,Survivor0、survivor1空间三个区域)
- 老年代中的对象声明周期较长,采用标记清除 或 标记-压缩 算法。
增量收集算法(Incremental Collection)
- 将垃圾回收过程分成多个小步进行,每次只执行一小部分回收工作。
- 这样可以在一定程度上减少应用程序的暂停时间
并行收集算法(Parallel Collection)
- 利用多核处理器的优势,使用多个线程同时进行垃圾回收。
- 可以提高垃圾回收的效率,但可能会增加系统的总体负载。
并发标记扫描(Concurrent Mark and Sweep,CMS)算法
- 在应用程序运行的同时进行大部分的标记和清理工作。
- 目标是减少垃圾回收导致的暂停时间。
Garbage First(G1) 算法
- 同时作用于年轻代和老年代
- 将堆划分为多个区域,并优先回收垃圾最多的区域
- 使用了标记-压缩的变种,同时兼顾低延迟和高吞吐量。
在实际的Java垃圾回收实现中,这些算法常常结合使用或者有所改进,以适应不同的场景和性能需求。例如:HotSpot JVM中的不同垃圾回收器(如Serial GC、Parallel GC、CMS、G1、ZGC和Shenandoah)就采用了以上算法的不同组合和优化。
ZGC
垃圾回收器
扩展
可达性分析
在堆内存中,存在一个根对象GC Root,GC Root 对象一般是如下几种情况,
- 线程栈 中的 栈帧 中的 局部变量表 中的引用对象。
- 方法区 种的 静态引用对象;
- 方法区 中的 常量引用对象。
- 本地方法栈 中的 JNI中的 引用的对象。
根对象GC Root 指向了对象1; 对象1又指向了对象2,对象3;整个链条称为引用链。
- 只要是处于整个链条上的对象,都是非垃圾对象,不能进行垃圾回收。
- 不处于 引用链条 上的对象,就是垃圾对象。
GC回收前的两次标记
当对象被定义为 垃圾对象后,并不会马上被回收,相当于只是判了死缓,没有真正执行垃圾回收。
当GC ROOT 引用链断开后,对象不可达。
-
JVM会对 不可达对象 进行一次标记,然后执行一次筛选,执行对象的finalize方法。
finalize 方法是对象被 GC 垃圾回收之前 , 被调用的方法 , 该方法不能保证一定能执行完毕 , JVM 会给对象一个时间限制 , 在这个时间内执行 finalize 方法 , 重写的该方法中不要执行很耗时的操作 ; -
之后 JVM 会对不可达对象 进行第二次标记 , 此时如果发现 该对象 仍然是垃圾对象 , 此时直接将该对象回收 ;
finalize 方法只会被调用一次 , JVM 对 对象第二次标记时 , 发现对象如果没有被引用 , 直接回收 , 不再调用 finalize 方法 ;
gc的种类
- Minor GC,从年轻代回收内存(所有的minor gc 都会触发全世界的暂停,停止应用程序的线程,不过这个过程很短暂,Stop the World)
- Major GC, 清理老年代(Major gc 通常伴随着一次Minor gc)
- FULL GC : 清理整个堆空间,包括年轻代和老年代
gc触发
gc触发的条件有2种,
- 程序调用System.gc()时可以出发
- 系统自身来决定gc触发的时机。
gc过程
- 我们创建的对象会优先在eden区分配,如果是大对象(很长的字符串数组)、长期存活的对象则可以直接进入老年代。
- 当Eden区填满时,需要回收,回收时现将eden区存活对象复制到一个survivor0,然后清空eden区;当Eden区再次填满时,则将Eden区 和 S0 的存活对象复制到S1区,然后清空Eden和S0,如此反复。
- 当对象在Survivor区躲过一次Minor gc后,其年龄就会+1,只有经历15次,默认情况下年龄能到达15的对象会被移到老年代中。