详解Java 虚拟机垃圾收集机制

1 垃圾收集发生的区域

之前我们介绍过 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程共存亡。栈中的每一个栈帧分配多少内存基本上在类结构确定下来时就已知,因此这几个区域的内存分配和回收都具有确定性,不需要考虑如何回收的问题,当方法结束或线程结束,内存自然也跟着回收了

而 Java 堆和方法区这两个区域则有显著的不确定性,只有在程序运行时我们才能知道程序究竟创建了哪些对象,创建了多少对象,所以这部分内存的分配和回收是动态的,垃圾收集器所关注的正是这部分内存该如何管理

2 如何判定需要被回收的对象?

如果一个对象没有被其他对象引用,则证明这个对象可以被回收,因为它已经没有实际用途了。那我们怎么去判断一个对象是否可回收呢?业界主要有两种判断方式:

1. 引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加一;当引用失效,计数器值减一;任何时刻计数器值都为零的对象就是不可能再被使用了。这种方法虽然会占用额外的内存空间用于计数,但它的原理简单,判定效率也高,大多数情况下它都是一个不错的算法。然而,这个看似简单的算法却需要考虑很多额外情况,否则将无法保证其正确工作,例如单纯的引用计数法就很难解决对象之间相互循环引用的问题

2. 可达性分析算法
该算法的基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链。如果某个对象到 GC Roots 间没有任何引用链相连,则证明此对象是不可能再被使用,可以回收

在 Java 技术体系中,可以作为 GC Roots 的对象包括:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(NullPointException、OutOfMemoryError)
  • 所有被同步锁持有的对象
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

除了这些固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域的不同,还可以有其他对象临时加入,共同构成完整的 GC Roots 集合

3 四种引用类型

无论是通过引用计数法还是可达性分析算法,判断对象是否存活都和引用离不开关系。在 JDK1.2 以前,Java 里的引用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用。这种定义当然没有什么不对,但现在看来显得太狭隘了,比如我们希望描述一类对象:当内存空间足够时,能保留在内存中,如果内存空间在进行了垃圾收集后仍然紧张,则可以抛弃这些对象,很多系统的缓存功能都符合这样的应用场景

JDK1.2 对引用的概念作了补充,将引用分为强引用(Strongly Reference)、软引用(SoftReference)、弱引用(Weak Reference)和虚引用(Phantom Reference),强度依次减弱

  • 强引用

形如 Object obj = new Object() 这种引用关系就是我们常说的强引用。无论什么情况,只要强引用关系存在,对象就永远不会被回收

  • 软引用

用来描述一些有用但非必须的对象。此类对象只有在进行一次垃圾收集仍然没有足够内存时,才会在第二次垃圾收集时被回收。JDK1.2 之后提供了 SoftReference 类来实现软引用

  • 弱引用

也是用来描述那些非必须对象,但它的强度比软引用更弱一些。被软引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。JDK1.2 之后提供了 WeakReference 类来实现软引用

  • 虚引用

最弱的一种引用关系,一个对象是否存在虚引用,丝毫不会对其生存时间造成任何影响,也无法通过虚引用来取得一个对象实例。设置虚引用关联的唯一目的就是让这个对象被回收时能收到一个系统通知。JDK1.2 之后提供了 PhantomReference 类来实现软引用

4 finalize() 方法

在可达性分析中被判定为不可达的对象,并不是立即赴死,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与 GC Root 相连接的引用链,那么它将被第一次标记,随后再进行一次筛选,筛选条件是对象是否有必要执行 finalize() 方法,如果对象没有覆盖 finalize() 方法或是 finalize() 方法已经被调用过,则都视为“没有必要执行”

如果对象被判定为有必要执行 finalize() 方法,那么该对象将会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动创建的、低调度优先级的 Finalizer 线程去执行它们的 finalize() 方法。注意这里所说的执行是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是防止某个对象的 finalize() 方法执行缓慢,或者发生死循环,导致 F-Queue 队列中的其他对象永久处于等待状态

finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对 F-Queue 中的对象进行第二次小规模标记,如果对象希望在 finalize() 方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上就真的要被回收了

任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法将不会再执行。finalize() 方法运行代价高,不确定性大,无法保证各个对象的调用顺序,因此已被官方明确声明为不推荐使用的语法

5 回收方法区

方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型。判定一个常量是否废弃相对简单,与对象类似,只要某个常量不再被引用,就会被清理。而判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了,需要同时满足下面三个条件:

  • 该类的所有实例都已经被回收,即 Java 堆中不存在该类及其任何派生子类的实例
  • 加载该类的类加载器已经被回收
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

Java 虚拟机允许对满足上述三个条件的无用类进行回收,但并不是说必然被回收,仅仅是允许而已。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制

