一. 概述

    说起垃圾收集(Garbage Collection, GC), 大部分人都把这项技术当做Java语言的伴随生产物. 事实上, GC的历史远远比Java久远, 1960年 诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言. 当Lisp还在胚胎时期时,人们就在思考GC需要完成的三件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

  现在内存的动态分配与内存回收技术已经相当成熟, 那为什么我们还要去了解GC和内存分配呢? 答案很简单: 当需要排查各种内存溢出, 内存泄漏问题时, 当垃圾收集称为系统达到更高并发量的瓶颈时, 我们就需要对这些"自动化"的技术实施必要的监控和调节.

二. 对象的生与死

    堆中几乎存放着Java世界中所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事情就是要确定这些对象还有哪些还"存活", 哪些已经"死去"(即不可能再被任何途径适用的对象).

  1. 引用计数算法

    概念: 给对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值+1; 当引用失效时, 计数器值-1; 任何时刻计数器都为0的对象就是不可能再被使用的.

    客观地说, 引用计数算法(Reference Counting) 的实现简单, 判定效率也很高, 在大部分情况下它都是一个不错的算法, 也有一些比较著名的应用案例, 例如微软的COM(Component Object Model) 技术, 使用ActionScript 3的FlashPlayer, Python语言以及在游戏脚本领域被广泛引用的Squirrel中都使用了引用计数算法进行内存管理.但是, Java语言没有选用引用计数算法来管理内存, 其中最主要的原因是它很难解决对象之间的相互循环引用的问题.

    2. 根搜索算法

    概念: 通过一系列的名为"GC Roots" 的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots 没有任何引用链想连(用图论的话来说就是从GC Roots到这个对象不可达)时, 则证明此对象是不可用的.

    在Java和C#, 以及上面提到的古老的Lisp, 都是使用跟搜索算法(GC Roots Tracing) 判断对象是否存活的.

    在Java语言里, 可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象.
  • 方法区中的类静态属性引用的对象.
  • 方法区中的常量引用的对象.
  • 本地方法栈中JNI(即一般说的Native方法)的引用的对象.

  3. 生存还是死亡

    在跟搜索算法中不可达的对象, 也并非是"非死不可"的, 这时候他们暂时处于"缓刑"阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程: 如果对象在进行根搜索后发现没有与GC Roots相连接的引用链, 那它将会第一次被标记并且进行一次筛选, 筛选的条件是此对象是否有必要进行finalize()方法, 当对象没有覆盖finalize() 方法, 或者finalize()方法已经被虚拟机调用郭, 虚拟机将这两种情况都视为"没有必要执行".

    如果这个对象有必要执行finalize()方法, 那么这个对象将会被放置在一个名为F-Queue的队列之中, 并在稍后由一条由虚拟机自动建立的, 低优先级的Finalizer线程去执行. finalize()方法是对象逃脱死亡命运的最后一次机会, 稍后GC将对F-Queue中的对象进行第二次小规模标记, 如果对象要在finalize()中成功拯救自己---只要重新与引用链上的任何一个对象建立关联即可, 譬如把自己赋值给某个类变量或对象的成员变量, 那在第二次标记时它将被移除出"即将回收的集合", 如果对象这时候还没有逃脱, 那它就这的离死不远了.

    代码: 一次对象自我拯救的演示

public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() {
System.out.println("Yes, I'm still alive.");
} @Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
} public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No, I'm dead.");
} SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("No, I'm dead.");
} } }

运行结果:

Finalize method executed!
Yes, I'm still alive.
No, I'm dead.

三. 垃圾收集算法

  1. 标记-清除算法(Mark-Sweep)

    如他的名字一样, 算法分为"标记"和"清除"两个阶段: 首先标记出所有需要回收的对象, 在标记完成后统一会受到所有被标记的对象,它的标记过程在上面讲述对象标记判定时已经基本介绍过了. 它是最基础的收集算法, 是因为后续的书记算法都是基于这种思路对其缺点进行改进而得到的.

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

  2. 复制收集算法(Coping)

    为了解决效率问题, 一种称为"复制"的收集算法出现了, 它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块. 当着一块的内存用完了, 就将还存活着的对象复制到另一块上面, 然后再把已使用过的内存空间一次清理掉, 这样使得每次都是对其中的一块进行内存回收, 内存分配时也不用考虑内存碎片等复杂情况, 只要移动堆顶指针, 按顺序分配内存即可, 实现简单, 运行高效. 只是这种算法的代价是将内存缩小为原来的一半, 未免太高了一点.

    现在的商业虚拟机都采用这种收集算法来回收新生代, IBM的专门研究表明, 新生代中的对象98%都是朝生夕死, 所以并不需要按照1:1的比例来话费呢内存空间, 二十将内存分为一块较大的Eden空间和两块较小的Survivor空间, 每次使用Eden和其中的一块Survivor. 当回收时, 将Eden和Survivor中还存活着的对象一次性的拷贝到另一块Survivor空间上, 最后清理掉Eden和刚才用过的Survivor的空间, HotSpot虚拟机默认Eden和Survivor的大小比例是8:1, 也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+ 10%), 只有10%的内存会被"浪费". 当然98%的对象可回收只是一般场景下的数据, 我们没有办法保证每次回收都只有不多余10%的对象存活, 当Survivor空间不够时, 需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion).

  3. 标记-整理算法(Mark-Compact)

    复制收集算法在对象存活率较高时就要执行较多的复制操作, 效率将会变低, 更关键的是, 如果不想浪费50%的空间, 就需要有额外的空间进行分配担保, 以应对被使用的内存中所有对象都100%存活的极端情况, 所以在老年代一般不能直接选用这种算法.

    根据老年代的特点, 于是提出了另一种"标记-整理"(Mark-Compact)算法, 标记过程仍与"标记-清除"算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉边界以外的的内存.

  4. 分代收集算法

    当前商业虚拟机的垃圾收集都采用"分代收集"(Generation Collection)算法, 这种算法并没有什么新思想, 只是根据对象的存活周期的不同将内存划分为几块. 一般是把Java堆氛围新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法, 在新生代中, 每次垃圾收集时都发现有大批对象死去, 只有少量存货, 那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集. 而老年代中因为对象存活率高, 没有额外的空间对它进行分配担保, 就必须使用"标记-清理"或"标记-整理"算法来进行回收.

