1、Dead Or Alive

  我们都知道对象死亡的时候需要进行垃圾回收来回收这些对象从而释放空间,那么什么样的对象算是死亡呢,有哪些方法可以找出内存中的死亡对象呢?一般来说,我们可以这样认为:如果内存中不存在对当前对象的引用,则此对象一定是死亡状态;但是死亡状态的对象并不一定没有其他对象进行引用(可能存在死亡对象循环引用的情况)。这里需要说明一下,死亡的对象并不一定会被回收释放占用的空间,这种情况就是常称的"内存泄漏"。判定对象存活的算法一般是以下两种。

1.1 引用计数法

  引用计数法,即在对象内放置一个变量来表示这个对象被引用的次数,如果其他对象引用了当前对象则变量值+1,如果失去引用则-1,当变量值为0的时候表示没有引用,应当回收。此算法并没有被Java采用,因为其存在着一个致命的问题——循环引用。

  如上图中,栈中没有任何堆中两个对象的引用,而堆中的两个对象则互相持有对方的引用,如果使用引用计数法的话引用变量值永远不会为0,从而造成内存泄漏,两个互相引用的对象无法释放空间。

public class TestForGc {

    TestForGc testInstance;

    // 模拟上图的现象
public static void main(String[] args) {
TestForGc testA = new TestForGc();
TestForGc testB = new TestForGc();
testA.testInstance = testB;
testB.testInstance = testA;
testA = null;
testB = null; // 建议垃圾回收器进行回收操作
System.gc();
}
} 然后设置-XX:+PrintGCDetails打印GC日志:

最终新生代的对象全部被回收,说明JVM使用的并不是使用引用计数法来实现垃圾回收。

1.2 可达性分析算法(GCRoots)

  GCRoots,大意为选中一些特定的对象作为根节点,然后从这些根节点出发寻找可以引用到的所有对象,形成一条引用链引用网),不在这条链中的对象则标记为死亡,进行回收。根节点的特定对象从下列四种产生:

  1、虚拟机栈中引用的对象。

  2、本地方法栈中引用的对象。

  3、方法区中静态变量引用的对象。

  4、方法区中常量引用的对象。

  使用GCRoots便不会出现循环引用的问题,如图,虽然A、B相互引用,但是由于不在根节点的引用链中,所以会被标记为可回收对象。

  在Hotspot虚拟机对GCRoots算法的实现中,大致可以分为三个部分理解。

  1.2.1 枚举根节点

  如上所说,根节点的选取对象有四处,如果虚拟机对这些位置进行全盘扫描的话,效率自然要影响不少,所以Hotspot采用一种数据结构——OopMap来解决这个问题。在类加载完成的时候,虚拟机将对象的什么偏移量有什么对象计算出来,在JIT编译过程中在特定的位置记录下栈和寄存器中哪些位置是引用。这样一来GC在扫描的时候就可以直接得到这些引用的信息,从而减少GC的停顿时间。顺便一提,在枚举根节点的时候,为了保持“一致性”,不能再扫描的时候还出现对象引用变化的情况,所以需要暂停所有Java执行线程(被称为"STOP-THE-WORLD"),即便在具有划时代意义、可以并发执行的CMS收集器中在枚举根节点的时候也需要STW。

  1.2.2 安全点的设置

  OopMap数据结构可以说为GC的扫描减少了不少的时间,但是随之而来的还有一个问题,如果每条指令都生成对应的OopMap,那么想必需要大量的额外空间,GC的空间成本将十分巨大,就是何时生成对应OopMap成为当前面临的问题。之前说过在特定的位置会记录下引用的位置,这个特定的位置就是OopMap的生成时机,也就是“安全点(SafePoint)”,在Sop-The-World的时候线程要先跑到安全点才可以进行线程的停顿。那该如何判断这个特定的位置呢?如果设置的太少可能会导致GC时间变长,设置的太多会增大运行时的负荷。Hotspot给出的答案是以程序“是否具有让程序长时间执行的特征”为标准进行选定。"长时间执行"的明显特征就是指令复用,例如方法调用、循环跳转、异常处理等,只有这些指令才能产生安全点。

  对于安全点来说,另外一个问题就是采用什么样的方式让所有的线程跑到最近的安全点停顿。有两种实现的方式:

  1、抢先式中断:在GC发生的时候首先暂停所有线程,如果发现有线程没在安全点的话,则恢复线程,让其跑到最近的安全点再进行暂停。现在已经很少有使用抢先式的了。

  2、主动式中断:GC发生的时候不强制暂停线程,而是设置一个标识变量,线程会去轮询这个标志,如果为true则将自己中断挂起。这个轮询的位置和安全点是重合的,还有创建对象时需要分配内存的地方。

  1.2.3 安全区域

  上面安全点的设置几乎已经解决了问题,但是还少了一点,就是建立在线程都是执行状态的时候,那线程不执行的时候呢,例如进入休眠状态的线程,这时候自己不能跑到安全点也不能等待JVM分配时间。此时就需要安全区域来解决这一点。

  安全区域指的是在一段代码块中,引用关系不会发生变化。当程序走到安全区域的时候,则标识当前线程进入了安全区域。这时候发生GC的时候则可以不用管有安全区域标识的线程,而这些线程在快离开安全区域的时候必须要检查是否完成了根节点的枚举或者整个GC的过程),如果完成了才可以离开安全区域,否则必须待到完成为止。

