你必须了解的java内存管理机制(三)-垃圾标记

本文在我的技术博客不一样步发布,详情可用力戳
亦可扫描屏幕右侧二维码关注我的公众号,公众号内有我的联系方式,等你来撩...html

相关连接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8)
一、 你必须了解的java内存管理机制-运行时数据区
二、 你必须了解的java内存管理机制-内存分配
三、 你必须了解的java内存管理机制-垃圾标记java

前言

  前面花了两篇文章对JVM的内存管理机制作了较多的介绍,经过第一篇文章先了解了JVM的运行时数据区,而后在第二篇文章中经过一个建立对象的实例介绍了JVM的内存分配的相关内容!那么,万众瞩目的JVM垃圾回收是时候登场了!JVM垃圾回收这块的内容相对较多、较复杂。可是,想要作好JVM的性能调优,这块的内容又必须了解和掌握!安全

正文

一、怎么找到存活对象?

  经过上篇文章咱们知道,JVM建立对象时会经过某种方式从内存中划分一块区域进行分配。那么当咱们服务器源源不断的接收请求的时候,就会频繁的须要进行内存分配的操做,可是咱们服务器的内存确是很是有限的呢!因此对再也不使用的内存进行回收再利用就成了JVM肩负的重任了! 那么,摆在JVM面前的问题来了,怎么判断哪些内存再也不使用了?怎么合理、高效的进行回收操做?既然要回收,那第一步就是要找到须要回收的对象!服务器

1.一、引用计数法

  实现思路:给对象添加一个引用计数器,每当有一个地方引用它,计数器加1。当引用失效,计数器值减1。任什么时候刻计数器值为0,则认为对象是再也不被使用的。举个小栗子,咱们有一个People的类,People类有id和bestFriend的属性。咱们用People类来造两个小人:并发

People p1 = new People();
      People p2 = new People();

  经过上篇文章的知识咱们知道,当方法执行的时候,方法的局部变量表和堆的关系应该是以下图的(注意堆中对象头中红色括号内的数字,就是引用计数器,这里只是举栗,实际实现可能会有差别):jvm

  

  造出来的p1和p2两我的,我想让他们互为最好的朋友,因而代码以下:ide

People p1 = new People();
    People p2 = new People();
    p1.setBestFriend(p2);
    p2.setBestFriend(p1);

  对应的引用关系图应该以下(注意引用计数器值的变化):性能

  

  而后咱们再作一些处理,去除变量和堆中对象的引用关系。this

People p1 = new People();
        People p2 = new People();
        
        p1.setBestFriend(p2);
        p2.setBestFriend(p1);
        
        p1 = null;
        p2 = null;

  这时候引用关系图就变成以下了,因为p1和p2对象还相互引用着,因此引用计数器的值还为1。线程

  

  优势:实现简单,效率高。
  缺点:很难解决对象之间的相互循环引用。且开销较大,频繁的引用变化会带来大量的额外运算。在谈实现思路的时候有这样一句话“任什么时候刻计数器值为0,则认为对象是再也不被使用的”。可是经过上面的例子咱们能够看到,虽然对象已经再也不使用了,但计数器的值仍然是1,因此这两个对象不会被标记为垃圾。
  现状:主流的JVM都没有选用引用计数法来管理内存。

1.二、可达性分析

  实现思路:经过GC Roots的对象做为起始点,从这些节点向下搜索,搜索走过的路径成为引用链,当一个对象到GC Root没有任何引用链相连时,则证实对象是不可用的。以下图,红色的几个对象因为没有跟GC Root没有任何引用链相连,因此会进行标记。

  

  优势:能够很好的解决对象相互循环引用的问题。
  缺点:实现比较复杂;须要分析大量数据,消耗大量时间;
  现状:主流的JVM(如HotSpot)都选用可达性分析来管理内存。

二、标记死亡对象

  经过可达性分析能够对须要回收的对象进行标记,是否标记的对象必定会被回收呢?并非呢!要真正宣告一个对象的死亡,至少要经历两次的标记过程!

2.一、第一次标记

  在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记。而且判断此对象是否必要执行finalize()方法!若是对象没有覆盖finalize()方法或者finalize()已经被JVM调用过,则这个对象就会认为是垃圾,能够回收。对于覆盖了finalize()方法,且finalize()方法没有被JVM调用过期,对象会被放入一个成为F-Queue的队列中,等待着被触发调用对象的finalize()方法。

2.二、第二次标记

  执行完第一次的标记后,GC将对F-Queue队列中的对象进行第二次小规模标记。也就是执行对象的finalize()方法!若是对象在其finalize()方法中从新与引用链上任何一个对象创建关联,第二次标记时会将其移出"即将回收"的集合。若是对象没有,也能够认为对象已死,能够回收了。

  finalize()方法是被第一次标记对象的逃脱死亡的最后一次机会。在jvm中,一个对象的finalize()方法只会被系统调用一次,通过finalize()方法逃脱死亡的对象,第二次不会再调用。因为该方法是在对象进行回收的时候调用,因此能够在该方法中实现资源关闭的操做。可是,因为该方法执行的时间是不肯定的,甚至,在java程序不正常退出的状况下该方法都不必定会执行!因此在正常状况下,尽可能避免使用!若是须要"释放资源",能够定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用,如File相关类的close()方法。下面咱们看一个在finalize中逃脱死亡的栗子吧:

public class GCDemo {
    public static GCDemo gcDemo = null;

