JVM垃圾收集器.docx

上传人:b****4 文档编号:11679548 上传时间:2023-03-30 格式:DOCX 页数:6 大小:119.79KB
下载 相关 举报
JVM垃圾收集器.docx_第1页
第1页 / 共6页
JVM垃圾收集器.docx_第2页
第2页 / 共6页
JVM垃圾收集器.docx_第3页
第3页 / 共6页
JVM垃圾收集器.docx_第4页
第4页 / 共6页
JVM垃圾收集器.docx_第5页
第5页 / 共6页
点击查看更多>>
下载资源
资源描述

JVM垃圾收集器.docx

《JVM垃圾收集器.docx》由会员分享,可在线阅读,更多相关《JVM垃圾收集器.docx(6页珍藏版)》请在冰豆网上搜索。

JVM垃圾收集器.docx

JVM垃圾收集器

一、垃圾收集器

在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给了JVM来处理。

顾名思义,垃圾回收就是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。

那么在Java中,什么样的对象会被认定为“垃圾”?

那么当一些对象被确定为垃圾之后,采用什么样的策略(或者说按机回收算法)来进行回收(释放空间)?

在目前的商业虚拟机中,有哪些典型的垃圾收集器?

这些只做些了解就可,因为我们程序员不需要太深入了解,而且目前已近推出JDK8.0,相应的也有JVM8.0更新文档帮助大家深入了解JVM机制。

考虑到JVM中存活对象的生命周期具有两极化,大部分Java对象生命周期很短暂,有的对象生命周期很长,甚至与JVM周期一致,因此应该采用不同的垃圾手机策略,分代收集由此诞生。

几乎目前所有的GC都是采用的分代收集算法执行垃圾回收,所以Java堆区如果进一步细分,可分为新生代(Young)、老年代(Old),新生代(Young)又被划分为三个区域:

Eden、FromSurvivor、To Survivor。

分代收集算法:

采用不同算法处理[存放和回收]Java瞬时对象和长久对象。

大部分Java对象都是瞬时对象,朝生夕灭,存活很短暂,通常存放在Young新生代,采用复制算法对新生代进行垃圾回收。

老年代对象的生命周期一般都比较长,极端情况下会和JVM生命周期保持一致;通常采用标记-压缩算法对老年代进行垃圾回收。

这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。

相对于年老代,通常年轻代要小很多,回收的频率高,速度快。

年老代则回收频率低,耗时长。

内存在年轻代里面分配,年轻代里面的对象经过多个回收周期依然存活的会自动晋升到年老代。

内存空间究竟该如何划分,完全依赖于GC的设计。

当内存空间划分完成后,GC就可以为新对象分配内存空间,并区分出存储在内存中的对象哪些是存活的,哪些已经死亡,如果对象已经死亡,那么就可以将其标记为垃圾。

为了避免内存溢出,GC就会释放掉无用对象所占用的内存空间,便于有足够的可用内存空间分配给新的对象实例。

大家思考一下,被标记为垃圾的无用对象所占用的内存空间何时进行回收?

一般来说,当内存空间中的内存消耗达到了一定阈值的时候,GC就会执行垃圾回收,而且回收算法必须非常精准,一定不能造成内存中存活的对象被错误的回收,也不能造成已死亡的对象没有被及时的回收掉。

而且GC执行内存回收的时候应该做到高效,不应该导致应用程序出现长时间的暂停,以及避免产生内存碎片。

不过当GC执行垃圾回收时,不可避免的会产生一些内部碎片,因为被回收的内存空间极有可能是一些不连续的内存块,这将导致没有足够的连续可用内存分配给较大对象,不过可以使用压缩算法消耗内存碎片(后面会讲)。

在很多情况下,GC不应该成为影响系统性能的瓶颈,所以参考《HotSpot内存管理白皮书》的描述来看,可以根据一下6点来评估一款GC的性能:

1.吞吐量:

程序运行时间/(程序运行时间+内存回收时间)

2.垃圾收集开销:

吞吐量的补数

3.暂停时间:

执行垃圾收集时,程序的工作线程被暂停的时间

4.收集频率:

收集操作发生的频率

5.堆空间:

Java堆区所占内存大小

6.快速:

一个Java对象从诞生到被回收所经历的时间

二、垃圾标记算法

在GC执行垃圾回收之前,首先需要区分出内存空间哪些对象是存活对象,哪些是已经死亡了的,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们称之为垃圾标记阶段。

那么在JVM中究竟是如何标记一个死亡对象的呢?

简单来说,当一个对象已经不再被任何存活的对象继续引用时,就可以宣判已经死亡。

目前有两种比较常见的垃圾标记算法,分别是引用计数算法和根搜索算法。

1.引用计数方法

计数方法比较简单,对于每一个对象,都创建一个私有的引用计数器,当目标对象被其他存活对象引用时,就将计数器加1,取消一次引用,就将引用计数减1,如果一个对象的计数器是0,则该对象没有被引用,则可以标记为垃圾。

使用引用计数做垃圾收集的算法的一个优点是实现很简单,与其他垃圾收集算法相比还有个特点是它的垃圾收集过程不会造成程序暂停(这个后面会提到)。

