[三]垃圾收集理论

Java 和 c++ 之间有一堵有内存动态分配和垃圾收集围成的墙,墙外的人想进来,墙里的人想出去

以下所有内容来自于 《深入理解 Java 虚拟机》这本书的理解和回顾,非照抄原文,肯定有错误,也有故意致错,一切皆为方便理解

概述(垃圾收集理论相当于 扫地的手法,技巧)

垃圾收集理论是一种思想,算法,和 Java 并没有必然的联系,垃圾收集技术的历史比 Java 更为久远,只是说 Java 在设计的时候加入了垃圾收集技术

相信很多人一提到 Java 的垃圾收集 GC 就想起新生代,老年代,整理-复制算法等等,这些并不是 Java 的产物,而是垃圾收集技术的产物,这种思想和做法被 Java 垃圾收集器予以实现,简单来说,就是 Java 采用了一系列垃圾收集器,垃圾收集器采用了一系列算法实现垃圾收集,这些算法就是我们熟知的新生代,老年代。不同的收集器,算法不一样,所以不能说 Java 的 GC 就是新生代这些,在下一代收集器中,新生代,老年代,整理-复制算法可能都会消失,因为新生代,老年代是目前主流的垃圾收集算法,大多数垃圾收集器都遵循分代收集原则,所以在很多文章中就直接把 这些和 Java 本身等效了,但是要明白这两者之间是通过垃圾收集器连接的,一旦更换其他的收集器,可能就不存在新生代,老年代了。

千万不要把垃圾收集算法和 Java 绑定在一起,垃圾收集算法绑定垃圾收集器,垃圾收集器才和 Java 直接交互

这一章我打算简单回顾一下通用的垃圾收集理论,它们将会被垃圾收集器所实现,进而为 Java 服务

三个问题,注意,以下都是理论,并不代表垃圾收集器实际使用

那些内存需要回收

什么时候回收

怎么回收

1.那些内存需要回收?

判断对象 ”存活“ or ”死亡“ ,死掉的对象就被回收

两种算法:

1.引用计数法

向每一个对象添加一个计数器,当它被引用时 +1 ,引用失效 -1 ,为 0 ,判定死亡,因为已经没有任何地方使用它了

简单高效,但是如果两个对象互相引用,左脚蹬右脚,就没完没了了,永远不可能回收

2.可达性分析

找到所有的根对象(GC ROOT)作为起始节点,根据它们的引用关系向下搜索,形成引用链,在这条链之外的对象,就是不能被使用的,不能用,自然就要被回收,至于如何找到根节点参考 ”hotspot 算法具体实现“

那么那些是根对象呢?

位置对象
虚拟机栈栈帧中的本地变量表
本地方法栈JNI(一般指的Native方法)引用的对象
方法区静态属性引用对象
方法区常量引用对象

2.在什么时候?

这里就用到另一个理论,分代收集

说是理论,其实是长期以来程序员的经验总结,其建立在两个分代假说之上:

  • 弱分代假说:大多数对象都是朝生夕死的
  • 强分代假说:熬过越多次垃圾收集的对象就越难以消亡

垃圾收集器按照这两个原则设计:由于对象的生命力不同,所以要进行分类处理,将 Java 堆划分为不同区域,对每个区域针对性制定对策,各个击破

将朝生夕死的对象放在一起,只关注存活对象(量少,耗费资源就少),将老不死的对象集中放入一个区域,因为已经确定它们很难死掉,所以减少回收频率,降低人手配给,就可以省出一笔资源。

由于现在可以只回收其中一个区域,所以便出现了 Minor GC/MajorGC (新生代收集/老年代收集)等一些名词,和一些针对不同区域设计的处理方法 – 整理复制/整理删除,这一切都始于分代收集理论

基于以上原则:我们的 Java 虚拟机 将 Java 堆分为新生代和老年代,新出现的对象放在新生代,经过一定回收次数还没有死的对象移入 VIP 内存(老年代)

但是在发展过程中,程序员发现分代理论有一个漏洞,类似于引用计数法中的相互引用的问题,老年代和新生代之间的相互引用,由于现在的垃圾回收只限定于一个区域,也就是说,对于垃圾收集器,它并不知到你在另一个区域还有 ” 小三 “ ,它把你干掉后,另一个区域的对象找不到你,于是垃圾收集器为了避免这个错误,不得已把两个区域都要扫描一遍,才知道,哦,原来你还有个远方亲戚,这样的话,效率又降低了,每碰到一个对象,都要把两个区域扫描一遍,失去了分代收集的初衷,那该怎么解决呢?

俗话所,科学不够,玄学来凑,事实上,徐徐多多的理论都是建立在哲学之上的。

引入第三条假说:

  • 跨代引用假说:跨代引用相对于同代引用来说,占比太低

意思就是,我若被老年代引用,那只要他不死,我也不会死,我迟早会变成老年代,也就不存在跨代引用了,互相引用的对象,应该是同生同死,但是该怎么处理这少量的跨代引用呢?

建立全局数据结构(记忆集),记录这不多的跨代引用,扫描的时候,把他加入到进去就行了

现在回到开头,依托于分代收集理论,将堆分为不同的区域,收集分为两种方式

  • 定时回收,新生代频率 > 老年代频率
  • 当内存占满时触发回收

3.怎样回收?

面对如何回收,程序员们又提出了三个理论

