深入理解 Java 虚拟机:GC垃圾收集器及相关算法
JVM 知识点整理:GC垃圾收集器判断哪些对象需要回收引用计数器算法可达性分析算法判断哪些对象需要回收Java 堆里存放着几乎所有的对象实例,因此在回收前需要判断哪些对象是 “存活” 的,这些对象不需要回收,只回收已经 “死去” 的对象(即不可能再被任何途径使用的对象)。引用计数器算法算法原理:给对象添加一个引用计数器,每当有一个地方引用它时,计算器 +1;当引用失效时,计数器 -1;任...
深入理解 Java 虚拟机:GC垃圾收集器
判断哪些对象需要回收
Java 堆里存放着几乎所有的对象实例,因此在回收前需要判断哪些对象是 “存活” 的,这些对象不需要回收,只回收已经 “死去” 的对象(即不可能再被任何途径使用的对象)。
引用计数器算法
算法原理:
给对象添加一个引用计数器,每当有一个地方引用它时,计算器 +1;当引用失效时,计数器 -1;任何时刻计数器为 0 的对象就是不可能再被使用的,可以被回收。
优点: 实现简单,判断效率高
缺点: 无法解决对象间相互引用的问题
应用: Python 语言,游戏脚本领域使用的 Squirrel 等
可达性分析算法
算法原理:
从一系列称为 “GC Roots
” 的对象为起点,沿着引用链
向下搜索,当一个对象到 GC Roots 没有任何引用链相连,
则证明此对象可以被回收(即 GC Roots 无法到达此对象)。
GC Roots 的对象:
-
虚拟机栈(栈帧中的本地变量表)中引用的对象。
对象
在创建时候,会在堆上开辟一个空间用于分配实例,之后把堆的地址
作为引用
存放在栈
中,在对象生命周期结束
后,引用就会从虚拟机栈中出栈
-
方法区(永久代)中类静态属性引用的对象。
即被static
修饰的静态对象
,存放在方法区
中 -
方法区(永久代)中常量引用的对象。
即被static
与final
修饰的对象,存放在方法区
中 -
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
JNI
即Java Native Interface
,主要用于帮助 Java 与 别的语言进行通信(主要 C,C++)提供接口。
缺点: GC停顿
,为了保证一致性
,导致GC进行时必须停顿
所有的 Java 执行线程
。可以想象下,系统运行一半,突然像被人按了暂停键一样突然卡住了,然后 GC 结束才继续。
引用还有分类(了解)
目的: 我们希望描述这样的一类对象,当内存空间足够
时,则保留
在内存中;如果空间在回收后还非常的紧张
,则可以抛弃
这些对象,很多系统的缓存功能
都符合这种场景。
强引用: 程序代码中普遍存在,类似 “Object obj = new Object()” 这类的引用,只要强引用
还在,垃圾收集器就永远不可能回收这个对象
。
软引用: 用来描述一些还有用但非必须的对象
。这种对象在系统将要发生内存溢出
钱,会把这些对象列进回收范围进行第二次回收
,依然没有足够内存,才会抛出内存溢出异常
。提供了 SoftReference 类
来实现软引用
。
弱引用: 用来描述非必须对象
,强度比软引用更弱
一些,被引用关联的对象只能生存到下次垃圾收集之前
。提供了 WeakReference 类
来实现弱引用
。
虚引用: 也成为幽灵引用或幻影引用,最弱的一张引用关系。这个引用不会对对象的生存时间产生影响
,也无法通过虚引用获取一个对象实例,这个引用的唯一目的,就是被收集器回收时收到一条系统通知
。提供了 PhantomReference 类
来实现虚引用
。
“缓刑” finalize(了解)
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖 finalize 方法
,或者 finzlize 方法已经被虚拟机调用
过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
如果对象要在 finalize() 中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联
即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。
第二次标记,基本就是被回收了,因为 finalize() 只会执行一次
。
不建议使用 finalize()
,原因是运行代价高昂,不确定性大,无法保证调用顺序
,比如使用 try-finally
之类的方式都可以做的更好,更及时
。
开始垃圾收集
标记 - 清除算法
最基础的收集算法(后面都是这个基础的改进算法)
,算法分为 “标记” 和 “清除” 两个阶段。
标记阶段: Java
算法就是上面的 可达性分析算法
。
回收阶段: 没什么特别的,就是直接把标记的回收了。
缺点:
效率问题
,标记和清除两个过程效率都不高。空间问题
,由于时标记后直接就回收了,导致空间会有大量不连续的内存碎片
,导致如果分配大内存
对象,无法找到足够的连续内存而不得不提前 GC。
复制算法
为了解决效率问题
,因此出现了称为 “复制”
的收集算法。
原理: 将内存分为大小相等
的两块,每次只用其中一块。当内存用完
了,把存活的对象
复制到另一块上,然后把之前已使用的空间清理掉。
优点:
- 只对半区的内存进行回收
- 不需要考虑内存碎片情况,因为移到另一半空闲的上面,按顺序分就行了
- 实现简单,运行高效
缺点: 内存变成原来的一半
改进:
现在商用虚拟机都采用这种算法回收新生代
,因为 98% 的新生代都死得快(Java 里一个方法跑完,里面的对象基本就都没用了)。
内存划分:一块较大的 Eden 空间
和 两块较小的 Survivor 空间
,Eden 比 Survivor 大小比例是 8 比 1。
每次使用 1 块 Eden,1 块 Survivor
,进行回收时候,会把 Eden 和 Survivor 上存活的对象
都复制到另一个 Survivor
上。当 另一个 Survivor
内存不足时,需要依赖其他内存(老年代)
进行分配担保
。
缺点: 老年代都是存活时间较长的对象,因此这种算法不适合于老年代。
标记 - 整理算法
原理: 标记过程与 “标记 - 清除” 算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
。
分代收集算法
把 Java 堆分为新生代
和老年代
,然后依照各自的特点采用最适当的收集算法。
规则:
在新生代
中如果发现有大批量
的对象死去,只有少量存活,就采用复制算法
。
在老年代
中因为对象存活率高且没有额外的空间对它进行分配担保,就必须使用 “标记 - 清理”
或 “标记 - 整理”
算法来进行回收。
HotSpot 算法
虽然标记算法用的是 可达性分析算法
,但是缺点存在 GC 停顿
问题,因此 HotSpot
在实现方面做了修改。
枚举根节点
即寻找所有可达性分析
中的 GC Roots
。
为了节省枚举根节点而采用的解决方案:
为了让虚拟机知道哪里存放着对象引用,即 GC Roots
,而不是把整个执行上下文和全局引用检查一遍,HotSpot
中使用了一组称为 OopMap
的数据结构来达到目的。
类加载完成的时候,把对象什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样,GC在扫描时就可以直接得知这些信息了。
安全点
在 OopMap 的协助下,HotSpot 可以快速且准确
地完成 GC Roots 枚举
,但有另一个问题,OopMap 内容变化的指令非常多,如果为每一个条指令都生成对应的OopMap,将需要大量的额外空间
。
解决方法:
在 特定的位置
记录信息(即 OopMap 的信息),这些位置被称为安全点
。
特点:
只在遇到 安全点
才能停顿
进行 GC
。
安全点既不能太少(内容就多了,GC 停顿就会变长)
,也不能过于频繁
以至于增大运行时的负荷(频繁 GC)
,标准就是 “是否据有让程序长时间执行的特征”
。
因此最明显的特征就是指令序列复用
,例如:方法调用、循环跳转、异常跳转等,才会产生安全点
。
如何让GC发生时,所有线程(除执行JNI调用的线程)都到最近的安全点停顿下来?
-
抢先式中断
,不需要线程的执行代码主动配合,在GC发生时,首先把所有线程中断
,如果有线程中断的地方不在安全点,就恢复线程,让它执行到安全点
。 -
主动式中断
,需要中断线程时,不直接对线程操作,而是设置一个标志
,各个线程执行时主动去轮询这个标志,发现中断标志为真时就中断挂起
。轮询标志的地方和安全点是重合的
。
安全区域
由于线程处于 Sleep
或 Blocked
的时候,线程无法响应 JVM
的中断请求,这时候就需要 安全区域
来解决问题。
特点:
在一段代码片段中,引用关系不会发生变化
。在这个区域的任何地方 GC 都是安全
的,可以把安全区域
看作是安全点
的扩展。
描述:
在线程执行到安全区域代码时,首先标识自己进入安全区域,当这段时间里 JVM 发起 GC,不用管标识为安全区域的线程了。在线程要离开安全区域时,要检查系统是否已经完成了根节点枚举,如果完成,线程继续执行,否则等待直到收到可以安全离开安全区域的信号为止。
更多推荐
所有评论(0)