因为计数的递增和递减是在程序运行过程中进行的,当一发现某个对象的计数为0马上可以回收掉。

但是这种算法,有一个缺点,就是对循环引用没有办法处理;比如现在A对象引用B对象,B对象的计数器加1,然后B引用C,C的计数加1,后来C又引用B,B的计数加1得到2。

假如现在A不再引用B了,B的计数器成为1。

而由于B、C互相引用,形成一个孤岛,但是计数器又没有变成0,又无法回收。

这个问题在面向对象这类语言里更加严重,因为环形引用在面向对象里是很普遍的现象。

除此之外,使用引用计数实现的垃圾收集方式还会将内存管理的代码和其他代码(比如一个引用更新的时候就要更新计数器)搅混在一起,这跟软件工程所提倡的模块化的原则相违背。

也就是如果两个对象相互引用时,引用计数器中的值则永远不会为0,则这两个对象永远都不会被清除,所占内存空间无法释放,极可能引发内存泄漏。

2.根搜索算法

根搜索算法,可以解决对象相互引用的问题,也是现在广泛采用的标记对象死活的算法,它是以一系列的GCRoots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(ReferenceChain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。

如下所示,对象object5、object6、object7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为是可回收的对象。

 在Java语言里,可作为GCRoots的对象包括下面几种:

    虚拟机栈(栈帧中的本地变量表)中的引用的对象。

    方法区中的类静态属性引用的对象。

    方法区中的常量引用的对象。

    本地方法栈中JNI(即一般说的Native方法)的引用的对象。

与一个类对应的唯一数据类型的Class对象。

在根搜索算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

如果对象在进行根搜索后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

3、垃圾回收算法

当成功区分出内存中的存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收释放无用的对象所占用的内存,以便有足够的内存空间为新的对象分配内存。

由于执行垃圾回收的算法相当多,这里只列举目前JVM中比较常见的三种垃圾回收算法。

1.标记-清除算法(Mark-Sweep)

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经基本介绍过了。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:

一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

在开始的时候内存可能是按顺序分配的,然后经过几次垃圾回收后,这块连续的内存空间中有的对象变成了垃圾,被回收了,而有的还是存活的对象。

这样这块内存中就会出现很多小内存块。

内存碎片是非常有害的,有可能空闲内存还很多,但都是不够大的碎片,会造成下一次分配时因没有任何一个“洞”可以装得下这个对象,抛出outofmemory的异常(OOM)。

除此之外,这些碎片还会破坏程序的空间的局部性。

这样另一种算法就出现了----标记-压缩算法。

标记-清除算法的执行过程如下图所示。

        

 

2.复制算法(Copying)

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块:

From,To。

开始的时候我们只在From里分配,当From分配满的时候出发垃圾收集,这个时候会找出From空间里所有的存活对象,然后将这些存活的对象拷贝到To空间里。

这样From空间里剩下的就都全是垃圾,而且对象拷贝到To里,在To里是紧凑排列的。

这个事儿做完了之后From和To的角色就转变了一下。

原来的From变成了To,原来的To变成了现在的From。

现在又可以在这个完全是空的From里分配了。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

堆的利用率只有一半了,这对那些内存占用率比较低的对象还算好,如果随着应用的内存占用率的增高,问题就出现了,第一个要拷贝的对象太多了,还有可能无法回收内存了。

复制算法的执行过程如图3-3所示。

        

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。

当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)。

内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。

内存的分配担保也一样,如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

关于对新生代进行分配担保的内容,本章稍后在讲解垃圾收集器执行规则时还会再详细讲解。

 

3.标记-压缩算法(Mark-Compact)

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。

更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

而标记--清除算法会产生内部碎片,所以JVM的设计者们在此基础上进行了改进,标记--压缩算法由此诞生,被应用于老年代内存回收。

标记--压缩算法的标记阶段和标记--清除算法的标记阶段是一致的,就不再重复。

使用标记--压缩算法时,标记完可达对象之后,我们不再遍历所有对象清扫垃圾了,我们只需要将所有存活对象向“左”靠齐,让不连续的空间变成连续的,这样就没有内存碎片了。

不仅如此,因为不再连续的空间变成连续的,内存分配也更快速了。

对于标记--清除算法来说,因为内存中有碎片,空闲内存不再连续,为了分配内存,系统内可能要维护着一个空闲内存空间的链表。

当需要分配内存时,会遍历这个链表,找到一个够大的内存块,然后将其分成两份,一份用作当前的分配,另一份放回链表(这样有造成更多的内存碎片,也有一些策略并不是按顺序查找,找到够大的就好,有可能是找到一个更好的空闲内存块为止)。

而对于标记--压缩算法,内存空间是连续的,我们只需要一个指针标记出下一次分配工作要从哪里开始就可以了,分配后将指针递增所分配对象的大小,这个工作是非常快速的,而且不用维护那个空间内存链表了。

这样一看好像标记--压缩算法绝对的优于标记--清除算法,那标记--清除还有啥存在的必要了呢?

不过要记住的一点是标记--压缩算法为了达到压缩的目的,是需要移动对象的,这会有性能消耗的,这样所有对象的引用都必须更新。

看来有利必有弊。

       

参考资料:

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 幼儿教育 > 唐诗宋词

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1