JVM垃圾回收机制

一、回收堆区

垃圾回收器在堆进行垃圾回收前,首先要判断这些对象那些还存活,那些已经“死去”。判断对象是否已“死”有如下几种算法:

1.引用计数法

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;

当引用失效时,计数器就-1;

任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个比较好的算法。

但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题。

2. 可达性分析算法

在上面讲了,Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活。

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。以下图为例:

在Java语言中,可作为GC Roots的对象包含以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中(Native方法)引用的对象

引用

JDK1.2以前,Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。

JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

1.强引用

类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。

当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用

如果一个对象只具有软引用,那就类似于可有可无的生活用品。

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

软引用可用来实现内存敏感的高速缓存。

举例:查看网页,可能后退,那么刚才的网页要不要一直存储,一直存储就是强引用,不存储就是回收,那么折中一下,于是产生了弱引用,当内存空间足够时,就不回收,不足时,再回收。这个根据内存敏感程度而变化而决定是否缓存,就是内存敏感的高速缓存。

3.弱引用

对象拥有更短暂的生命周期。

在gc线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否充足,都会回收它的内存。

由于gc是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

4.虚引用

就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

作用: 虚引用主要用来跟踪对象被垃圾回收的活动

区别: 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

3.真正判决

要宣告一个对象的真正死亡,至少要经历两次标记过程

如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。

​ 没必要:没有覆盖finalize()方法finalize()方法已经被调用过一次了

审判

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它

如果对象在finalize()中成功拯救自己:与引用链上的任何一个对象建立起关联关系

那在第二次标记时它将会被移除出"即将回收"的集合,也就暂时逃脱死亡的命运了。

如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

二、回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量无用类。

废弃常量:

没有任何一个String对象引用常量池中的"abc"常量,也没有其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。

常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

无用类

1.该类的所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
2.加载该类的ClassLoader已被回收
3.该类对应的Class对象没有任何其他地方被引用,无法在任何地方通过反射访问该类的方法

三、垃圾回收算法

1.标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

“标记-清除”算法的不足主要有两个

  1. 效率问题:标记和清除这两个过程的效率都不高
  2. 空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

2.复制算法(新生代回收算法)

它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。

当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。

因为复制过去后,另一边的内存肯定是连续的了,此时再把使用过得内存区域清理,从而达到了整理的效果。

也就是伊甸园区移动到survive0和survive1区的算法。

但是,伊甸园区的对象都是朝生夕死的,所以并不需要1:1的空间,所以出现了8:1:1的默认比例

3.标记整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。

而是采用标记整理算法

标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉除存活对象以外的内存。流程图如下:

4.分代收集算法

就是将堆区分开,不同的位置采用不同的算法

新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;

老年代中对象存活率高,就必须采用"标记-清理"或者"标记-整理"算法。

四 .Minor GC、Major GC、Full GC的区别?

Minor GC 又称为新生代GC 指的是发生在新生代的垃圾回收操作(包括Eden区和Survivor区)。

当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫做Minor GC。

Major GC通常是跟full GC是等价的,收集整个GC堆。

但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了

Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。

针对HotSpot VM GC来看

它里面的GC其实准确分类只有两大种:

Partial GC:并不收集整个GC堆的模式

  • Young GC:只收集年轻代的GC
  • Old GC:只收集老年代的GC。只有CMS的concurrent collection是这个模式
  • Mixed GC:收集整个年轻代以及老年代的GC。只有G1有这个模式

Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

posted @ 2021-11-11 21:42  橡皮筋儿  阅读(405)  评论(0编辑  收藏  举报