垃圾回收系列(3):CLR与JVM垃圾回收器的比较
- 格式:docx
- 大小:458.36 KB
- 文档页数:8
你必须了解的java内存管理机制(四)-垃圾回收本⽂在个⼈技术博客不同步发布,详情可亦可扫描屏幕右侧⼆维码关注个⼈公众号,公众号内有个⼈联系⽅式,等你来撩...相关链接(注:⽂章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8)1、2、3、4、前⾔ 在前⾯三篇⽂章中,对JVM的内存布局、内存分配、垃圾标记做了较多的介绍,垃圾都已经标记出来了,那剩下的就是如何⾼效的去回收啦!这篇⽂章将重点介绍如何回收旧⼿机、电脑、彩电、冰箱~啊呸(⊙o⊙)…将重点介绍⼏种垃圾回收算法、HotSpot中常⽤的垃圾收集器的主要特点和应⽤场景。
同时,这篇⽂章也是这个系列中的最后⼀篇⽂章啦!正⽂ 上⼀篇⽂章中,我们详细介绍了两种标记算法,并且对可达性分析算法做了较多的介绍。
我们也知道了HotSpot在具体实现中怎么利⽤OopMap+RememberedSet的技术做到“准确式GC”。
不管使⽤什么优化的技术,⽬标都是准确⾼效的标记回收对象!那么,为了⾼效的回收垃圾,虚拟机⼜经历了哪些技术及算法的演变和优化呢?(注:G1收集器及回收算法本⽂不涉及,因为我觉得后⾯可以单独写⼀篇⽂章来谈!)回收算法 在这⾥,我们会先介绍⼏种常⽤的回收算法,然后了解在JVM中式如何对这⼏种算法进⾏选择和优化的。
标记-清除 "标记-清除"算法分为两个阶段,“标记”和“清除”。
标记还是那个标记,在上⼀篇⽂章中已经做了较多的介绍了,JVM在执⾏完标记动作后,还在"即将回收"集合的对象将被统⼀回收。
执⾏过程如下图: 优点: 1、基于最基础的可达性分析算法,它是最基础的收集算法。
2、后续的收集算法都是基于这种思路并对其不⾜进⾏改进⽽得到的。
缺点: 1、执⾏效率不⾼。
2、由上图能看到这种回收算法会产⽣⼤量不连续内存碎⽚,如果这时候需要创建⼀个⼤对象,则⽆法进⾏分配。
复制算法 “复制”算法将内存按容量划分为⼤⼩相等的两块,每次使⽤其中的⼀块。
Java虚拟机(JVM)的内存垃圾回收机制主要涉及自动内存管理和垃圾回收两个核心功能。
自动内存管理主要是针对对象内存的回收和对象内存的分配。
JVM的堆是垃圾收集器管理的主要区域,也被称作GC堆(Garbage Collected Heap)。
大部分情况下,对象都会首先在Eden区域分配。
在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
垃圾回收是JVM自动内存管理的另一个重要方面。
主要有两种通用的垃圾回收方法:引用计数法和可达性分析算法。
引用计数法为每个对象增加一个计数器,当该对象被引用时,计数器加一,当引用失效时,计数器减一。
当计数器为零时,该对象就可以被回收了。
这种方法无法解决循环引用的问题。
可达性分析算法是通过GC Root的对象作为起始节点,通过引用向下搜索,所走过的路径称为引用链。
当对象没有任何一条引用链链接的时候,就会被认定为垃圾。
可作为GC Root的对象包括:类加载器,Thread,虚拟机栈的本地变量表,static成员,常量引用,本地方法栈的变量等等。
大部分情况下,对象都会首先在Eden区域分配,在一次新生代
垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
以上信息仅供参考,如果还想了解更多信息或遇到相关问题,建议咨询专业人士。
垃圾回收算法1.标记清除标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。
然后,在清除阶段,清除所有未被标记的对象。
适用场合:●存活对象较多的情况下比较高效●适用于年老代(即旧生代)缺点:●容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收●扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)2.复制算法从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉现在的商业虚拟机都采用这种收集算法来回收新生代。
适用场合:●存活对象较少的情况下比较高效●扫描了整个空间一次(标记存活对象并复制移动)●适用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少缺点:●需要一块儿空的内存空间●需要复制移动对象3.标记整理复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。
这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。
如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。
之后,清理边界外所有的空间。
这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
4.分代收集算法分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。
一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。
Java语言出来之前,大家都在拼命的写C或者C++的程序,而此时存在一个很大的矛盾,C++等语言创建对象要不断的去开辟空间,不用的时候有需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的allocated,然后不停的~析构。
于是,有人就提出,能不能写一段程序在实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?1960年基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时Java还没有出世呢!所以实际上GC并不是Java的专利,GC的历史远远大于Java 的历史!那究竟GC为我们做了什么操作呢?1、哪些内存需要回收?2、什么时候回收?3、如何回收?这时候有人就会疑惑了,既然GC已经为我们解决了这个矛盾,我们还需要学习GC么?当然当然是肯定的,那究竟什么时候我们还需要用到的呢?1、排查内存溢出2、排查内存泄漏3、性能调优,排查并发瓶颈我们知道,GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢?1、对象没有引用2、作用域发生未捕获异常3、程序在作用域正常执行完毕4、程序执行了System.exit()5、程序发生意外终止(被杀进程等)其实,我们最容易想到的就是当对象没有引用的时候会将这个对象标记为可回收对象,那么现在就有一个问题,是不是这个对象被赋值为null以后就一定被标记为可回收对象了呢?我们来看一个例子:package com.yhj.jvm.gc.objEscape.finalizeEscape;import com.yhj.jvm.gc.objEscape.pojo.FinalizedEscapeTestCase;/*** @Described:逃逸分析测试* @author YHJ create at 2011-12-24 下午05:08:09* @FileNmae com.yhj.jvm.gc.finalizeEscape.FinalizedEscape.java */public class FinalizedEscape {public static void main(String[]args) throws InterruptedException {System.out.println(FinalizedEscapeTestCase.caseForEscape );FinalizedEscapeTestCase.caseForEscape = new FinalizedEscap eTestCase();System.out.println(FinalizedEscapeTestCase.caseForEscape );FinalizedEscapeTestCase.caseForEscape=null;System.gc();Thread.sleep(100);System.out.println(FinalizedEscapeTestCase.caseForEscape );}}package com.yhj.jvm.gc.objEscape.pojo;/*** @Described:逃逸分析测试用例* @author YHJ create at 2011-12-24 下午05:07:05* @FileNmae com.yhj.jvm.gc.pojo.TestCaseForEscape.java*/public class FinalizedEscapeTestCase {public static FinalizedEscapeTestCase caseForEscape = null;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("哈哈,我已逃逸!");caseForEscape = this;}}程序的运行结果回事什么样子的呢?我们来看这段代码1、System.out.println(FinalizedEscapeTestCase.caseForEscape);2、FinalizedEscapeTestCase.caseForEscape = new FinalizedEscapeTestCase();3、System.out.println(FinalizedEscapeTestCase.caseForEscape);4、FinalizedEscapeTestCase.caseForEscape=null;5、System.gc();6、Thread.sleep(100);7、System.out.println(FinalizedEscapeTestCase.caseForEscape);1、当程序执行第一行是,因为这个对象没有值,结果肯定是null2、程序第二行给该对象赋值为新开辟的一个对象3、第三行打印的时候,肯定是第二行对象的hash代码4、第四行将该对象重新置为null5、第五行触发GC6、为了保证GC能够顺利执行完毕,第六行等待100毫秒7、第七行打印对应的值,回事null么?一定会是null么?我们来看一下对应的运行结果本例中打印了GC的日志,让我们看的更清晰一点,我们很清晰的看出,最后一句打印的不是null,并且子啊之前,还出现了逃逸的字样。
JVM内存区域(Java内存区域)、JVM垃圾回收机制(GC)初探⼀、JVM内存区域(Java内存区域) ⾸先区分⼀下JVM内存区域(Java内存区域)和Java内存模型(JMM)的概念。
Java线程之间的通信采⽤的是共享内存模型,这⾥提到的共享内存模型指的就是Java内存模型(简称JMM),Java内存模型(即Java Memory Model,简称JMM)本⾝是⼀种抽象的概念,并不真实存在;Java线程之间的通信由JMM控制,JMM决定⼀个线程对共享变量的写⼊何时对另⼀个线程可见。
⽽JVM内存区域,有的地⽅也称之为Java内存区域,是JVM对JMM的实现。
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,也可以理解为线程共享内存区和线程私有内存区。
接下来就具体介绍分析⼀下JVM内存区域。
Java程序是交由JVM执⾏的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。
在讨论JVM内存区域划分之前,先来看⼀下Java程序具体执⾏的过程: 从图中可以看到,JVM主要由三⼤部分组成:类加载器、执⾏引擎、运⾏时数据区。
本篇博客主要介绍运⾏时数据区部分,类加载器会后续单独在⼀篇博客中介绍。
如上图所⽰,⾸先Java源代码⽂件(.java后缀)会被Java编译器编译为字节码⽂件(.class后缀),然后由JVM中的类加载器中的defineClass()⽅法加载各个类的字节码⽂件进jvm内存中,⽣成ng.Class对象,每个类在JVM中都拥有⼀个对应的ng.Class 对象,并存放于运⾏时数据区(Runtime Data Area)的堆中,它提供了类结构信息的描述。
数组、枚举及基本Java类型(如int、double等)甚⾄void都拥有对应的Class对象。
这也就是我们常说的Java类的静态加载,即程序在运⾏时,所需要的类就必须加载好,如果编译时这个类的.class⽂件不存在,程序将编译出错⽆法运⾏;还有⼀种是动态加载是通过反射机制在运⾏时⽤Class.forName()加载字节码⽂件获得ng.Class对象并放到JVM内存中。
JVM的7种垃圾回收器(⼩结)垃圾回收算法和垃圾回收器对于JVM的垃圾回收算法有复制算法、标记清除、标记整理。
⽤阳哥的话就是:这些算法只是天上飞的理念,是⼀种⽅法论,但是真正的垃圾回收还需要有落地实现,所以垃圾回收器应运⽽⽣。
JVM回收的区域包括⽅法区和堆,jvm对于不同区域不同的特点采⽤分代收集算法,⽐如因为所有的对象都是在Eden区进⾏分配,并且⼤部分对象的存活时间都不长,都是“朝⽣⼣死”的,每次新⽣代存活的对象都不多,所以新采取复制算法;⽽jvm默认是新⽣代的对象熬过15次GC才能进⼊⽼年代,所以⽼年代的对象都是⽣命周期⽐较长的,采⽤标记清除或者标记整理算法。
那么对于这些算法的实现都有什么呢?新⽣代:serial、ParNew、Parallel⽼年代:Serial Old、Parallel Old、CMS全堆:G1并且他们的搭配组合如下:垃圾回收器jvm的垃圾回收器⼤体上的分类主要包括四种:串⾏、并⾏、并发(CMS)和G1。
串⾏垃圾回收器(Serial):它为单线程环境设计并且只使⽤⼀个线程进⾏垃圾回收,会暂停所有的⽤户线程。
所以不适合服务器环境。
并⾏垃圾回收器(Parallel):多个垃圾回收线程并⾏⼯作,此时⽤户线程是暂停的,适⽤于科学计算/⼤数据处理等弱交互场景。
并发垃圾回收器(CMS):⽤户线程和垃圾收集线程同时执⾏(不⼀定是并⾏,可能交替执⾏),不需要停顿⽤户线程。
互联⽹公司多⽤它,适⽤于对响应时间有要求的场景。
G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进⾏垃圾回收。
默认的垃圾回收器平时我们没有配置什么jvm参数,程序也能正常执⾏,那么JVM默认的垃圾回收器是什么呢?那么如何查看默认的回收器呢?有很多⽅式,这⾥简单列举⼏种:1.命令⾏⽅式:1java -XX:+PrintCommandLineFlags -version可以看到jdk8默认的是使⽤的Parallel并⾏回收器。
jvm 垃圾回收参数指标-回复JVM(Java虚拟机)是Java程序运行的基础平台,它负责解释和执行Java字节码,同时也负责内存管理。
JVM的垃圾回收器是其中一个重要的组成部分,它负责管理内存中的垃圾对象,并进行回收和释放,以防止内存泄漏和溢出。
在垃圾回收过程中,我们可以通过调整一些参数来控制回收行为和性能。
本文将介绍几个常用的JVM垃圾回收参数指标,并逐步详细回答他们的作用和影响。
1. -Xmx:此参数用于设置JVM堆内存的最大值。
堆内存是JVM所管理的Java对象的存储区域,用于分配和释放内存。
调整此参数可以改变堆内存的大小,从而影响垃圾回收的频率和效率。
较大的堆内存可以减少垃圾回收频率,但可能会增加垃圾回收的停顿时间。
较小的堆内存则会导致频繁的垃圾回收,但停顿时间相对较短。
2. -Xms:此参数用于设置JVM堆内存的初始大小。
与-Xmx相似,调整此参数也会对垃圾回收进行影响。
较大的初始堆内存可以减少垃圾回收的频率,但是在程序刚启动时,堆内存就会被完全分配,可能会增加系统的启动时间。
较小的初始堆内存则会导致频繁的垃圾回收,但启动时间相对较短。
3. -XX:NewRatio:此参数用于设置JVM新生代和老年代的比例。
JVM 的堆内存分为新生代和老年代两个区域,新生代用于存放刚刚创建的对象,而老年代用于存放存活时间较长的对象。
通过调整此参数,可以改变新生代和老年代的大小比例,从而影响对象的分配和回收行为。
较大的新生代会提高对象在新生代中的存活率,减少对象晋升到老年代的频率,但也会增加垃圾回收的时间。
相反,较小的新生代会减少对象在新生代中的存活时间,增加对象晋升到老年代的频率,但垃圾回收时间较短。
4. -XX:SurvivorRatio:此参数用于设置JVM新生代中Eden区和Survivor区的大小比例。
新生代中的内存被分为一个Eden区和两个Survivor区,用于存放对象的创建和回收过程。
JVM之GC回收算法与GC收集器GC回收算法1.标记清除算法分为标记阶段和清除阶段标记阶段:标记处可回收的对象清除阶段:将已标记的对象所占⽤的内存回收缺点:运⾏多次以后容易产⽣空间碎⽚,当需要⼀整段连续内存时虽然空间⾜够但是⽆法分配从⽽导致多次触发GC操作。
适合存活对象多,垃圾对象少的情况2.复制算法为了提⾼标记清除算法的效率,减少内存碎⽚的产⽣⽽出现的,该算法将内存空间分为两个完全相同的两部分,每次只使⽤其中的⼀部分。
分为标记阶段、复制阶段和清除阶段标记阶段:标记不可回收对象复制阶段:将标记的对象全部复制到另⼀块未使⽤的空间中清除阶段:将已标记对象所在空间全部清除缺点:虽然解决了空间碎⽚的问题,但是内存使⽤量变为了当前内存的⼀半,也会涉及到对象地址的改变,适⽤于存活对象较少的情况3.标记整理算法为了解决复制算法只能使⽤⼀半内存的情况分为标记阶段、整理阶段、清除阶段标记阶段:标记出所有存活对象整理阶段:将存活对象移到内存的⼀端清除阶段:清楚存活对象以外的空间缺点:虽然解决了空间碎⽚的问题和浪费空间的问题,但也会涉及到对象地址的改变。
4.分代收集算法根据对象存活周期的不同将内存划分为新⽣代区域和⽼年代区域,在新⽣代中的对象⽣存时间短,通常为朝⽣⼣死类型,⽼年代中的对象通常存活的时间都很长。
根据新⽣代中对象的类型采⽤改进的复制算法进⾏垃圾收集,将新⽣代分为Eden区和两个⼤⼩相等的Servior区,它们的⽐例默认为8:1,每次只使⽤Eden区和其中⼀个Servior区,垃圾收集时将未标记的对象移⼊到另⼀个Servior区。
根据⽼年代对象的类型采⽤标记整理算法。
此算法是⽬前HotSpot虚拟机中默认使⽤的算法。
GC收集器新⽣代1.Serial收集器是⼀个单线程的串⾏收集器,会出现Stop The World,即该收集器运⾏时会暂停其他所有线程。
适⽤于客户端模式下的虚拟机2.Parallel Scavenge收集器是Serial收集器的多线程版本,也就是并⾏收集器,也会出现Stop The World,即该收集器运⾏时会暂停其他所有线程。
JVM的内存管理机制详解JVM(Java Virtual Machine)是Java编程语言的基础,它允许Java应用程序在不同的操作系统上运行。
JVM负责将Java字节码翻译成机器可执行的指令,并管理Java应用程序的内存。
JVM的内存管理机制包括垃圾回收、内存分配和内存优化等方面。
下面将详细介绍JVM的内存管理机制。
1. 堆内存(Heap Memory):堆内存是JVM中最大的一块内存区域,用于存储对象实例。
我们创建的所有对象都存放在这个区域中。
堆内存由新生代和老年代组成。
新生代又分为Eden区和两个Survivor区,用于存放新创建的对象,而老年代存放存活时间较长的对象。
2. 栈内存(Stack Memory):栈内存用于存储Java方法的局部变量、方法参数和临时变量。
每个线程在执行方法的时候都会创建一个栈帧,栈帧包含了方法的局部变量和操作数栈。
栈帧的大小在方法编译时就确定了,因此栈内存的分配和回收是非常快速和高效的。
3. 方法区(Method Area):方法区用于存储已加载的类信息、常量、静态变量和编译后的代码等数据。
方法区在JVM启动时被创建,并且在JVM关闭时销毁。
方法区中存放的数据是共享的,所有线程共享同一块方法区内存。
4. 本地方法栈(Native Method Stack):本地方法栈用于存储Java应用程序调用本地方法的相关信息。
本地方法栈和栈内存的作用类似,不同之处在于本地方法栈存储的是本地方法调用相关的数据。
5. PC寄存器(Program Counter Register):PC寄存器用于存储当前线程执行的字节码指令地址。
每个线程都有独立的PC寄存器,用于控制线程的执行。
6. 垃圾回收(Garbage Collection):垃圾回收是JVM的一个重要特性,用于自动回收不再使用的对象和释放内存空间。
JVM中的垃圾回收器会定期扫描堆内存,将不再使用的对象标记为垃圾,并进行回收。
垃圾回收机制垃圾回收器GC(Garbage Collection):JAVA/.NET中的垃圾回收器。
Java是由C++发展来的。
它摈弃了C++中⼀些繁琐容易出错的东西。
其中有⼀条就是这个GC。
⽽C#⼜借鉴了JAVA。
垃圾回收的原因从计算机组成的⾓度来讲,所有的程序都是要驻留在内存中运⾏的。
⽽内存是⼀个限制因素(⼤⼩)。
除此之外,托管堆也有⼤⼩限制。
因为地址空间和存储的限制因素,托管堆要通过垃圾回收机制,来维持它的正常运作,保证对象的分配,尽可能不造成“内存溢出”。
⼤⽩话原理:我们定义变量会申请内存空间来存放变量的值,⽽内存的容量是有限的,当⼀个变量值没有⽤了(称为垃圾),就应该将其占⽤的内存给回收掉。
变量名是访问到变量的唯⼀⽅式,所以当⼀个变量值没有任何关联的变量名时,我们就⽆法访问到该变量了,该变量就是⼀个垃圾,会被程序的垃圾回收机制⾃动回收。
垃圾(Garbage)就是程序需要回收的对象,如果⼀个对象不在被直接或间接地引⽤,那么这个对象就成为了「垃圾」,它占⽤的内存需要及时地释放,否则就会引起「内存泄露」。
有些语⾔需要程序员来⼿动释放内存(回收垃圾),有些语⾔有垃圾回收机制(GC)。
本⽂就来讨论GC实现的三种基本⽅式。
其实这三种⽅式也可以⼤体归为两类:跟踪回收,引⽤计数。
美国IBM的沃森研究中⼼David F.Bacon等⼈发布的「垃圾回收统⼀理论」⼀⽂阐述了⼀个理论:任何垃圾回收的思路,⽆⾮以上两种的组合,其中⼀种的改善和进步,必然伴随着另⼀种的改善和进步。
垃圾回收的基本原理算法思路都是⼀致的:把所有对象组成⼀个集合,或可以理解为树状结构,从树根开始找,只要可以找到的都是活动对象,如果找不到,这个对象就被回收了垃圾回收算法跟踪回收跟踪回收的⽅式独⽴于程序,定期运⾏来检查垃圾,需要较长时间的中断。
标记—清除算法(Mark-Sweep)标记—清除算法是最基础的收集算法,它分为“标记”(mark)和“清除”(sweep)两个阶段:⾸先标记出所需回收的对象,在标记完成后统⼀回收掉所有被标记的对象,它的标记过程其实就是前⾯的可达性分析算法中判定垃圾对象的标记过程。
垃圾回收系列(3):CLR与JVM垃圾回收器的比较
发布者:TerryLee | 分类:.NET框架 | Java技术 | 计算机科学
本文为垃圾回收讲座的第三篇,在前面两篇(一、二)文章里介绍了手工管理内存带来的一些问题,以及一些经典的GC算法。
本文我们主要关注微软的CLR与JVM垃圾回收器方面的比较。
我们知道CLR和JVM都采用了分代式垃圾回收器,而分代式垃圾回收器则基于以下几点假设:
1. 对象越新,其生存期就越短
2. 对象越老,其生存期就越长
3. 对堆的一部分执行GC比对整个堆执行GC要快
CLR和JVM尽管都采用了分代式垃圾回收器,但是它们在很多处理方面都有些不同:分代机制,大对象堆,回收模式,回收算法,寻找存活对象效率等。
分代机制
在CLR中,对象按年龄可以分为三代:第0代、第1代、第2代,如下图所示:
在这三代之间,对象代的提升过程,大家可以参考《CLR via C#》,里面有比较详细的介绍。
JVM中对于对象的分代式新生代和旧生代:
回收模式
在CLR4.0之前,提供三种不同的垃圾回收模式:工作站并发GC、工作站非并发GC以及服务器GC,如下图所示:
工作站非并发GC模式,没有专门的GC线程,而是由工作线程负责回收,在回收过程中,需要暂时挂起应用程序,回收结束后应用程序继续运行,所以在回收过程中会有应用程序暂停现象:
工作站并发GC模式,为了解决在执行垃圾回收时引起的应用程序暂停问题,会有一个专门的GC线程负责垃圾回收,大多数时间垃圾回收都可以应用程序并发执行,但是仅仅是针对Full GC,而对于第0代、第1代对象,仍然会使用非并发模式执行,并发垃圾回收本质上牺牲了更多的CPU时间和内存来换取应用程序暂停时间的减小:
服务器GC模式运行在多CPU服务器上,如果在单CPU机器上配置了使用服务器GC,不会起任何作用,垃圾回收仍然会使用工作站非并发模式执行。
服务器GC模式为每个CPU分配一个专用的垃圾回收线程和一个托管堆,并且该垃圾回收线程具有较高的优先级,在执行垃圾回收期间,应用程序工作线程会暂时挂起:
CLR 4.0中提供了后台垃圾回收机制,用于取代并发GC。
JVM(以Hotspot为例)中使用的垃圾回收更为复杂,针对新生代、旧生代在工作站和服务器上,分别使用不同的垃圾回收模式,如下图所示:
在Client端默认的方式为串行GC,而在服务端,对于新生代和旧生代默认的方式分别为:并行回收GC和并行GC:
下图体现了默认串行GC与并行GC之前的区别,并行GC会把堆分成多个区,分区进行标记和回收,但这两种方式都会引起应用程序的暂停:
下图体现了默认的标记缩并回收与并发GC,在并发GC中,标记的总共分为三个阶段,分别为:Initial Mark、Concurrent Marking和Remark,只有在Initial Mark和Remark阶段才会引起应用程序暂停,而在Concurrent Marking和清除阶段都是与应用程序并发执行,并不会引起暂停:
回收算法
在CLR中有专门的大对象堆(LOH),超过85000字节的对象将会分配在LOH上面,只有在进行第2代对象垃圾回收时才会同时对LOH进行回收,即一次Full GC。
第0代、第1代、第2代对象所在的堆称之为小对象堆(SOH)。
在CLR中,对于小对象堆SOH,采用的回收算法为标记-缩并算法,由于移动大对象比较耗费时间,所以在LOH上,CLR采用了标记-清除算法,即只做对象的清除并不对大对象堆进行压缩。
在JVM中,对于新生代对象采用节点复制算法进行回收,这也是为什么我们在上面的图中,新生代对象堆分为S0和S1的原因,有时也称之为为From和To;对于旧生代对象根据不同的回收模式,采用不同的回收算法:
串行GC:标记-缩并算法(滑动缩并)
并行GC:标记-缩并算法
并发GC:标记-清除算法
提高查找存活对象的效率
现在考虑这样一个问题,只对第0代(或者新生代)对象做回收,在查找存活对象过程中(不关是标记-缩并还是节点复制算法),从根集合开始,如果发现有一个根对象指向了第1代、第2代(或者旧生代),为了提高效率,垃圾回收器将会立即终止这条线上的查找:
这样带来的一个问题是:如果在第0代(或者新生代)分配的对象X,没有根对象指向它,但有一个第1代或者第2代(旧生代)的对象,指向了该新创建的对象:
此时虽然没有根对象可以到达新创建的对象X,但由于有其他代的对象指向它,所以它仍然不能当做垃圾回收,为了解决这个问题,人们提出了各种解决方案,常见的有:
1. 记忆集
2. 硬件标记
3. 虚拟页面标记
4. 字标记
5. 卡标记
由于硬件标记需要特定的硬件平台来支持,所以不具有可移植性,现在更多的垃圾回收器都使用了软件解决方案,在CLR和JVM上都使用了卡标记技术,即把托管堆分成一个一个的卡片,这个卡片代表一个抽象的概念(卡片的大小可以等于一个字,也可以等于一个虚拟页面)。
如果有卡片上的对象发生了变化,则在Card Table中做一个标记,在查找存活对象时,除了要查找根对象之外,还有查找Card Table中标记的对象所引用的对象:
在CLR和JVM上所使用的卡片大小也不相同,CLR为128字节一个卡片,而JVM上为512字节一个卡片。
在更新卡片过程中,它们都是按字节(Byte)进行更新而不是按位(Bit)进行更新。
总结
虽然CLR和JVM都采用了分代式垃圾回收器,但是它们在很多处理上都有区别,不管怎样,分代式垃圾回收器的一个原则就是把对象分成不同的区(按年龄也好、按对象大小也好),以便在不同的分区上采用不同的回收算法和模式。
本系列前两篇文章链接:。