JVM 深入笔记(3)垃圾标记算法
如果您还不了解 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 之后还引入了 SoftReference 和 WeakReference,前者就是那些“可死可不死的家伙”。当进行了一次内存清理(干掉“必死无疑”的家伙)后,还是不够用,就再进行一次清理,这次清理的内容就是 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)
对,没错,就是“跟搜索算法”。我来介绍以下吧。
其实思路也很简单(算法领域,除了红黑树、KMP等等比较复杂外,大多数思路都很简单),可以概括为如下几步:
- 选定一些对象,作为 GC Roots,组成基对象集(这个词是我自己造的,与其他文献资料的说法可能不一样。但这无所谓,名字只是个代号,理解算法内涵才是根本);
- 由基对象集内的对象出发,搜索所有可达的对象;
- 其余的不可达的对象,就是可以被回收的对象。
这里的“可达”与“不可达”与图论中的定义一样,所有的对象被看做点,引用被看做有向连接,整个引用关系就是一个有向图。在“引用计数法”中提到的循环引用,其实就是有向图中有环的情况,即构成“有向有环图”。引用计数法不适用于“有向有环图”,而根搜索算法适用于所有“有向图”,包括有环的和无环的。那么是如何解决的呢?
如果你的逻辑思维够清晰,你会说“一定与选取基对象集的方法有关”。是的,没错。选取 GC Roots 组成基对象集,其实就是选取如下这些对象:
- 方法区(Method Area,即 Non-Heap)中的类的 static 成员引用的对象,和 final 成员引用的对象;
- Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象;
- 原生方法栈(Native Method Stack)中 JNI 中引用的对象。
所以这个算法实施起来有两部分,第一部分就是到 JVM 的几个内存区域中“找对象”,第二部分就是运用图论算法。
3. 废话
JVM 的标记算法并不是 JVM 垃圾回收策略中最重要的。真正的核心,是回收算法,当然标记算法是基础。如果你想复习一下前两篇文章,链接在这里:
-
如果这篇文章帮助到了您,欢迎您到我的博客留言,我会很高兴的。
转载请注明来自“柳大的CSDN博客”:blog.csdn.net/Poechant
-
相关推荐
### 马士兵JVM调优笔记知识点梳理 #### 一、Java内存结构 Java程序运行时,其内存被划分为几个不同的区域,包括堆内存(Heap)、方法区(Method Area)、栈(Stack)、程序计数器(Program Counter Register)以及...
本笔记是马老师的 JVM 调优实战笔记,涵盖了 JVM 的概述、内存结构、堆内存、垃圾回收算法、JVM 参数等方面的内容。 JVM 调优目录 JVM 调优是 JavaVirtual Machine(Java 虚拟机)的优化过程,目的是为了提高 Java...
### JVM学习笔记 #### JVM内存模型 (JMM) JVM内存模型主要分为以下几个部分: - **Java堆**:这是所有线程共享的一块区域,在虚拟机启动时创建。主要用于存放对象实例,几乎所有的对象实例都在这里分配内存。 - *...
《深入理解JVM:垃圾回收与优化》 在Java编程领域,JVM(Java Virtual Machine)扮演着至关重要的角色,它是Java程序运行的基础。本文将深入探讨JVM的内存管理,特别是垃圾回收机制,以及相关的优化策略。 首先,...
《JVM笔记(阳哥)》是一份深入探讨Java虚拟机(JVM)的资料,由阳哥精心整理。这份笔记涵盖了JVM的基础概念、内存管理、类加载机制、性能优化等多个方面,对于理解Java程序的运行机制以及提升开发效率具有重要的...
这份资料出自B站上的【狂神说Java】系列教程,为快速入门JVM提供了详实的笔记。以下是根据这些资源可能包含的一些关键知识点的详细解析: 1. **JVM概述**: - JVM是Java平台的核心组成部分,它是一个运行Java字节...
Java虚拟机(JVM)是Java程序运行的基础,它的历史发展和内存回收机制是Java开发者必须深入了解的关键领域。本文将详细探讨JVM的发展历程以及内存管理中的垃圾回收机制。 一、JVM的历史发展 1. **早期阶段**:1995...
JVM使用不同算法对堆内存进行垃圾回收,如新生代的复制算法、老年代的标记-整理算法或标记-清除算法。垃圾回收的目标是回收不再使用的对象,以释放内存空间。此外,JVM还提供了垃圾收集器,如Serial、Parallel、CMS...
- 垃圾收集算法:标记-清除、复制、标记-整理和分代收集。 - 内存晋升策略:对象在新生代经过多次 Minor GC 后晋升到老年代。 4. **类加载机制** - 加载:找到并加载类文件到JVM中。 - 验证:确保字节码的安全...
4. 垃圾收集:JVM的自动内存管理关键在于垃圾收集,包括可达性分析、标记-清除、复制、标记-整理、分代收集等算法,以及新生代、老年代、永久代(或元空间)等区域划分。 5. 类型系统:JVM支持基本类型、引用类型...
常见的垃圾收集算法有标记-清除、复制、标记-整理和分代收集等。JVM提供了多种垃圾收集器,如Serial、Parallel、CMS、G1等,选择合适的垃圾收集器对系统性能至关重要。 五、内存溢出与性能优化 内存溢出(Out of ...
常见的垃圾收集算法有标记-清除、复制、标记-整理和分代收集等。 4. **运行时数据区**:JVM内存可以分为新生代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation 或 Metaspace),...
本文将围绕JVM的核心知识点进行深入探讨,主要包括类文件加载机制、运行时数据、JVM内存模型、GC算法以及垃圾收集器分类等内容。 #### 二、基础知识梳理 ##### 1. Java与JVM简介 - **Java**: 是一门面向对象的...
这份JVM相关的笔记包含了深入理解JVM内部工作机制的关键代码资源,是学习和优化Java应用程序的重要参考资料。下面,我们将深入探讨Java与JVM的相关知识点。 1. **类加载机制**:JVM通过类加载器(ClassLoader)将...
### 深入Java虚拟机JVM类加载学习笔记 #### 一、Classloader机制解析 在Java虚拟机(JVM)中,类加载器(ClassLoader)是负责将类的`.class`文件加载到内存中的重要组件。理解类加载器的工作原理对于深入掌握JVM以及...
这个笔记将深入探讨JVM的工作原理、内存管理、类加载机制以及性能优化等方面,帮助你更好地理解和掌握Java编程的底层运行机制。 首先,JVM的结构包括堆、栈、方法区、本地方法栈和程序计数器等组件。每个部分都有其...
在深入理解JVM之前,我们先要明白它的核心概念:类加载、内存管理、执行引擎、垃圾回收以及性能优化。 一、类加载机制 JVM的类加载过程包括加载、验证、准备、解析和初始化五个阶段。加载是找到.class文件并读入...
理解JVM的工作原理,包括类加载机制、内存模型(如堆、栈、方法区等)、垃圾回收机制(如分代收集、标记-清除、复制、标记-整理和CMS等)以及性能调优(如JVM参数设置),是每个Java开发者必备的技能。通过优化JVM...
2:垃圾回收:垃圾确定【引用计数法、可达性分析】+垃圾收集算法【标记-清除、标记整理、复制】+垃圾收集器【Serial+PareNew+Serial Old+Paralles Old+CMS+G1】 3:JVM内存调优:JVM参数【标准参数、-X参数、-XX参数等...