2、垃圾回收算法

  现在我们知道哪些对象是死亡的,哪些对象应该回收,而这个回收有许多种实现的方式(算法),有的算法对死亡对象进行标记最后一并清除、有的算法将内存分块然后将存活对象从一头搬到另一头,还有算法在清除完死亡对象贴心的将存活的对象整放在一块儿,这些都是我们接下来要说的。

2.1 标记-清除算法

  正如这个算法的名称一般,其总共有两个阶段——"标记"和"清除":首先其会对所有的死亡对象进行标记,最后再一起将这些对象回收。

  这个算法是基础的算法,后续的算法都是对其缺点的一些改进。此算法有两个不足的地方,其一从上图也可以看得出来,垃圾回收后的内存空间不连续,造成许多的内存碎片。其二就是其效率问题,标记和清除的效率并不是太高,所以后续出现的算法都对两个缺点的调整和改进。

2.2 复制算法

  为了解决效率内存碎片的问题,一种称作"copy"的算法出现,这个算法将内存空间分成两份或以上,一份存放对象,一份空白,当进行垃圾回收的时候将所有的存活对象复制到空白的一份中,然后清空之前存放对象的空间

  上图也可以看得出,对此算法最重要的是内存空间的切分,如果切分不当可能会浪费大量的空间。当然使用得当也有十分值得的优点:一定范围内的高效率没有内存碎片。

 缺点:

  1、适用于存活对象相较死亡对象少的情况,例如新生代,如果存活的对象较多的话可能得到相反的效果。所以才说是一定范围的高效率。

  2、需要划分内存空间。如果本身的内存空间比较小还去划分的话那可能会导致频繁的GC,停顿时间增多,影响用户体验。

  另:此算法一般用在新生代做垃圾回收算法,并且将新生代分成三个部分——两个Survivor和一个Eden区,其比例默认为1:1:8(可以通过虚拟机参数改变)。当我们生成一个对象(通过关键字new或者反射)的时候,对象首先会分配在Eden区,等到Eden区放不下的时候则触发一次MinorGC,将Eden和其中一个Survivor中的存活对象一起移到另一个Survivor中,然后清空。顺带一提,有存活对象的Survivor总是称作From区,空白的Suvivor总是称作To区,一般新生代存活对象占5%左右。

2.3 标记-整理法

  复制算法是一个非常优秀的算法,但其只在存活对象想多较少的情况下表现良好,而对于其他例如年老代中这些存活对象较多的区域则可能是一种糟糕的选择。所以,需要一个更加合适的算法——标记-整理法。这个算法原理和步骤基本跟标记-清除一样,但是多出一个整理的步骤,也就是说整个过程为标记-清除-整理,结束之后不会产生内存碎片

2.4 分代算法

  严格来说这不能算是一种算法,应该是一种理念。其把整个内存空间分为两个区域——新生代年老代(1.8之前还有一个永久代,也就是方法区,但是在1.8之后已经删除)。并且虚拟机对对象定义了年龄的概念,表示该对象熬过了多少次GC,以此来作为对象放在新生代还是年老代的标准之一,默认新生代的对象15岁之后就可以进入年老代了。对于两个区域采用的回收算法也是不同的,新生代一般采用复制算法,年老代一般采用标记-整理法,当然具体还是得看使用的垃圾回收器,如果年老代使用的是CMS的话那么就是标记-清除了。

It is an honor if I could get some advices or corrections from you guys.

