java垃圾回收_Allione_新浪博客

本文深入探讨了Java垃圾回收机制的核心原理与技术细节,包括对象存活判断标准、垃圾收集算法、收集器工作原理及其选择策略等内容。

目录:判断对象是否存活;垃圾收集算法;垃圾收集时机;垃圾收集器​G1

垃圾收集GC(Garbage Collection)是Java语言的核心技术之一,之前我们曾专门探讨过Java 7新增的垃圾回收器G1的新特性,但在JVM的内部运行机制上看,Java的垃圾回收原理与机制并未改变。垃圾收集的目的在于清除不再使用的对象。GC通过确定对象是否被活动对象引用来确定是否收集该对象。GC首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数算法可达性分析算法(对象引用遍历)。

1、判断对象是否可被收集​

​(1)引用计数收集算法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。

缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

(2)可达性分析算法

​在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。

  

(3)finalize()方法最终判定对象是否存活

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1).第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

  2).第二次标记。  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

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

​大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就量正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。

2、垃圾收集算法

​(1)标记-清除(Mark-Sweep)

这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。并且,由于它只是清除了那些未标记的对象,而并没有对标记对象进行压缩,导致会产生大量内存碎片,从而浪费内存。

(2)标记-压缩(Mark-Compact)

有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

​(3)复制算法 (Copying)

 将现有的内存空间分为两快,每次只使用其中一块,当其中一块时候完的时候,就将还存活的对象复制到另外一块上去,再把已使用过的内存空间一次清理掉。

优点:1).由于是每次都对整个半区进行内存回收,内存分配时不必考虑内存碎片问题。

          2).只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

 缺点:1).内存减少为原来的一半,太浪费了。

           2).对象存活率较高的时候就要执行较多的复制操作,效率变低。

           3).如果不使用50%的对分策略,老年代需要考虑的空间担保策略。

(4)分代收集算法 (Generational Collecting)

        当前的商业虚拟机的垃圾收集都采用,把Java堆分为新生代和老年代。根据各个年代的特点采用最适当的收集算法。

    在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用:复制算法

    在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-整理”算法来进行回收。

3、垃圾收集时机​

​(1)对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

​(2)大对象直接进入老年代

    所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

​(3)长期存活的对象将进入老年代

    虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。 为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

 新创建的对象会分配到Young的Eden上,如果Eden满了就触发MinorGC,将Eden区中存活的对象保存到Survivor的一个区中,同时将Survivor另一个区存活的对象也保存到Survivor这个区中,Survivor区始终有一个区是空的。

survivor的一个区满了之后,会将对象直接放到Old区,Old区满了会触发FullGC,将回收整个堆空间。

perm区主要保存class对象(类信息),垃圾回收也是FullGC触发。 

Java 中的也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。

  1).Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。

    新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。

    当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域,一般回收速度也比较快。

  2).Major GC / Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

    现实的生活中,老年代的人通常会比新生代的人 "早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。MajorGC的速度一般会比Minor GC慢10倍以上。

    出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。

    另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

4、垃圾收集器​

两个收集器之间存在连线,就说明它们可以搭配使用。

--------------------------新生代收集器------------------------

1、Serial收集器:最基本、发展历史最悠久的收集器

适用:看上去没什么用,但实际上到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器

 特点1).单线程的收集器,说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作            2).在它进行垃圾收集时,必须暂停其他所有的工作线程(Sun将这件事情称之为“Stop The World”),直到它收集结束。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的。

  收集算法:采用复制算法   

  优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

  缺点:GC时暂停线程带给用户不良体验

  搭配:CMS 或Serial Old(MSC)  

2、ParNew收集器

  ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器完全一样,实现上这两种收集器也共用了相当多的代码。

  适用:运行在Server模式下的虚拟机中的新生代

  特点 1).多线程GC(并行):ParNew是Serial的多线程版本,两者共用了许多代码。

           2).在GC时暂停所有用户线程

  算法:采用复制算法

  优点:高效

  缺点:GC时暂停线程带给用户不良体验,单线程下效果不一定优于Serial

  搭配:CMS 或Serial Old(MSC) 

    ParNew收集器有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。在JDK 1.5以后使用CMS来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。

3、Parallel Scavenge收集器

  适用:新生代收集器,在后台运算而不需要太多交互的任务。

  特点: 1.多线程GC(并行)       2.在GC时暂停所有用户线程

  与其他收集器的不同:

    1).ParNew,CMS等收集器的关注点在于尽可能缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

    吞吐量:运行代码时间/(运行用户代码时间+垃圾收集时间)

    停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    2).Parallel Scavenge可采用GC自适应的调节策略(这是与另外两一个重要的区别)

  参数:用于精确控制吞吐量 

    -XX:MaxGCPauseMillis 最大垃圾收集停顿时间

    -XX:GCTimeRatio  垃圾收集时间与运行用户代码时间的比例=垃圾收集时间/运行用户代码时间,相当于是吞吐量的倒数。                                                     

  实现:降低GC停顿时间:牺牲吞吐量和新生代空间(减小新生代空间,GC频率变大,吞吐量降低)                                                 

  GC自适应的调节策略                                               

    -XX:+UseAdaptiveSizePolicy 使用自适应的调节策略 即不需要指定新生代的大小,Eden与Surivior的比例,晋升老年代的年龄等细节参数,虚拟机自动根据根据当前系统的状态动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。

  算法:采用复制算法 

  优点:高效

  搭配:Parallel Old或Serial Old(MSC) 