6 分代收集理论

当前商业虚拟机的垃圾收集器大多数都遵循了“分代收集”的设计理论,分代收集理论其实是一套符合大多数程序运行实际情况的经验法则,主要建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

这两个分代假说共同奠定了多款常用垃圾收集器的一致设计原则:收集器应该将 Java 堆划分出不同的区域,将回收对象依据年龄(即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储,把存活时间短的对象集中在一起,每次回收只关注如何保留少量存活的对象,即新生代(Young Generation);把难以消亡的对象集中在一起,虚拟机就可以使用较低的频率来回收这个区域,即老年代(Old Generation)

正因为划出了不同的区域,垃圾收集器才可以每次只回收其中一个或多个区域,因此才有了“Minor GC”、“Major GC”、“Full GC”这样的回收类型划分,也才能够针对不同的区域采用不同的垃圾收集算法,因而有了“标记-复制”算法、“标记-清除”算法、“标记-整理”算法

分代收集并非只是简单划分一下内存区域,它至少存在一个明显的困难:对象之间不是孤立的,对象之间会存在跨代引用。假如现在要进行只局限于新生代的垃圾收集,根据前面可达性分析的知识,与 GC Roots 之间不存在引用链即为可回收,但新生代的对象很有可能会被老年代所引用,那么老年代对象将临时加入 GC Roots 集合中,我们不得不再额外遍历整个老年代中的所有对象来确保可达性分析结果的正确性,这无疑为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:

  • 跨代引用假说:跨代引用相对于同代引用仅占少数

存在互相引用的两个对象,应该是倾向于同时生存或同时消亡的,举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,会使得新生代对象同样在收集时得以存活,进而年龄增长后晋升到老年代,那么跨代引用也随之消除了。既然跨带引用只是少数,那么就没必要去扫描整个老年代,也不必专门记录每一个对象是否存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,称为记忆集(Remembered Set),这个结构把老年代划分为若干个小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入 GC Roots 进行扫描

7 标记 - 清除算法
如其名,算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成之后,统一回收所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。标记 - 清除算法执行过程如图所示:

标记 - 清除算法是最基础的算法,后续的收集算法都是以标记 - 清除算法为基础,对其缺点进行改进,它的主要缺点有两个:

  • 执行效率不稳定

如果 Java 堆中包含大量对象且大部分需要回收,则必须进行大量标记和清除的动作‘

  • 内存空间碎片化问题

标记、清除之后会产生大量不连续的内存碎片,内存碎片太多会导致下次分配较大对象时无法找到足够的连续内存,从而不得不提前触发一次垃圾收集动作

8 标记 - 复制算法

为了解决标记 - 清除算法面对大量可回收对象时执行效率低的问题,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一内存上,再把已使用过的内存空间一次清理掉

如果内存中多数对象都是存活的,这种算法无疑会产生大量内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也不用考虑空间碎片的问题,只要移动堆顶指针,按顺序分配即可。不过这种算法的缺陷也显而易见,可用内存被缩小为原来的一半

标记 - 复制算法大多用于新生代。实际上,新生代中的对象大多数都熬不过第一轮收集,因此不需要按 1:1 的比例来划分新生代的内存空间。具体做法是将新生代划分为一块较大的 Eden 区和两块较小的 Survivor 区,每次分配只使用 Eden 区和其中一块 Survivor 区。发生垃圾收集时,将 Eden 区和 Survivor 区中仍然存活的对象一次性复制到另一个 Survivor 区,然后直接清理掉 Eden 区和已经用过的 Survivor 区。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1:1

当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时,就需要依赖其他内存区域(大多是老年代)进行分配担保,上一次新生代存活下来的对象直接进入老年代

9 标记 - 整理算法

标记 - 复制算法不适合用在对象存活率高的区域,而且会浪费一半的空间,因此老年代一般不采用这种算法,取而代之的是有针对性的标记 - 整理算法。标记 - 整理算法的标记过程与标记 - 清除算法一样,但后续步骤不是直接清理可回收对象,而是让所有存活对象都向内存空间的一侧移动,然后直接清理掉边界以外的内存

是否移动回收后的存活对象是一项优缺点并存的风险决策,尤其是在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新其引用将会是一个极为繁重的操作,必须暂停用户应用程序线程才能进行,像这样的停顿行为被称为“Stop the World”。但如果不考虑移动存活对象,又会影响内存分配和访问的效率,为此使用者必须小心权衡其中的得失。一种和稀泥式的解决方案就是让虚拟机平时采用标记 - 清除算法,直到内存空间碎片化程度大到影响对象分配时,再采用标记 - 整理算法收集一次,已获得规整的内存空间

以上就是详解Java 虚拟机垃圾收集机制的详细内容,更多关于java 虚拟机垃圾收集机制的资料请关注我们其它相关文章!

(0)

相关推荐

  • java虚拟机创建失败的原因整理

    创建java虚拟机失败的解决方法 解决问题的步骤: 1.从eclipse文件夹中打开eclipse.ini文件 2.修改–launcher.XXMaxPermSize属性 3.将值改为512m即可 配置文件格式: -startup plugins/org.eclipse.equinox.launcher_1.3.0.v20120522-1813.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_

  • java虚拟机学习高级篇

    还是继续说一下java虚拟机,为什么呢?因为我随意翻着别人的博客一不小心看到有关jvm的一点新的东西,挺有趣的,就按照我的理解分享一下: 还记得以前学过一首诗,"看成岭侧成峰,远近高低各不同",这一句诗的内在含义有的时候真的会让你猛然惊醒,进而如获至宝!的确,有的时候换一个角度看问题,你会发现不一样的世界. 我们平常学java的时候肯定涉及到了进程,多线程的概念,但是有没有想过操作系统也有进程和线程的概念,两者有关系吗?假如我们视角放高一点,以操作系统的角度看看一个java程序的运行,

  • Java虚拟机常见内存溢出错误汇总

    一.引言 从事java开发的小伙伴在平时的开发工作中,应该会遇见各式各样的异常和错误,在实际工作中积累的异常或者错误越多,趟过的坑越多,就会使我们编码更加的健壮,就会本能地避开很多严重的坑.以下介绍几个Java虚拟机常见内存溢出错误.以此警示,避免生产血案. 二.模拟Java虚拟机常见内存溢出错误 1.内存溢出之栈溢出错误 package com.jayway.oom; /** * 栈溢出错误 * 虚拟机参数:-Xms10m -Xmx10m * 抛出异常:Exception in thread

  • 老生常谈Java虚拟机垃圾回收机制(必看篇)

    在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中.如果在Java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致OOM. JVM提供了一种垃圾回收机制,简称GC机制.通过GC机制,能够在运行过程中将堆中的垃圾对象不断回收,从而保证程序的正常运行. 垃圾对象的判定 我们都知道,所谓"垃圾"对象,就是指我们在程序的运行过程中不再有用的对象,即不再存活的对象.那么怎么来判断堆中的对象是"垃圾&q

  • Java虚拟机内存溢出与内存泄漏

    一.基本概念 内存溢出:简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出. 内存泄漏:内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏. 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory: 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间

  • java命令调用虚拟机方法总结

    java命令调用虚拟机 java的虚拟机调用,按住Win+r命名,如图所示: 继续点击确定按钮,如图所示: 可以看到后台命令,如图所示: 调用虚拟机编译Test.java代码:如图所示: Test.java可以看到在E盘JavaTest文件夹下,,如图所示: 回到命令后台,输入:E: 按回车键,然后在输入:cd JavaTest,按回车键, 然后输入javac Test.java,按回车键,这个是调用虚拟机编程的java代码, 最后输入:java Test,按回车键,可以看到后台输出:Hello

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • 详解java中jvm虚拟机栈的作用

    jvm虚拟机栈的作用 jvm虚拟机栈栈帧的组成 jvm虚拟机栈,也叫java栈,它由一个个的栈帧组成,而栈帖由以下几个部分组成 局部变量表-存储方法参数,内部使用的变量 操作数栈-在变量进行存储时,需要进行入栈和出栈 动态连接-引用类型的指针 方法出口-方法的返回 一段原程序代码 package com.lind.basic; public class Demo1 { static int hello() { int a = 1; int b = 2; int c = a + b; return

  • Java虚拟机使用jvisualvm工具远程监控tomcat内存

    jdk中自带了很多工具可以用于性能分析,位于jdk的bin目录下,jvisualvm工具可以以图形化的方式更加直观的监控本地以及远程的java进程的内存占用,线程状态等信息. 一.配置tomcat 在tomcat的catalina.sh文件开头加上如下配置: JAVA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=fa

  • java虚拟机多线程进阶篇总结

    1.线程池基本参数 以Executors.newFixedThreadPool()这种创建方式为例: 大家想象,假如你创建一个线程池,你想这个池子有些什么参数呢?首先这个池子必须要有一个最大值:然后还希望这个池子的线程数量有一个警戒线,到了这个警戒线的位置说明线程池暂时已经满了,如果这个时候还有人过来拿线程,我们就要把这些人抓起来扔到一个地方去让他们排队,告诉他们:请稍等,等我们的线程有空闲的时候再来处理你的事:再然后假如人排队的地方都满了,玛德,好多人,于是线程池就想办法东拼西凑又多搞出来了几

随机推荐