    public static void main(String[] args) throws InterruptedException {

      gcDemo = new GCDemo();
        System.out.println("------------对象刚建立------------");
        if (gcDemo != null) {
            System.out.println("我还活得好好的!");
        } else {
            System.out.println("我死了!");
        }

        gcDemo = null;
        System.gc();
        System.out.println("------------对象第一次被回收后------------");
        Thread.sleep(500);// 因为finalize方法的调用时间不肯定(F-Queue线程调用),因此休眠一下子确保方法完成调用
        if (gcDemo != null) {
            System.out.println("我还活得好好的!");
        } else {
            System.out.println("我死了!");
        }

        gcDemo = null;
        System.gc();
        System.out.println("------------对象第二次被回收后------------");
        Thread.sleep(500);
        if (gcDemo != null) {
            System.out.println("我还活得好好的!");
        } else {
            System.out.println("我死了!");
        }

        // 后面不管多少次GC都不会再执行对象的finalize方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("execute method finalize()");
        gcDemo = this;
    }
}

  执行结果以下,具体就很少说啦,不明白的就本身动手去试试吧!

  

三、枚举根节点

  经过上面可达性分析咱们了解了有哪些GC Root,了解了经过这些GC Root去搜寻并标记对象是生存仍是死亡的思路。可是具体的实现就是那张图显示的那么简单吗?固然不是,由于咱们的堆是分代收集的,那GC Root链接的对象可能在新生代,也可能在老年代,新生代的对象可能会引用老年代的对象,老年代的对象也可能引用新生代。若是直接经过GC Root去搜寻,则每次都会遍历整个堆,那分代收集就无法实现了呢!而且,枚举整个根节点的时候是须要线程停顿的(保证一致性,不能出现正在枚举 GC Roots,而程序还在跑的状况,这会致使 GC Roots 不断变化,产生数据不一致致使统计不许确的状况),而枚举根节点又比较耗时,这在大并发高访问量状况下,分分钟就会致使系统瘫痪!啥意思呢,下面一张图感觉一下:

  

  若是是进行根节点枚举,咱们先要全栈扫描,找到变量表中存放为reference类型的变量,而后找到堆中对应的对象,最后遍历对象的数据(如属性等),找到对象数据中存放为指向其余reference的对象……这样的开销无疑是很是大的!

  为解决上述问题,HotSpot 采用了一种 “准确式GC” 的技术,该技术主要功能就是让虚拟机能够准确的知道内存中某个位置的数据类型是什么,好比某个内存位置究竟是一个整型的变量,仍是对某个对象的reference,这样在进行 GC Roots枚举时,只须要枚举reference类型的便可。那怎么让虚拟机准确的知道哪些位置存在的是reference类型数据呢?OopMap+RememberedSet!

  OopMap记录了栈上本地变量到堆上对象的引用关系,在GC发生时,线程会运行到最近的一个安全点停下来,而后更新本身的OopMap,记下栈上哪些位置表明着引用。枚举根节点时,递归遍历每一个栈帧的OopMap,经过栈中记录的被引用对象的内存地址,便可找到这些对象( GC Roots )。这样,OopMap就避免了全栈扫描,加快枚举根节点的速度。

  OopMap解决了枚举根节点耗时的问题,可是分代收集的问题依然存在!这时候就须要另外一利器了- RememberedSet。对于位于不一样年代对象之间的引用关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是RememberedSet!因此“新生代的 GC Roots ” + “ RememberedSet存储的内容”,才是新生代收集时真正的GC Roots(G1 收集器也使用了 RememberedSet 这种技术)。

3.一、安全点

  HotSpot在OopMap的帮助下能够快速且准确的完成GC Roots枚举,可是在运行过程当中,很是多的指令都会致使引用关系变化,若是为这些指令都生成对应的OopMap,须要的空间成本过高。因此只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint)。如何在GC发生时让全部线程(不包括JNI线程)运行到其所在最近的安全点上再停顿下来?这里有两种方案:

  一、抢先式中断:不须要线程的执行代码去主动配合,当发生GC时,先强制中断全部线程,而后若是发现某些线程未处于安全点,那么将其唤醒,直至其到达安全点再次将其中断。这样一直等待全部线程都在安全点后开始GC。

  二、主动式中断:不强制中断线程,只是简单地设置一个中断标记,各个线程在执行时主动轮询这个标记,一旦发现标记被改变(出现中断标记)时,就将本身中断挂起。目前全部商用虚拟机所有采用主动式中断。

  安全点既不能太少,以致于 GC 过程等待程序到达安全点的时间过长,也不能太多,以致于 GC 过程带来的成本太高。安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生安全点(在主动式中断中,轮询标志的地方和安全点是重合的,因此线程在遇到这些指令时都会去轮询中断标志!)。

3.二、安全区域

  使用安全点彷佛已经完美解决如何进入GC的问题了,可是GC发生的时候,某个线程正在睡觉(sleep),没法响应JVM的中断请求,这时候线程一旦醒来就会继续执行了,这会致使引用关系发生变化呢!因此须要安全区域的思路来解决这个问题。线程执行进入安全区域,首先标识本身已经进入安全区域。线程被唤醒离开安全区域时,其须要检查系统是否已经完成根节点枚举(或整个GC)。若是已经完成,就继续执行,不然必须等待,直到收到能够安全离开Safe Region的信号通知!

相关文章
相关标签/搜索