-------------------------老年代收集器----------------------------

4、Serial Old收集器

  适用:    1).运行在Client模式下的虚拟机中的老年代

               2).在Server模式下,它主要还有两大用途

     ①.与Parallel Scavenge搭配

     ②.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用

  特点: 1.单线程GC,Serial收集器的老年代版本 

        2.在GC时暂停所有用户线程

  算法:采用标记-整理算法

  优点:简单,高效

  缺点:GC时暂停线程带给用户不良体验

  搭配:Serial Old(MSC)或ParNew

5、Parallel Old收集器     

  适用:运行在Server模式下的虚拟机中的新生代.在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

  特点:    1).多线程GC(并行):Parallel Scavenge的老年代版本

    2).在GC时暂停所有用户线程

    3).这个收集器是在JDK 1.6中才开始提供的

  算法:采用标记-整理算法

  优点:高效

  缺点:GC时暂停线程带给用户不良体验,单线程下效果不一定优于Serial

  搭配:Parallel Scavenge

6、CMS(Concurrent Mark Sweep)收集器:Hotspot上第一个真正意义上的并发收集器。

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

  适用:运行在Server模式下的虚拟机中的老年代,适合对响应时间要求高的应用。

  算法:采用“标记-清除”算法

  特点: 多线程 并发

  过程:    1).初始标记:暂停用户线程,标记GC Roots能直接关联的对象,速度很快

    2).并发标记:用户线程与标记线程并发,进行GC Roots Tracing的过程

    3).重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

    4).并发清除:用户线程与清除线程并发。

    其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

优点:并发收集、低停顿--由于耗时最长的并发标记和并发清除阶段都与用户线程并行工作,故系统停顿时间极短。

  缺点:     1).对CPU资源非常敏感。

    原因:面向并发设计的程序都对CPU资源比较敏感。并发时,因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低,应用程序会变慢,当CPU数不足时,尤其明显。

    解决:增量式并发收集器(i-CMS):在并发标记、清除时让GC线程与用户线程交替运行,以降低GC线程独占CPU的时间。当GC时间将变长时,效果一般,被丢弃使用。

    2).无法处理浮动垃圾,可能出现“Concurrent Mode "Failure"失败而导致另一次Full GC的产生。浮动垃圾:在并发清除阶段,用户线程仍在运行,此时产生的垃圾无法在该次收集中处理。

同时由于要保证并发,就必须预留内存给用户线程使用,因此CMS无法等到老年代几乎完全填满时再进行收集。JDK 1.5中CMS默认当老年代被使用68%时被激发。1.6中为92%。

    当CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode "Failure"失败,这时虚拟机将启动后备预案:临时使用Serial Old收集器来重新进行老年代垃圾收集,这样停顿时间就会很长。

    3).产生空间碎片,影响大对象的分配。  

    这是由于该收集器是由“标记-清除”算法实现的所引起的。所以往往存在有很大空间剩余,当无法找到足够大的连续空间来分配当前对象,不得不提前出发一次Full GC。

    解决    1.-XX:+UseCMSCompactFullCollection 开关参数(默认开启)用于当CMS要进行Full GC时开启内存碎片的合并整理过程,该过程不能并发,故停顿时间变长。

    2.-XX:CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的Full GC后跟着来一次带压缩的Full GC。默认为0,表示每次进入Full GC时都进行碎片整理。                    搭配:Serial或ParNew

---------------------------新生代和老年代均适用---------------------

7、G1收集器

  适用:面向服务端应用,适用于新生代和老年代。当前收集器技术发展的最前沿成果

  特点: 

    1.并行+并发。可充分利用CPU资源

    2.分代收集。

    3.空间整合。 G1从整体看是”标记-整理“算法,从局部(两个Region之间)看,是”复制“算法。 不会产生空间碎片。

    4.可预测的停顿。建立可预测的态度时间模型,能让使用者明确指定在一个长度为M毫秒的时间内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。 

  Garbage First名称的由来                                            

    G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集。G1将内存划分为Region,跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

  难点:虽然内存分为Region,但垃圾收集不能真的以Region为单位进行,因为Region不可能是孤立的,存在某个对象被多个Region的引用,那在做可达性判断确定对象是否存活时,是否需要扫描整个堆空间呢?注意:此问题在所有的收集器中都存在(如存在新生代与老年代之间的引用)。

  解决:1.使用Remembered Set来避免圈堆扫描。

  过程:G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作是,会产生一个Write Barrier暂时中断操作,检查Reference类型引用的对象是否处于不同的Region(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

  内存布局:G1的堆内存布局与其他收集器不同,G1将整个堆内存空间划分为多个大小相等的Region,虽然仍然有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。

  过程(与CMS相似)

    1.初始标记:暂停用户线程,标记GC Roots能直接关联的对象

    2.并发标记:用户线程与标记线程并发,进行GC Roots的Trace

    3.最终标记修正并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。

    4.筛选回收:  

  算法: 全局标记-整理+局部复制算法

  优点:高效,停顿时间可控、可预测



 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值