`
Poechant
  • 浏览: 227528 次
博客专栏
Bebe66e7-3a30-3fc9-aeea-cfa3b474b591
Nginx高性能Web服务...
浏览量:24243
5738817b-23a1-3a32-86de-632d7da73b1e
Cumulus实时媒体服务...
浏览量:22051
社区版块
存档分类
最新评论

JVM 深入笔记(3)垃圾标记算法

 
阅读更多

JVM深入笔记(3)垃圾标记算法

  • Author: Poechant
  • Blog:blog.CSDN.net/Poechant
  • Email: zhongchao.ustc#gmail.com (#->@)
  • Date: March 3rd, 2012
  • Copyright © 柳大·Poechant

如果您还不了解 JVM 的基本概念和内存划分,请先阅读《JVM 深入笔记(1)内存区域是如何划分的?》一文。然后再回来 :)

因为 Java 中没有留给开发者直接与内存打交道的指针(C++工程师很熟悉),所以如何回收不再使用的对象的问题,就丢给了 JVM。所以下面就介绍一下目前主流的垃圾收集器所采用的算法。不过在此之前,有必要先讲一下 Reference。

1 引用(Reference)

你现在还是 JDK 1.0 或者 1.1 版本的开发者吗?如果是的话,可以告诉你跳过“5 Reference”这一部分吧,甚至跳过本文。如果不是的话,下面这些内容还是有参考价值的。你可能会问,Reference 还有什么可讲的?还是有一点的,你知道 Reference 有四种分类吗?这可不是孔乙己的四种“回”字写法可以类比的。说引用,我们最先想到的一般是:

Object obj = new Object();

这种属于 Strong Reference(JDK 1.2 之后引入),这类 ref 的特点就是,只要 ref 还在,目标对象就不能被干掉。我们可以想一下为什么要干掉一些对象?很简单,因为内存不够了。如果内存始终够用,大家都活着就好了。所以当内存不够时,会先干掉一些“必死无疑的家伙”(下面会解释),如果这时候内存还不够用,就该干掉那些“可死可不死的家伙”了。

JDK 1.2 之后还引入了SoftReferenceWeakReference,前者就是那些“可死可不死的家伙”。当进行了一次内存清理(干掉“必死无疑”的家伙)后,还是不够用,就再进行一次清理,这次清理的内容就是 SoftReference 了。如果干掉 Soft Reference 后还是不够用,JVM 就抛出 OOM 异常了。

好像 WeakReference 还没说呢?它是干嘛的?其实它就是那些“必死无疑的家伙”。每一次 JVM 进行清理时,都会将这类 ref 干掉。所以一个 WeakReference 出生后,它的死期,就是下一次 JVM 的清理。

“回”字的最后一种写法,是PhantomReference,名字很恐怖吧(Phantom是鬼魂的意思,不仅含义恐怖,而且发音也恐怖——“坟头”)。这类 ref 的唯一作用,就是当相应的 Object 被 clean 掉的时候,通知 JVM。

虽然有四种“回”字,但是 Strong Reference 却没有相应的类,java.lang.ref.Reference只有三个子类。

你可能会发现,在 Reference 这一部分,我经常性地提到“清理”。什么“清理”?就是下面要说的 Garbage Collection 中对”无用”对象的 clean。

这是 JVM 的核心功能之一,同时也是为什么绝大多数 Java 工程师不需要像 C++ 程序员那样考虑对象的生存期问题。至于因此而同时导致 Java 工程师不能够放任自由地控制内存的结果,其实是一个 Freedom 与 Effeciency 之间的 trade-off,而 C++ 工程师与 Java 工程师恰如生存在两个国度的人,好像“幸福生活”的天朝人民与“水深火热”的西方百姓之间的“时而嘲笑、时而艳羡”一般。

言归正传,Garbage Collector(GC)是 JVM 中筛选并清理 Garbage 的工具。那么第一个要搞清楚的问题是,什么是 Garbage?严谨的说,Garbage 就是不再被使用、或者认为不再被使用、甚至是某些情况下被选作“牺牲品”的对象。看上去很罗嗦,那就先理解成“不再被使用”吧。这就出现了第二个问题,怎么判断不再被使用?这就是下面首先要介绍的 Object Marking Algorithms。

2 对象标记算法(Object Marking Algorithms)

下面还是先从本质一点的东西开始说吧。一个对象变得 useless 了,其实就是它目前没有称为任何一个 reference 的 target,并且认为今后也不会成为(这是从逻辑上说,实际上此刻没有被引用的对象,今后也没有人会去引用了⋯⋯)

2.1 引用计数法(Reference Counting)

核心思想:很简单。每个对象都有一个引用计数器,当在某处该对象被引用的时候,它的引用计数器就加一,引用失效就减一。引用计数器中的值一旦变为0,则该对象就成为垃圾了。但目前的 JVM 没有用这种标记方式的。为什么呢?

因为引用计数法无法解决循环引用(对象引用关系组成“有向有环图”的情况,涉及一些图论的知识,在根搜索算法中会解释)的问题。比如下面的例子:

package com.sinosuperman.jvm;

class _1MB_Data {
    public Object instance = null;
    private byte[] data = new byte[1024 * 1024 * 1];
}

public class CycledReferenceProblem {

    public static void main(String[] args) {

        _1MB_Data d1 = new _1MB_Data();
        _1MB_Data d2 = new _1MB_Data();
        d1.instance = d2;
        d2.instance = d1;

        d1 = null;
        d2 = null;

        System.gc();
    }
}

在这个程序中,首先在堆内存中创建了两个 1MB 大小的对象,并且其中分别存储的 instance 成员引用了对方。那么即使 d1和 d2 被置为 null 时,引用数并没有变为零。如果这是采用引用计数法来标记的话,内存就被浪费了,gc 的时候不会被回收。好悲催啊 :(

重复一下在《JVM 深入笔记(1)内存区域是如何划分的?》中提到的运行环境:

**Mac OS X 10.7.3**,**JDK 1.6.0   Update 29**,**Oracle Hot Spot 20.4-b02**。

那么我们来试试Oracle Hot Spot 20.4-b02是不是采用引用计数法来标记的。对了,别忘了为CycledReferenceProblem使用的虚拟机开启-XX:+PrintGCDetails参数,然后运行结果如下:

[Full GC (System) [CMS: 0K->366K(63872K), 0.0191521 secs] 3778K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0192274 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
Heap
 par new generation   total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)
  eden space 17024K,   4% used [7f3000000, 7f30aa468, 7f40a0000)
  from space 2112K,   0% used [7f40a0000, 7f40a0000, 7f42b0000)
  to   space 2112K,   0% used [7f42b0000, 7f42b0000, 7f44c0000)
 concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)
 concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)

可以看到,在Full GC时,清理掉了 (3778-366)KB=3412KB 的对象。这一共有 3MB 多,可以确定其中包括两个我们创建的 1MB 的对象吗?貌似无法确定。好吧,那下面我们使用_2M_Data对象来重复上面的程序。

package com.sinosuperman.jvm;

class _2MB_Data {
    public Object instance = null;
    private byte[] data = new byte[1024 * 1024 * 2];
}

public class CycledReferenceProblem {

    public static void main(String[] args) {

        _2MB_Data d1 = new _2MB_Data();
        _2MB_Data d2 = new _2MB_Data();
        d1.instance = d2;
        d2.instance = d1;

        d1 = null;
        d2 = null;

        System.gc();
    }
}

运行结果如下:

[Full GC (System) [CMS: 0K->366K(63872K), 0.0185981 secs] 5826K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0186886 secs] [Times: user=0.04 sys=0.00, real=0.02 secs] 
Heap
 par new generation   total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)
  eden space 17024K,   4% used [7f3000000, 7f30aa4b0, 7f40a0000)
  from space 2112K,   0% used [7f40a0000, 7f40a0000, 7f42b0000)
  to   space 2112K,   0% used [7f42b0000, 7f42b0000, 7f44c0000)
 concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)
 concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)

这次清理掉了 (5826-366)=5460KB 的对象。我们发现两次清理相差 2048KB,刚好是 2MB,也就是 d1 和 d2 刚好各相差 1MB。我想这可以确定,gc 的时候确实回收了两个循环引用的对象。如果你还不信,可以再试试 3MB、4MB,都是刚好相差 2MB。

这说明Oracle Hot Spot 20.4-b02虚拟机并不是采用引用计数方法。事实上,现在没有什么流行的 JVM 会去采用简陋而问题多多的引用计数法来标记。不过要承认,它确实简单而且大多数时候有效。

那么,这些主流的 JVM 都是使用什么标记算法的呢?

2.2. 根搜索算法(Garbage Collection Roots Tracing)

对,没错,就是“跟搜索算法”。我来介绍以下吧。

2.2.1 基本思想

其实思路也很简单(算法领域,除了红黑树、KMP等等比较复杂外,大多数思路都很简单),可以概括为如下几步:

  1. 选定一些对象,作为 GC Roots,组成基对象集(这个词是我自己造的,与其他文献资料的说法可能不一样。但这无所谓,名字只是个代号,理解算法内涵才是根本);
  2. 由基对象集内的对象出发,搜索所有可达的对象;
  3. 其余的不可达的对象,就是可以被回收的对象。

这里的“可达”与“不可达”与图论中的定义一样,所有的对象被看做点,引用被看做有向连接,整个引用关系就是一个有向图。在“引用计数法”中提到的循环引用,其实就是有向图中有环的情况,即构成“有向有环图”。引用计数法不适用于“有向有环图”,而根搜索算法适用于所有“有向图”,包括有环的和无环的。那么是如何解决的呢?

2.2.2 GC Roots

如果你的逻辑思维够清晰,你会说“一定与选取基对象集的方法有关”。是的,没错。选取 GC Roots 组成基对象集,其实就是选取如下这些对象:

《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》一书中提到的 GC Roots 为:

  1. 方法区(Method Area,即 Non-Heap)中的类的 static 成员引用的对象,和 final 成员引用的对象;
  2. Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象;
  3. 原生方法栈(Native Method Stack)中 JNI 中引用的对象。

但显然不够全面,[参考2]中提到的要更全面:(March 6th,2012 update

  1. 由系统类加载器加载的类相应的对象:这些类永远不会被卸载,且这些类创建的对象都是 static 的。注意用户使用的类加载器加载的类创建的对象,不属于 GC Roots,除非是 java.lang.Class 的相应实例有可能会称为其他类的 GC Roots。
  2. 正在运行的线程。
  3. Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象。
  4. 原生方法栈(Native Method Stack)的局部变量表(Local Variable Table)中引用的对象。
  5. JNI 中引用的对象。
  6. 同步监控器使用的对象。
  7. 由 JVM 的 GC 控制的对象:这些对象是用于 JVM 内部的,是实现相关的。一般情况下,可能包括系统类加载器(注意与“1”不一样,“1”中是 objects created by the classes loaded by system class loaders,这里是 the objects, corresponding instances of system class loaders)、JVM 内部的一些重要的异常类的对象、异常句柄的预分配对象和在类加载过程中自定义的类加载器。不幸的是,JVM 并不提供这些对象的任何额外的详细信息。因此这些实现相关的内容,需要依靠分析来判定。

所以这个算法实施起来有两部分,第一部分就是到 JVM 的几个内存区域中“找对象”,第二部分就是运用图论算法。

3. 废话

JVM 的标记算法并不是 JVM 垃圾回收策略中最重要的。真正的核心,是回收算法,当然标记算法是基础。如果你想复习一下前两篇文章,链接在这里:

JVM 深入笔记(1)内存区域是如何划分的?

JVM 深入笔记(2)各内存区溢出场景模拟

JVM 深入笔记(3)垃圾标记算法

参考

  1. http://www.yourkit.com/docs/10/help/gc_roots.jsp
  2. 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》周志明(著),机械工业出版社

-

如果这篇文章帮助到了您,欢迎您到我的博客留言,我会很高兴的。

转载请注明来自“柳大的CSDN博客”:blog.csdn.net/Poechant

-

分享到:
评论

相关推荐

    马士兵jvm调优笔记.docx

    ### 马士兵JVM调优笔记知识点梳理 #### 一、Java内存结构 Java程序运行时,其内存被划分为几个不同的区域,包括堆内存(Heap)、方法区(Method Area)、栈(Stack)、程序计数器(Program Counter Register)以及...

    马老师jvm调优实战笔记

    本笔记是马老师的 JVM 调优实战笔记,涵盖了 JVM 的概述、内存结构、堆内存、垃圾回收算法、JVM 参数等方面的内容。 JVM 调优目录 JVM 调优是 JavaVirtual Machine(Java 虚拟机)的优化过程,目的是为了提高 Java...

    JVM学习笔记

    ### JVM学习笔记 #### JVM内存模型 (JMM) JVM内存模型主要分为以下几个部分: - **Java堆**:这是所有线程共享的一块区域,在虚拟机启动时创建。主要用于存放对象实例,几乎所有的对象实例都在这里分配内存。 - *...

    jvm学习笔记.zip

    《深入理解JVM:垃圾回收与优化》 在Java编程领域,JVM(Java Virtual Machine)扮演着至关重要的角色,它是Java程序运行的基础。本文将深入探讨JVM的内存管理,特别是垃圾回收机制,以及相关的优化策略。 首先,...

    JVM笔记(阳哥).zip

    《JVM笔记(阳哥)》是一份深入探讨Java虚拟机(JVM)的资料,由阳哥精心整理。这份笔记涵盖了JVM的基础概念、内存管理、类加载机制、性能优化等多个方面,对于理解Java程序的运行机制以及提升开发效率具有重要的...

    狂神说JVM探究.rar

    这份资料出自B站上的【狂神说Java】系列教程,为快速入门JVM提供了详实的笔记。以下是根据这些资源可能包含的一些关键知识点的详细解析: 1. **JVM概述**: - JVM是Java平台的核心组成部分,它是一个运行Java字节...

    JVM历史发展和内存回收笔记

    Java虚拟机(JVM)是Java程序运行的基础,它的历史发展和内存回收机制是Java开发者必须深入了解的关键领域。本文将详细探讨JVM的发展历程以及内存管理中的垃圾回收机制。 一、JVM的历史发展 1. **早期阶段**:1995...

    JVM工作原理学习笔记

    JVM使用不同算法对堆内存进行垃圾回收,如新生代的复制算法、老年代的标记-整理算法或标记-清除算法。垃圾回收的目标是回收不再使用的对象,以释放内存空间。此外,JVM还提供了垃圾收集器,如Serial、Parallel、CMS...

    JVM 学习笔记(Java虚拟机)

    - 垃圾收集算法:标记-清除、复制、标记-整理和分代收集。 - 内存晋升策略:对象在新生代经过多次 Minor GC 后晋升到老年代。 4. **类加载机制** - 加载:找到并加载类文件到JVM中。 - 验证:确保字节码的安全...

    JVM学习资料+笔记

    4. 垃圾收集:JVM的自动内存管理关键在于垃圾收集,包括可达性分析、标记-清除、复制、标记-整理、分代收集等算法,以及新生代、老年代、永久代(或元空间)等区域划分。 5. 类型系统:JVM支持基本类型、引用类型...

    jvm学习笔记

    常见的垃圾收集算法有标记-清除、复制、标记-整理和分代收集等。JVM提供了多种垃圾收集器,如Serial、Parallel、CMS、G1等,选择合适的垃圾收集器对系统性能至关重要。 五、内存溢出与性能优化 内存溢出(Out of ...

    ImagesForJVM-JVM笔记图片

    常见的垃圾收集算法有标记-清除、复制、标记-整理和分代收集等。 4. **运行时数据区**:JVM内存可以分为新生代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation 或 Metaspace),...

    JVM学习笔记核心知识点整理

    本文将围绕JVM的核心知识点进行深入探讨,主要包括类文件加载机制、运行时数据、JVM内存模型、GC算法以及垃圾收集器分类等内容。 #### 二、基础知识梳理 ##### 1. Java与JVM简介 - **Java**: 是一门面向对象的...

    Java,JVM相关笔记的代码知识

    这份JVM相关的笔记包含了深入理解JVM内部工作机制的关键代码资源,是学习和优化Java应用程序的重要参考资料。下面,我们将深入探讨Java与JVM的相关知识点。 1. **类加载机制**:JVM通过类加载器(ClassLoader)将...

    深入Java虚拟机JVM类加载学习笔记

    ### 深入Java虚拟机JVM类加载学习笔记 #### 一、Classloader机制解析 在Java虚拟机(JVM)中,类加载器(ClassLoader)是负责将类的`.class`文件加载到内存中的重要组件。理解类加载器的工作原理对于深入掌握JVM以及...

    这是一个关于JVM的笔记,可以帮助你

    这个笔记将深入探讨JVM的工作原理、内存管理、类加载机制以及性能优化等方面,帮助你更好地理解和掌握Java编程的底层运行机制。 首先,JVM的结构包括堆、栈、方法区、本地方法栈和程序计数器等组件。每个部分都有其...

    学习jvm笔记.zip

    在深入理解JVM之前,我们先要明白它的核心概念:类加载、内存管理、执行引擎、垃圾回收以及性能优化。 一、类加载机制 JVM的类加载过程包括加载、验证、准备、解析和初始化五个阶段。加载是找到.class文件并读入...

    jvm、数据结构与算法、数据库、redis的笔记

    理解JVM的工作原理,包括类加载机制、内存模型(如堆、栈、方法区等)、垃圾回收机制(如分代收集、标记-清除、复制、标记-整理和CMS等)以及性能调优(如JVM参数设置),是每个Java开发者必备的技能。通过优化JVM...

    《JVM从入门到入魔》笔记.pdf

    2:垃圾回收:垃圾确定【引用计数法、可达性分析】+垃圾收集算法【标记-清除、标记整理、复制】+垃圾收集器【Serial+PareNew+Serial Old+Paralles Old+CMS+G1】 3:JVM内存调优:JVM参数【标准参数、-X参数、-XX参数等...

Global site tag (gtag.js) - Google Analytics