Java对象"后事处理"那点事儿——垃圾回收(一)

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.

原文地址:https://www.cnblogs.com/zhangweicheng/p/11795887.html

时间: 2024-08-16 13:51:50

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

Java实现类的重载及使用垃圾回收钩子函数

下面以代码加注释,来方便读者理解. package com.company; public class Main { public static void main(String[] args) { // 未被引用的,无用对象 new Book("hello Java"); // 手动调用系统回收进行标记,进行回收 System.gc(); } } //声明一个书类 class Book { // 书的名称 String bookName; // 书的价格 double bookPri

深入洞见:你所不知道的Java 对象序列化的5件事儿

关于 Java 对象序列化的一些有用的小知识 不易理解,但对于解决 Java 编程挑战迟早有用. 将 Java 对象序列化 API它从一开始就存在于 JDK 1.1 中.本文介绍的关于序列化的 几件事情将说服您重新审视那些标准 Java API. Java 序列化简介 Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态. 到现在为止,还没有看到什么新鲜

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

Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-the-world 的发生. JVM GC回收哪个区域内的垃圾? 需要注意的是,JV

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

Java 中有四种引用:强引用.软引用.弱引用.虚引用: 其主要区别在于垃圾回收时是否进行回收: 1.强引用 使用最普遍的引用.如果一个对象具有强引用,那就 类似于必不可少的生活用品,垃圾回收器绝不会回收它.当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题. 2.软引用(SoftReference) 如果一个对象只具有软引用,那就类似于可有可物的生活用品.如果内存空间足够,垃圾回收器就不会回收它,如果

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

在这个Java GC系列教程中,让我们学习用于垃圾回收监视和分析的工具.然后,选用一种工具来监视一个Java示例程序的垃圾回收过程.如果你是一名初学者,你最好仔细阅读该系列教程.你可以从这里(垃圾回收介绍)开始. Java GC监视和分析工具 下面是一些可用的工具,每个都有自己的优势和缺点.我们可以通过选择正确的工具并分析,来提升应用程序的性能.这篇教程中,我们选用Java VisualVM. Java VisualVM Naarad GCViewer IBM Pattern Modeling

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

如何区分垃圾 上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断.垃圾回收程序收集计数为0的对象即可.但是这种方法无法解决循环引用.所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象.那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的.上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始.同时,一个栈是与一个线程对应的,因此,如果有多

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

章来源:https://zhuanlan.zhihu.com/p/25539690 Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成.GC优化很多时候就是减少stop-

Java垃圾回收原理(2)

Java虚拟机采用一种自适应的垃圾回收技术.依据的思想:对任何"活"的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用.这个引用链条可能会穿过数个对象层次.由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有活的对象.对于发现的每个引用,必须追踪它所引用的对象,然后是此对象所包含的所有的引用,如此反复进行,直到"根源于堆栈和静态存储区的引用"所形成的网络全部被访问为止.至于如何处理找到存活的对象,取决于不同的Java虚拟机的实现.有一种名为停止-复

Java内存管理及垃圾回收总结

概述 Java和C++的一个很重要的差别在于对内存的管理.Java的自己主动内存管理及垃圾回收技术使得Java程序猿不须要释放废弃对象的内存.从而简化了编程的过程.同一时候也避免了因程序猿的疏漏而导致的内存泄露问题. 内存管理和垃圾回收是JVM很重要的一个部分.深入理解Java的内存管理和垃圾回收机制是避免及修复Java相关异常(OutOfMemoryError, StackOverflowError),理解Java对象创建过程,有效利用内存.构建高性能Java应用的前提.本文将先后介绍Java