JVM原理及其机制(二)
目录
一 . 垃圾回收机制(GC)
二 . 垃圾回收的具体步骤
(1)先找出谁是垃圾
方案一:引用计数
方案二:可达性分析
(2)释放垃圾的内存空间
方案一:标记清除
方案二:复制算法
方案三:标记 — 整理
方案四:分代回收算法
一 . 垃圾回收机制(GC)
在我们上一期讲了 Java 在运行时内存的各个区域,对于程序计数器、虚拟机栈、本地方法栈这三个部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或线程结束时,内存就自然跟着线程回收了,
什么是垃圾回收机制呢?顾名思义,就是清理掉我们在 Java 堆中用完的、不用的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还 “ 活着 ”,哪些已经 “ 死去 ” 。
这个垃圾回收机制是很香的,程序员写代码放心大胆的 new ,JVM 会自动识别哪些 new 完的对象再也不用了,就会将其自动释放掉。
像我们主流语言中绝大部分都是内置了 GC 的,例如 GO、Python、PHP、JS ...... 但是我们的C++ 却不支持 GC ,这是为什么呢?
任何功能都是有代价的,C++ 的大佬们评估了风险之后,不愿意承担,所以 C++ 并不支持 GC,其主要原因有:要想在JVM 中引入 GC 机制,就需要额外的逻辑,首先会消耗不少的 CPU 开销,进行垃圾扫描和释放,其次在进行 GC 的时候可能会触发 STW(Stop The World)问题,导致程序卡顿。
二 . 垃圾回收的具体步骤
(1)先找出谁是垃圾
需要针对每个对象分别判定是否为 “ 垃圾 ”
方案一:引用计数
给每个对象分配一个计数器,这一计数器用来衡量该对象有多少个引用指向。每增加一个引用,计数器 + 1 ,每减少一个引用,计数器 - 1 ,当计数器减为 0 ,此时该对象就是 “ 垃圾 ” 了。
但是这个方案在实施过程中存在着一些问题,例如会消耗额外的空间。假设我们 Test 类就只有一个 int 成员(4个字节),为了引入引用计数,少说得搞个 short(2字节),内存就多占了 50% 。其次很可能导致 “ 循环引用 ” 使得判定出错,这跟我们之前学过的死锁问题有些相似。循环引用也是有解决方案的,需要引入更多的机制,例如环路检测(在 Python 中就使用的这种机制),但是这样一来,代价就更大了。所以综上所述,Java 并没有采用这种引用计数的方法,而是另一种:
方案二:可达性分析
可达性分析主要就是 “ 用时间换空间 ” ,在 JVM 中,专门搞了一波线程,周期性的去扫描代码中的所有对象,来判定每个对象是否 “ 可达 ”(可以被访问到),对应的,不可达的对象,就会被视为垃圾。
可达性分析的起点称为 GC root ,从 root 出发,尽可能的通过 root 访问到更多的对象,相当于遍历的过程,但严格来说,并不是树的遍历,而是图的遍历。一个程序中,GC root 不是只有一个,而是有很多很多,可以作为 GC root 的对象有三种:栈上的局部变量(引用类型)、方法区中,静态成员变量(引用类型)、常量池引用指向的对象。把所有的 GC root 都遍历一遍,针对每一个尽可能往下延伸。
(2)释放垃圾的内存空间
方案一:标记清除
标记清除法就是将需要回收的垃圾标记上,然后直接对其进行清理。
标记清除法主要有两大缺陷,首先是效率问题,标记和清除这两个过程效率都不高,其次是空间碎片问题,在清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行中需要分配较大对象时,就可能申请不了,因为无法找到足够的连续的空间(申请内存一定是需要连续的空间)。尽量避免内存碎片,时释放内存的关键,所以我们又有了第二种方案:
方案二:复制算法
复制算法就是将我们的内存空间一分为二,每次只使用一半,当我们进行垃圾清理的时候,将不是垃圾的对象拷贝到另一半中,并且确保拷贝的这些对象在这一半内存空间中是连续的,然后直接将原本那一半的内存空间全部释放掉。
复制算法的缺点也很明显:首先就是每次只能用一半的内存,内存空间利用率非常低,其次是如果存活下来的对象很多,复制的成本将会非常大。于是我们引入了第三种方案:
方案三:标记 — 整理
标记、整理非常类似与我们之前学习过的顺序表,删除中间元素。当我们需要对垃圾对象进行清理时,依次将还存活的对象往一端移动,将垃圾对象替换掉,最后直接清理掉端边界以外的内存。
这里需要注意的是,这里对于存活对象搬运的开销也不少。所以,综上所述,其实并没有哪一种方案能做到十全十美,在实际开发过程中,我们都是结合运用,取长补短,于是就有了分代回收:
方案四:分代回收算法
JVM 根据对象的 “ 年龄 ” ,将对象进行区分,年轻的我们叫做新生代,年老的我们称作老年代。这种 “ 年龄 ” 是怎样划分的呢?通过我们的可达性分析,周期性的,每次经过一轮的扫描对象仍然存活(不是垃圾),其年龄就 + 1 。
分代回收,是 JVM 的 GC 中的基本思想,当具体落实到 JVM 的实现层面上,JVM 还提供了许多种 “ 垃圾回收器 ” ,这些垃圾回收器在具体实施中就会对分代回收做进一步的扩充和实现,如下:
CMS 的原理同样也是采用分代回收,它的设计理念就是把整个 GC 过程拆分成多个阶段,能和业务线程并发执行就尽量并发,从而尽可能的减少 STW 的耗时:
G1 就是把整个内存分成很多个小块,不同的颜色(字母)就表示这一小块是新生代(伊甸区 / 幸存区)还是老年区。进行 GC 的时候,不要求一个周期就将其中的所有内存都回收一遍,而是一轮 GC 只回收其中的一部分就好,这样就可以很好的限制你一轮 GC 所花的时间,这样就使得 STW 的耗时在一个可控的范围之内。
OKK,咱们有关 JVM 的原理及其相关机制的内容板块就说这么多了,这部分主要就是靠被,这些知识就是靠我们去背八股文这种来记,当然了,大家也需要理解记忆,并不是说硬背啦。就这样吧,咱们下期再见咯,与诸君共勉!!!