Java对象"后事处理"那点事儿——垃圾回收(一)的更多相关文章

  1. Java虚拟机详解(三)------垃圾回收

    如果对C++这门语言熟悉的人,再来看Java,就会发现这两者对垃圾(内存)回收的策略有很大的不同. C++:垃圾回收很重要,我们必须要自己来回收!!! Java:垃圾回收很重要,我们必须交给系统来帮我 ...

  2. Java GC系列(4):垃圾回收监视和分析

    本文由 ImportNew - lomoxy 翻译自 javapapers. 目录 垃圾回收介绍 垃圾回收是如何工作的? 垃圾回收的类别 垃圾回收监视和分析 在这个Java GC系列教程中,让我们学习 ...

  3. Java 中的四种引用及垃圾回收策略

    Java 中有四种引用:强引用.软引用.弱引用.虚引用: 其主要区别在于垃圾回收时是否进行回收: 1.强引用 使用最普遍的引用.如果一个对象具有强引用,那就 类似于必不可少的生活用品,垃圾回收器绝不会 ...

  4. java虚拟机学习-JVM调优总结-垃圾回收面临的问题(8)

    如何区分垃圾 上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断.垃圾回收程序收集计数为0的对象即可.但是这种方法无法解决循环引用.所以,后来实现的垃圾判断算法中,都是从程序运行 ...

  5. 巩固java(二)----JVM堆内存结构及垃圾回收机制

    前言:        我们在运行程序时,有时会碰到内存溢出(OutOfMemoryError)的问题,为了解决这种问题,我们有必要了解JVM的内存结构和垃圾回收机制. 正文: 1.JVM堆内存结构   ...

  6. 【转载】Java性能优化之JVM GC(垃圾回收机制)

    文章来源:https://zhuanlan.zhihu.com/p/25539690 Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我 ...

  7. Java虚拟机运行时数据区域及垃圾回收算法

    程序计数器 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空). Java 虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口 ...

  8. Java性能优化之JVM GC(垃圾回收机制)

    Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.st ...

  9. Java虚拟机(三):JVM垃圾回收机制

    概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计数器.虚拟机栈.本地方 ...

随机推荐

  1. js深度克隆对象、数组

    function deepCopy(o) { if (o instanceof Array) { var n = []; for (var i = 0; i < o.length; ++i) { ...

  2. SSM相关知识

    1.SpringMVC的工作流程? 1. 用户发送请求至前端控制器DispatcherServlet 2. DispatcherServlet收到请求调用HandlerMapping处理器映射器. 3 ...

  3. 我又不是你的谁--java instanceof操作符用法揭秘

    背景故事 <曾经最美>是朱铭捷演唱的一首歌曲,由陈佳明填词,叶良俊谱曲,是电视剧<水晶之恋>的主题曲.歌曲时长4分28秒. 歌曲歌词: 看不穿你的眼睛 藏有多少悲和喜 像冰雪细 ...

  4. vue父子组件钩子函数的执行顺序

    加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount-& ...

  5. .NET GC垃圾回收器

    GC垃圾回收器简介 全名: Garbage Collector 原理: 以应用程序的根(root)为基础,遍历应用程序堆(heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡 ...

  6. Java中NIO及基础实现

    NIO:同步非阻塞IO 来源:BIO是同步阻塞IO操作,当线程在处理任务时,另一方会阻塞着等待该线程的执行完毕,为了提高效率,,JDK1.4后,引入NIO来提升数据的通讯性能 NIO中采用Reacto ...

  7. 题解:2018级算法第二次上机 Zexal的流水线问题

    题目描述: 样例: 实现解释: 最基础的流水线调度问题,甚至没有开始和结束的值 实现方法即得出状态转移方程后完善即可,设a[][i]存储着第一二条线上各家的时间花费,t[][i]存储着i处进行线路切换 ...

  8. LeetCode初级算法--链表02:合并两个有序链表

    LeetCode初级算法--链表02:合并两个有序链表 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn. ...

  9. Ubuntu安装时卡死在启动界面

    上下选中Install Ubuntu后,按'e'进入编辑页面(不要按回车),删除'quiet splash'之后的"---",输入"$vt_handoff acpi_os ...

  10. thinkphp5底层基类封装、内部类函数

    记录下thinkphp5自定义底层基类.内部类函数使用笔记 大部分笔记来自tp手册. 底层常用代码的封装 在控制器中基类的起着至关重要的作用,整个项目的代码安全,复杂程度,易读性都要看你项目的基类架构 ...