欲穷千里目之JVM (二)

193 阅读8分钟

  上一篇文章中,对Java虚拟机运行时区结构有了初步了解,本文将垃圾收集器进行解读,主要知识来源于《深入理解Java虚拟机》同时我也会用一些例子来加深自己的理解。从本文开始,将会引入一些例子来对知识点进行类比。

  垃圾回收开始之初就是要知道什么是垃圾,怎么定义垃圾?垃圾要分类,在Java世界也不例外!不能被任何途径使用的对象就是已经死去的对象。

引用计数法

为对象添加一个引用计数器,每当一个地方使用到该对象,引用计数器加1;当引用失效,引用计数器就减1,当引用计数器为0时表示,该对象已经没有任何地方引用。

这就是引用计数法的定义,这是非常简单的,这时候就会产生一个问题,当两个对象相互引用,除此之外没有任何别的引用,这两个对象实际上已经没有价值是应该被判断为垃圾。但是因为相互引用,计数器不为0,不能被回收。举个例子,当发生凶案时,怎么证明自己是没有嫌疑的,当然有人证明自己有不在场,那么自己的嫌疑是不是很容易洗脱,但是如果你的人证是对方的人证怎么证明不是你们两合谋作案。为了解决这一个弊端,现在主流实现判定对象死亡的方法是可达性分析。

可达性分析

通过系列称为“GC Roots“的对象为起点,从这个起点开始往下搜索,搜索走过的路径称为”引用链“,当一个对象到“GC Roots“没有任何引用链时,说明该对象是不可用。

“GC Roots“对象

虚拟机栈(前文中有相关的知识)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象

一个对象只有引用与被引用两种状态,对于一些可有可无的对象就很不友好,在Java1.2后就引入了强引用、软引用、弱引用、虚引用,这四种引用引用强度依次减弱,意味着被回收的可能性就越大。对于强软弱虚四大引用需要用别的文章来详细解读,本文就先点出。

  出于考虑,不可达对象也不是非死不可,只能说有很大概率会死。要宣布一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有引用链到“GC Roots“那么它将会被标记并进行一次筛选。筛选的条件是该对象是不是有必要执行finalize()方法,如果对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将判定”没有必要执行‘;如果判定为必要执行,该对象将进入到一个F-Queue的队列中,在稍后又虚拟机自动建立一个线程去执行。将在队列中进行第二次标记,第二次标记的对象将回收。有点绕是不是?

举个例子:有一个县官把一个罪犯判定为死囚,这时候罪犯将进入到死囚区,等待执行死刑;这时候县官将死囚名单送往刑部,刑部审查案件证物,确定县官没有误判,将名单送交到皇帝 ,等待皇帝的朱批;在这个过程中会发生很多事,这个罪犯都是不用死的,发现新证据,证明他是清白的,释放;刑部检查,发现疑点,退回重审,暂时不用死,还押牢房,等待查明真相;皇帝没有在该对象上朱批,等待下一次朱批,押在牢房等待下一次批示。

判定一个对象是否无用是比较简单,要判定一个类是否无用就需要足够条件:

类的实例都已经被收回,Java堆中已经不存在类的任何实例;

加载该类的ClassLoader已经被回收;

类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过发射访问到该类的方法。

电视剧里面普通老百姓犯法了是不是马上就收监候审,而那种达官贵人,往往只有那种头铁的官才敢去硬刚,但是也没有那么容易除非是后台没人保护了,所有势力连根拔起才把他收监。

垃圾收集算法

标记-清除算法

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

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

举例:朝廷不可能每天都对死囚执行死刑,都是刑部将死囚标记好,收集好各地区的死囚名单,然后在万物枯荣的秋后进行统一的死刑执行。这就很耗费时间,同时要是没有及时将死囚的监牢释放出来,万一捕获了一伙亡命之徒,不是没地方关押了?

复制算法

将可用内存按照容量划分为大小相等的两块,每次只使用其中一块,当一块的内存用完,就将存活的对象复制到另一块上,然后把使用过的内存空间一次清理掉。这种算法将内存一份为二,缩小了一半的内存空间,未免牺牲太大。

很多对象都是很快释放,不需要按照1:1来分配内存空间,将内存分为较大块的Eden空间和两块较小的survivor空间,每次使用Eden空间和survivor空间,回收是将存活的对象放到另一块survivor空间中,最后清理Eden空间和survivor空间

举例:将牢房分为两个大小的空间,每次都将囚犯放到一个区域,等到执行完判决就将存活的囚犯放到另一半去,清理监牢;这样一来看押的犯人就少了一半的监牢,对于偷鸡摸狗的人,关押几天就放了,这样一来就需要分配。

标记-整理算法

标记方法和标记清理算法一样,后续步骤不是直接对可回收对象进行清理。而是让所有存活的对象向一端移动,然后直接清理掉端边界以为的内存。

举例:不是让县官关押在牢房里,而让死囚押往京城,然后本地的监牢就空出来了。

分代算法

根据对象存活周期的不同将内存划分为几块,在不同的区域使用不同的算法,非常灵活。

举例:不同罪行的人关押在不同的区域,然后根据量刑将他们进行审判,及时清理出监牢来关押新犯人。

HotSpot的算法实现

枚举根节点

 可达性分析是要将所有的根节点都找到,那么必将消耗很多时间;除此之外,可达性分析对执行时间的敏感海体现在GC停顿上,在枚举根节点时将要停顿所有线程。

安全点

程序执行时并非在所有地方都能停下来GC,只要到达安全点才能暂停。安全点的选定既不能太少以致于让GC等待时间太长,也不能过于频繁导致GC频发。除此之外,对于安全点,如何GC发生时让所有线程都跑到最近的安全点停顿下来。抢先式中断和主动式中断,抢先式不需要线程执行的代码主动配合;主动式中断是当需要中断线程时候,不直接对线程操作,设置标志,各线程执行时主动轮询这个标志,发现标志时自己中断挂起。

安全区域

当有的线程不执行,处于sleep或者blocked状态,线程不能自己轮询或者跑到安全点。安全区是指在一段代码片段中,引用关系不会发生变化,在这个区域任意地方开始GC都是安全。

本文要是有遗漏的地方,我将在评论区给出,同时对本文不当之处也会在评论区指出。

特别注意:本文部分摘抄文字版权属于原作者!!!