一. 概述

    说起垃圾收集(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.");}@Overrideprotected 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堆氛围新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法, 在新生代中, 每次垃圾收集时都发现有大批对象死去, 只有少量存货, 那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集. 而老年代中因为对象存活率高, 没有额外的空间对它进行分配担保, 就必须使用"标记-清理"或"标记-整理"算法来进行回收.

转载于:https://www.cnblogs.com/vinsen/p/7879674.html

深入理解JVM(二)--垃圾收集算法相关推荐

  1. 深入理解JVM - ZGC垃圾收集器

    如果下面的一些概念有些不清楚的可以先看深入理解JVM - 垃圾收集器和深入理解JVM - Shenandoah垃圾收集器. ZGC(Z Garbage Collector)是一款由Oracle公司研发 ...

  2. JVM(三)--垃圾收集算法

    JVM(三)–垃圾收集算法 这篇博客的内容包括: 一.垃圾收集算法: 1,标记--清除算法: 2,复制算法: 3,标记--整理算法: 4,分代收集算法: 二.涉及到的问题: 1,标记清除,标记整理,复 ...

  3. 深入理解JVM - Shenandoah垃圾收集器

    如果下面的一些概念有些不清楚的可以先看深入理解JVM - 垃圾收集器. Shenandoah是一款只有OpenJDK才会包含的收集器,最开始由RedHat公司独立发展后来贡献给了OpenJDK,相比G ...

  4. JVM之垃圾收集算法和垃圾收集器详解

    这篇文章相比上一篇记录性的,多了不少我自己的理解,花费了很大的功夫整理,如果有时间和精力建议好好看一看深入理解JVM这本书. 也建议熟读背诵. JVM-垃圾收集器和内存分配策略 程序计数器.虚拟机栈. ...

  5. 深入理解JVM(2)——GC算法与内存分配策略

    说起垃圾收集(Garbage Collection, GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Sca ...

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

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

  7. JVM之垃圾收集算法

    在C++中垃圾回收是需要程序员显示地进行收集,在java语言中是由JVM进行负责垃圾收集的,以下介绍几种垃圾收集算法的思想. 标记-清除算法 标记-清除是最基础的算法,就如其名字一样,分为" ...

  8. 深入理解GBDT二分类算法

    我的个人微信公众号: Microstrong 微信公众号ID: MicrostrongAI 微信公众号介绍: Microstrong(小强)同学主要研究机器学习.深度学习.计算机视觉.智能对话系统相关 ...

  9. JVM中垃圾收集算法

    1.标记-清除算法 最基础的垃圾收集算法,见名知意,该算法分为标记和清除两个阶段. ①首先标记所有需要回收的对象 ②标记后,统一回收所有被标记的对象 缺点: 效率问题:标记.清除两个过程效率都不高 空 ...

最新文章

  1. JavaScript的Array一些非常规玩法
  2. stm32 udp连续发送大量数据_TCP和UDP详解
  3. vscode php输出,js程序如何在vscode控制台输出
  4. 操作系统中抢占式和非抢占式内核的区别
  5. JAVA对时间的几个处理小方法
  6. 解决微信小程序新建项目没有样式问题,以及官方demo
  7. 如何在TypeScript/JavaScript项目里引入MD5校验和
  8. 七年级计算机与信息安全教案,计算机与信息安全教案.docx
  9. keras搭建多层LSTM
  10. 现代编译原理——第五章:活动记录
  11. css网页设计qq彩贝
  12. 记录deecamp2018之旅
  13. Ubuntu deb文件 安装 MySQL
  14. 基于Apache Kylin的分析案例
  15. wps图表横纵坐标怎么设置_用WPS的excel插入图表怎样快速设置横纵坐标轴
  16. 【Unity Shader】Special Effects(一)UI特效的动画播放器
  17. 7z001怎么解压在安卓手机上面_安卓手机用户换iPhone11怎么转移手机便签内容?...
  18. c语言设计题目代码,C语言课程的设计题目.doc
  19. 汽车电子EMC试验标准ISO11452
  20. 计算机基础知识学习题,超全的计算机基础知识题库【精心整理_完全免费】.pdf...

热门文章

  1. 2022-2028年中国污泥处理处置行业深度调研及投资前景预测报告
  2. 网络安全工具:Nmap
  3. 宿主机虚拟机文件复制 apt-get 换成yum
  4. linux 虚拟环境
  5. 算法精解:DAG有向无环图
  6. nvGRAPH API参考分析(一)
  7. 新十年嵌入式音频的五大趋势
  8. 克服汽车摄像头连接挑战
  9. 激光雷达和V2X技术
  10. CVPR2019:无人驾驶3D目标检测论文点评