深入理解JVM(二)--垃圾收集算法的更多相关文章

  1. 深入理解Java虚拟机 - 垃圾收集算法与垃圾收集器

    1. 垃圾收集算法       JVM的垃圾收集算法在不同的JVM实现中有所不同,且在平时工作中一般不会深入到收集算法,因此只对算法做较为简单的介绍.       1.1 标记-清除算法        ...

  2. 垃圾收集器与内存分配策略 (深入理解JVM二)

    1.概述 垃圾收集(Garbage Collection,GC). 当需要排查各种内存溢出.内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调 ...

  3. 理解JVM之垃圾收集器概述

    前言 很多人将垃圾收集(Garbage Collection)视为Java的伴生产物,实际1960年诞生的Lisp是第一门真正使用内存动态分配与垃圾手机技术的语言.在目前看来,内存的动态分配与内存回收 ...

  4. JVM中垃圾收集算法总结

      通过前面的介绍我们了解了对象创建和销毁的过程.那么JVM中垃圾收集器具体对对象回收采用的是什么算法呢?本文主要记录下JVM中垃圾收集的几种算法. JVM的垃圾回收的算法 标记-清除算法(Mark- ...

  5. JVM笔记-垃圾收集算法与垃圾收集器

    1. 一些概念 1.1 垃圾&垃圾收集 垃圾:在 JVM 语境下,"垃圾"指的是死亡的对象所占据的堆空间. 垃圾收集:所谓"垃圾收集",就是将已分配出去 ...

  6. 深入理解JVM(5)——垃圾收集和内存分配策略

    1.垃圾收集对象 垃圾收集主要是针对堆和方法区进行. 程序计数器.虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收. 哪 ...

  7. 深入理解JVM(三)——垃圾收集策略具体解释

    Java虚拟机的内存模型分为五个部分.各自是:程序计数器.Java虚拟机栈.本地方法栈.堆.方法区. 这五个区域既然是存储空间,那么为了避免Java虚拟机在执行期间内存存满的情况,就必须得有一个垃圾收 ...

  8. 深入浅出JVM之垃圾收集算法

    判断哪些对象需要被回收 引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就是不可能再被使用的. 但是JVM没有使 ...

  9. 深入理解Java虚拟机-垃圾收集算法

    一.判断对象是否可进行回收 1.引用计数算法 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就是不可能再被使用的.但是主流的 ...

随机推荐

  1. js提取JSON数据中需要的那部分数据

    var data =[ { name: "程咬金",sex:"1",age:26 }, { name: "程才",sex:"0&q ...

  2. must appear in the GROUP BY clause or be used in an aggregate function

    今天在分组统计的时候pgsql报错 must appear in the GROUP BY clause or be used in an aggregate function,在mysql里面是可以 ...

  3. 【Tool】---ubuntu18.04配置oh-my-zsh工具

    作为Linux忠实用户,应该没有人不知道bash shell工具了吧,其实除了bash还有许多其他的工具,zsh就是一款很好得选择,基于zsh shell得基础之上,oh-my-zsh工具更是超级利器 ...

  4. C++读书笔记

    C与C++的不同点 C++在struct,union中定义的函数叫成员函数,在class中定义的数据叫数据成员 C++引入了三个存取权限的关键字:public,protected,private pu ...

  5. 头条一面竟然问我Maven?

    maven package和maven install 有什么区别? 你常用的maven命令有哪些? <dependencyManagement> 是干什么的? 还有用过其它构建工具吗? ...

  6. [bzoj4872] [洛谷P3750] [六省联考2017] 分手是祝愿

    Description Zeit und Raum trennen dich und mich. 时空将你我分开. \(B\) 君在玩一个游戏,这个游戏由 \(n\) 个灯和 \(n\) 个开关组成, ...

  7. [bzoj1041] [洛谷P2508] [HAOI2008] 圆上的整点

    Description 求一个给定的圆(x^2+y^2=r^2),在圆周上有多少个点的坐标是整数. Input 只有一个正整数n,n<=2000 000 000 Output 整点个数 Samp ...

  8. [bzoj1297] [洛谷P4159] [SCOI2009] 迷路

    Description windy在有向图中迷路了. 该有向图有 N 个节点,windy从节点 0 出发,他必须恰好在 T 时刻到达节点 N-1. 现在给出该有向图,你能告诉windy总共有多少种不同 ...

  9. 深入浅出WPF笔记

    数据层(Database,Oracle等) 业务逻辑层(Service,Data Access Layer,WCF) 表示层(WPF,Win Form,ASP.net,Silverlight) [WP ...

  10. error while loading shared libraries: libevent-2.1.so.6 的解决办法

    执行 memcached 启动命令时,报错,提示:error while loading shared libraries: libevent-2.1.so.6: cannot open shared ...