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

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

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

前言

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

正文

1、怎么找到存活对象?

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

1.1、引用计数法

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

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

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

  

  造出来的p1和p2两个人,我想让他们互为最好的朋友,于是代码如下:

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

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

  

  然后我们再做一些处理,去除变量和堆中对象的引用关系。

        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.2、可达性分析

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

  

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

2、标记死亡对象

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

2.1、第一次标记

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

2.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;
    }
}

  执行结果如下,具体就不多说啦,不明白的就自己动手去试试吧!

  

3、枚举根节点

  通过上面可达性分析我们了解了有哪些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.1、安全点

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

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

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

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

3.2、安全区域

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

原文地址:https://www.cnblogs.com/sujing/p/11110335.html

时间: 2024-08-09 23:13:21

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

java内存管理机制(一)-运行时数据区

前言 本打算花一篇文章来聊聊JVM内存管理机制,结果发现越扯越多,于是分了三遍文章(文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8),本文为其中第一篇.from java内存管理机制(一)-运行时数据区  1. java内存管理机制-运行时数据区 2. java内存管理机制-内存分配 3. java内存管理机制-垃圾回收 正文 C++与java之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙里的人却想出来…… 与C.C++程序员时刻要关注着内存的分配与释放

java内存管理机制

JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间.释放 :对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作.但同时,它也加重了JVM的工作.因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请.引用.被引用.赋值等,GC都需要进行监控. 2. 

PHP内存管理机制与垃圾回收机制

PHP内存管理机制 1 var_dump(memory_get_usage()); //获取内存 2 $a = "laruence"; //定义一个变量 3 var_dump(memory_get_usage()); //定义变量之后获取内存 4 unset($a); //删除该变量 5 var_dump(memory_get_usage()); //删除变量后获取内存 6 从上面可以看出php的内存管理机制是:预先给出一块空间,用来存储变量,当空间不够时,再申请一块新的空间. 1.存

Java 内存管理机制:04 Java 内存分配策略

Java 内存分配策略 Java 内存分配策略 优先在 Eden 区分配 大对象直接进入老年代 长期存活的对象将进入老年代 空间分配担保 新生代和老年代的 GC 操作 新生代 GC 操作:Minor GC 发生的非常频繁,速度较块. 老年代 GC 操作:Full GC / Major GC 经常伴随着至少一次的 Minor GC: 速度一般比 Minor GC 慢上 10 倍以上. 优先在 Eden 区分配 Eden 空间不够将会触发一次 Minor GC: 虚拟机参数: -Xmx:Java 堆

Java内存管理第三篇 - 内存可能产生的问题

Java内存在分配和回收的过程中会产品很多的问题,下面来说一说可能会产生的问题. 1.垃圾处理 从程序运行的根节点出发,遍历整个对象引用,查找存活的对象.那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的.上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始.同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查. 同时,除了栈外,还有系统运行时的寄存器等

一次由于Java内存管理机制导致的线程异常阻塞之旅

引言 一转眼已经两年多没写多博客了:一转眼也要快工作两三年了:一转眼我又开始写Java代码了.希望自己可以坚持写写博客,总结总结的习惯!加油. 今天在调试代码的时候,发现两个毫不相关的thread用jstack看竟然其中一个在等待另一个的线程持有的锁,很是奇怪.经过研究,是因为Integer类的实现机制导致的. 一.异常阻塞代码 1 package xxx; 2 3 public class TestDeadLock { 4 public static void main(String[] ar

JVM内存管理机制和垃圾回收机制

JVM自身结构物理图: Java代码编译和执行的整个过程包含了以下三个重要的机制: 1.java源码编译机制 1)分析和输入到符号表 class文件结构包含: 结构信息.包括class文件格式版本号及各部分的数量与大小的信息 元数据.对应于Java源码中声明与常量的信息.包含类/继承的超类/实现的接口的声明信息.域与方法声明信息和常量池 方法信息.对应Java源码中语句和表达式对应的信息.包含字节码.异常处理器表.求值栈与局部变量区大小.求值栈的类型记录.调试符号信息 2.类加载机制 1)Boo

2、COCOS2D-X内存管理机制

在C++中,动态内存分配是一把双刃剑,一方面,直接访问内存地址提高了应用程序的性能,与使用内存的灵活性:另一方面,由于程序没有正确地分配与释放造成的例如野指针,重复释放,内存泄漏等问题又严重影响着应用程序的稳定性. 人们尝试着不同的方案去避免这个问题,比较常用的如智能指针,自动垃圾回收等,这些要么影响了应用程序的性能,要么仍然需要依赖于开发者注意一些规则,要么给开发者带来了另外一些很丑陋的用法(实际上笔者很不喜欢智能指针).因此,优秀的C++内存管理方案需要兼顾性能,易用性,所以到目前为止C++

【Java深入研究】3、JVM内存管理机制

转自:http://blog.csdn.net/lengyuhong/article/details/5953544 近期看了看Java内存泄露的一些案例,跟原来的几个哥们讨论了一下,深入研究发现JVM里面还是有不少以前不知道的细节,这里稍微剖析一下.先看一看JVM的内部结构-- 如图所示,JVM主要包括两个子系统和两个组件.两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统:两个组件分别是Runtime data area (运行时数据区域)组