1.标记-清除

  • 将需要回收的对象标记起来,完成后统一清除,下面两种方式均是它的升级版
  • 问题:
  • 1.空间碎片化
  • 2.对象越多,标记和清除效率越低

2.标记-复制

  • 内存分成两个部分,一部分存放对象,将存活对象标记,然后统一复制到第二个区域
  • 优势:解决碎片化,实现简单
  • 问题:1.对象越多,标记和清除效率越低 。 2.空间利用率低,只有1/2
  • 一般用作新生代收集,由于新生代对象存活率低,所以设计成 eden 和 两个Survivor区,比例 8 : 1:1

3.标记-整理

  • 将标记对象向一边移动,然后清理掉另一边的对象
  • 优势:解决碎片化,并且空间利用率高
  • 问题;需要对移动后的对象更新地址,是一个极为负重的操作,甚至要停顿用户线程, 对程序影响较大

4. Hotspot 的算法细节实现

上面从三个问题简单阐述了 Java垃圾回收的理论基础,由于hotspot 使用可达性分析,所以这里也提一下它的细节实现

1.根节点枚举(找到根节点)

上面说了 Java 通过 GC Root 找到所有根节点,这里又会出现一些问题

  • 由于 Java 应用越来越庞大,扫描 GC Root 会越来越慢
  • 可达性分析必须保证对象不在变化,根节点枚举时必须停顿

解决方案:

**oopMap :**在类类加载完毕时,Hotspot 会将 GC root 保存到名为 oopMap 的数据结构中,就不用去扫描了

安全点: 代码特定位置,在安全点生成 oopMap,gc 也在安全点进行,就像爬山过程中的补给站一样,在这里,线程停滞,并进行一系列的活动,安全点均匀分布在程序各处,保证任何时候,程序都能找到最近的安全点

gc 使用安全点:为垃圾收集设置标志位,开始收集时,操作标志位,各个线程轮询这个标志位,发现标志为真,则运行到最近的安全点挂起来,等待 gc

安全区域: 安全点升级版,能够确保代码在一个片段类引用关系不变化,就把这个片段称之为安全区域,主要是解决程序不往前走,到达不了安全点的问题,声明为安全区域的代码,不管有没有走到安全点, gc 都不管他。,不过程序必须要等到 gc 完毕才可以走出安全区域

2.记忆集和卡表

前面我们提到,为了解决跨代引用问题,在新生代建立了一个记忆集,记录跨代引用的对象

记忆集是一种抽象的数据结构,卡表是记忆集的一种实现方式,本质是一个数组

如图,我们将需要标识的内存区域划分为以 512 字节为单位的内存块,每一块称之为卡页 ,然后将卡表中的元素和每一个卡叶一一对应,若对应的卡页内存在跨代引用(不管有多少个),则将对应的卡表元素设置为 1 ,在垃圾收集时,把元素为 1 的对应的内存区域加入 GC root 扫描当中

那么如何更新卡表呢?换句话说,卡表怎么知道它对应的内存有引用对象

3.内存屏障

在机器码层面,对象 A 引用了对象 B 时,会执行一条赋值指令 – 引用字段的赋值,在赋值前后加入更新卡表的代码,前面叫做写前屏障,后面的叫写后屏障。

当然为了防止伪共享的问题(多线程操作同一卡表元素,类似线程安全),在写屏障之前加入一条判断代码,如果卡表元素已经被标记为 1 了,后面再遇到关于这个内存的跨代引用,就不用再重复标记了

4.并发标记

上面提到在可达性分析中,会让用户线程停顿,完成根枚举,保证正确性,但是事实是复杂的,我们必须考虑到各个方面,在并发标记时,用户线程和 gc 并发进行,这个时候,对象的关系是不断变化的,那么该如何保证根枚举的正确性?

先来分析一下这个过程:

如图,下图表示可达性分析过程,黑色标识已经扫描过了,灰色表示已经扫描过了,但是他后面至少还有一个对象没有被扫描,白色表示完全没有被扫描,实线表示原来对象从属关系,虚线表示被用户程序改变后的关系,正常情况下应该是从左到右,依次变黑,但是如果在这过程中,1 被扫描后,4 被改动到 1 下,那么在扫描 2 时就找不到 4 ,扫描完毕后,整个引用链中没有 4 ,那么就出现 “对象消失” 现象

总之,并发会导致两种情况:浮动垃圾(本来要消亡的误标记为存活,这个可以留到下一次 gc ,不影响)和对象消失

解决方案:

增量更新:当 4 跑到 1 下面时,标记一下,在扫描完毕后,对标记对象 1 重新扫描

原始快照:不管 4 跑到哪里,在 4 删除对 2 的引用时 ,2 记录这个引用,在扫描完毕后,以 2 为根,根据历史引用记录重新扫描到 4 .

至此,关于垃圾收集器的所有基本理论回顾完毕,下一章 :将理论变为现实 – 垃圾收集器

,标记一下,在扫描完毕后,对标记对象 1 重新扫描

原始快照:不管 4 跑到哪里,在 4 删除对 2 的引用时 ,2 记录这个引用,在扫描完毕后,以 2 为根,根据历史引用记录重新扫描到 4 .

至此,关于垃圾收集器的所有基本理论回顾完毕,下一章 :将理论变为现实 – 垃圾收集器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值