Java 虚拟机 垃圾回收

如何判断对象已是垃圾

引用计数法

引用计数法实现简单,判定效率也很高,但是JAVA虚拟机并没有用引用计数法来判断对象是否存活

原理:给对象中添加一个引用计数器,每当一个地方引用他的时候,计数器的值就加一,当引用失效时就减一,任何时刻计数器为0时对象就是不可能被使用的。
缺点:对于两个互相引用的对象无法当做垃圾收集,虽然他们再无任何引用。

可达性分析算法

原理:

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则代表此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

缺点:

实现比较复杂;
需要分析大量数据,消耗大量时间;
分析过程需要GC停顿,即停顿所有JAVA执行线程。

引用:

在jdk 1.2之前对于引用的定义就是如果一个reference类型的数据中储存着另一个内存的起始地址就称作引用。
这种定义太过狭隘,一个对象只有被引用和不被引用两种状态,对于如何描述一些“食之无味,唾之可惜”的对象就显得无能为力。我们希望在内存充足的情况下保留这些对象,在不充足的情况下抛弃这些对象。
在jdk1.2之后,Java对引用的概念进行了扩充,将引用分为了强引用,软引用,弱引用,虚引用这四种。

强引用:

Object object = new Object();
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

软引用:

描述一些还有用但非必须的对象,在系统将要发生内存溢出异常前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出内存异常,1.2之后提供了SoftReference类来实现软引用。

弱引用:

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

虚引用:

他是最弱的一种引用一个对象是否有虚引用的存在,完全不会对其生存时间构成引用关联的唯一目的就是能在这个对象被垃圾回收器时收到一个系统通知。jdk1.2后提供了Phantom Reference 类来实现虚引用。

finalize()方法

在可达性分析算法中不可达的对象,也并非是“非死不可”的,他们距离真正的死亡至少要经历两次标记过程:

1.第一次标记

在可达性分析后发现GC Roots没有任何引用1链相链时,被第一次标记;
并进行一次筛选; 此对象有没有必要执行finalize()方法;

  • A 没有必要执行
    + 对象没有覆盖finalize()方法;
    + finalize()方法已经被JVM调用过;
    这两种情况就可以认为对象已死,可以回收;
  • B 有必要执行
    对于有必要执行finalize()方法的对象,被放入F-Queue队列中;
    稍后再JVM自动建立,低优先级的Finalizer线程(可能多个线程)中触发这个方法;

2 第二次标记

GC将对F-Queue队列中的对象进行第二次小规模标记:
finalize()方法是对象逃脱死亡的最后机会:

  • A 如果对象在其finalize()方法中重新与引用链上的任何一个对象建立关联,第二次标记时会将其移出“即将回收”的集合
  • B 如果对象没有,也可认为对象已死,可以回收了

一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱的对象,第二次不会再调用;

finalize()是Object类的一个方法,是Java刚诞生时为了使C/C++程序员容易接受它所做出的一个妥协,但不要当作类似C/C++的析构函数;

HotSpot虚拟机中的对象可达性分析的实现

问题:

1,消耗大量时间:
GC Roots 主要在全局性的引用(常量或静态属性)
和执行上下文(栈帧中的本地变量表);
要在这些大量的数据中,逐个检查引用,会消耗很多时间;
2,GC停顿
可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化;
导致GC进行时必须停顿所有JAVA执行线程(“Stop the world”);
几乎不会发生停顿的CMS收集器中,枚举根节点时也必须停顿
Stop the World:
是JVM 在后台自动发起和自动完成的;
在用户不可见的情况下,把用户正常的线程全部停掉;

枚举根节点:

枚举根节点也就是查找GC Roots:
目前主流的JVM都是准确式GC,可以直接得知哪些地方存放着对象引用,所以执行系统停顿下来后,并不需要全部、逐个检查全局性和执行上下文中的引用位置

在Hotspot中,是使用一组叫OopMap的数据结构来达到这个目的;
在类加载时,计算对象内什么偏移量上是什么类型的数据
在JIT编译时,也会记录栈和寄存器中的哪些位置是引用;
这样GC扫描时就可以直接得知这些信息;

安全点 :

1.HotSpot 在Oopmap的帮助下可以准确的完成GC Roots的枚举,但是这有一个问题:
运行中,非常多的指令会导致引用关系变化;
如果这些指令都生成对应的OopMap,需要的空间成本太高;

问题解决:
只在特定的位置记录OopMap引用关系,这些位置叫安全点;
所以程序执行时并非所有地方都能停顿下来开始GC

2.安全点的选定:

指令序列复用:
方法调用,循环跳转,循环的末尾,异常跳转等,
只有这些功能的指令才会产生Safepoint;

  1. 如何在安全点上停顿
    • A.抢占式中断(Preemptive Suspension)
      + 在GC发生时,首先中断所有线程;
      + 如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;
    • B 主动式中断(Voluntary Suspension)
      + 在GC发生时,不直接操作线程中断,而是仅简单的设置标志
      + 让各个线程执行时主动去轮询这个标志,发现中断标志位真时就自己中断挂起;而轮询标志的地方是和SafePoint是重合的;
      在JIT执行方式下:test指令是HotSpot生成的轮询指令

回收方法区

永久代的垃圾收集主要回收两部分内容:
废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常类似,“abc”,如果没有一个String对象叫做“abc”时,就会被系统清理出常量池。
判断一个类是否是无用的类的条件则相对苛刻:
**+ 该类的所有实例都已经被回收,在Java堆中不存在该类的任何实例

  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。**
    虚拟机可以对满足上述三个无用的类进行回收,不是必然,
    HotSpot虚拟机提供了-Xnoclassgc参数来进行控制,还可以使用-verbose:class 以及-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息,在大量使用反射,动态代理,这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

垃圾收集算法

标记-清除算法:

标出所有要回收的对象,然后统一回收,

这两个步骤的效率并不高,之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后分配大对象时,无法找到足够的内存而不得不提前触发另一次垃圾收集动作。

复制算法

将内存划分为两块,每次只使用一块,当这块用完了,将存活着的对象复制到另一块内存上去,然后将使用过的一块清空
不用考虑内存碎片等情况,只要一动堆顶指针,按顺序分配内存就ok

JVM 没有按照1:1来划分内存空间,而是将内存划分为较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间,当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和Survivor空间。
Hotspot默认Eden和Survivor的空间比例是8:1,也就是新生代中可用内存是整个新生代的90%。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间就要有额外的空间分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。
在这里插入图片描述

分代收集算法:

新生代,每次收集垃圾时都有大量的对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中因为对象存活率高,没有额外的空间对他进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

垃圾收集器

Serial收集器

他是最基本的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择,这个收集器时一个单线程去完成垃圾收集工作的,
在他进行垃圾收集时必须暂停其他所有的工作线程,直到他收集结束,在用户不可见的情况下将用户正常工作的线程全部停掉。
优点:
简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,在用户桌面应用场景中,需要的内存一般来说不会很大,收集几十兆甚至几百兆的新生代,停顿时间完全可以控制在集市毫秒最多一百多毫秒之内,只要不是频繁发生,这点停顿是可以接受的,所以Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择

ParNew 收集器

ParNew收集器除了多线程收集外,其他与Serial收集器没有不同,
他是许多运行在Server模式下的虚拟机首选的新生代收集器,其中有一个与性能无关的很重要的原因就是,除了Serial收集器外,目前只有他能与CMS收集器配合工作。在JDK1.5时期,HOtSpot推出了一款强交互应用中很强的垃圾收集器--------CMS收集器(Concurrent Mark Sweep),可以让垃圾收集线程和用户线程同时工作。
CMS作为老年代的收集器无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenger 配合工作,所以在JDK1.5.0中使用的CMS收集老年代时,新生代只能选择ParNew或者Serial收集器中的一个。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
ParNew在单核CPU的效果绝对没有Serial收集器好但是随着可以使用的Cpu数量的增加,他对于GC时系统资源的有效利用还是有好处的,他默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾回收的线程数。

Parallel Scavenger收集器

他是一个新生代收集器,他也是使用复制算法的收集器,又是并行的多线程收集器
他的关注点与其他收集器不同,CMS等收集器关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenger收集器的目的是达到一个可控制的吞吐量,
吞吐量就是CPU用于运行用户代码与CPU总消耗的时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)
总共100min,垃圾收集1min,那吞吐量就是99%
停顿时间越短越适合需要与用户进行交互的程序
而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多的交互任务。
Paralllel Scavenger提供了两个参数用于精准控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数,

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数收集器就尽可能保证内存垃圾回收的时间在这个范围内。
但你不要以为把这个参数设置的稍小一点就能使得垃圾收集的速度变得更快,GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,这也导致收集发生的更频繁,原来十秒收集一次每次停顿100ms,现在变成每5秒收集一次,每次停顿70ms,停顿的时间确实下降了,但是吞吐量也降下来了。

GCTimeRatio参数的值应该是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率

由于与吞吐量关系密切,Parallel Scavenger 收集器也经常称为“吞吐量优先”收集器。
他还有一个参数:-XX: +UseAdaptiveSizePolicy,这是一个快关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式叫
FC自适应的调节策略(GC Ergonomics)

Serial Old收集器

Serial old收集器是Serial收集器的老年版本,他同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是给Client模式下的虚拟机使用。如果在Server模式下,他还有两大用途

  • 在1.5之前的版本中与Parallel Scavenger收集器搭配使用,
  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old是Parallel Scavenger收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器于JDK1.6之后开始提供。
在此之前新生代的Parallel Scavenger 收集器一直处于比较尴尬的状态,因为,新生代如果选择了Parallel Scavenger 收集器,老年代除了Serial Old收集器外别无选择。由于老年代SerialOld收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenger收集器也未必能在整体应用上获得吞吐量最大化的效果,
由於单线程的老年代收集中无法利用服务器多cpu的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量没有ParNew加上CMS给力
Parallel Old收集器加上Parallel Scavenger收集器应用于注重吞吐量以及CPU资源敏感的场合。

CMS收集器

CMS是一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或者B/s系统的服务端上,这类系统尤其注重服务的响应速度系统停顿的时间最短,以给用户带来更好地体验
CMS收集器基于“标记-清除”算法,整个过程分为四个步骤

  • 初始化标记
  • 并发标记
  • 重新标记
  • 并发清除
    初始标记和重新标记仍然需要"Stop the World"。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing的过程,而重新标记阶段是为了修正并发标记期间因用户程序继续操作而导致标记产生变动的那一部分的标记记录,这个阶段的停顿时间会比初始标记阶段稍长一些,但远比并发标记的时间短
    CMS收集器内存回收过程是与用户线程一起并发执行的,并发标记和并发清除过程都可以于用户线程一起工作。
    缺点:
  • 对CPU资源非常敏感,因为他占用了一部分的cpu资源而导致应用程序变慢,总吞吐量hi降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU数量在四个以上时,并发回收时垃圾线程不少于25%的cpu资源,并且随着CPU数量的增加而下降。但是当CPU不足四个时,CMS对用户程序的影响就可能变得很大,
    为了应对这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种所做的事情就是在并发标记,清理时让GC线程,用户线程交替运行,尽量减小GC线程的独占资源时间,这样垃圾收集的时间会更长但对用户程序的影响就会显得更少一些,但是整个垃圾回收的过程会更长。
  • CMS无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生,这一部分垃圾出现在标记过程后,CMS无法在当次收集中处理掉他们,只好留在下一次GC时再清理掉。Concurrent Mode Failure这一部分就叫浮动垃圾,也是由于垃圾线程运行时用户线程还需要运行,那就还需给用户线程留够空间,因此CMS不会像其他收集器那样等到老年代几乎全部填满了再进行收集,需要预留一部分空间提供并发收集时程序运行做使用。在JDK1.5的默认配置下,CMS收集器当老年代使用了68%的空间后就会被激活,如果在应用中老年代的增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收的次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阈值已经提高到92%,要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure 失败,这时虚拟机将启动后备预案:启动临时Serial Old收集器来重新收集老年代,这样停顿的时间就很长了,所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低。
  • CMS基于“标记-清除”算法实现的收集器,会产生大量的空间碎片,碎片过多,会给对象的分配带来很多麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间分配,不得提前触发一次FullGc,为了解决这一问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的),用于在CMS顶不住的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数:-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,接着来一次压缩(默认值是0,表示每一次进入Full GC时都进行碎片整理)。

G1收集器

G1收集器时当今收集器的最牛逼的成果之一,是面向服务端应用的垃圾收集器。
特点:

  • 并行和并发:G1能充分利用多CPU,多核环境下的硬件优势,使多个CPU来缩短Stop The World停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中保留,虽然G1可以不需要其他垃圾收集器的配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活一段时间,熬过的多次GC的旧对象以获取更好地收集效果。
  • 空间整合:与CMS的“标记-整理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部来看是基于“复制”算法实现的,但无论如何,这两种算法都一位着G1运作期间不会产生内存碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对CMS的另一大优势,降低停顿时间是G1和CMS共用的关注点,但G1除了追求低停顿外,还能建立可预测停顿时间模型,能让使用者确定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这似乎已经是Java的垃圾收集器的特征了

别的垃圾收集器进行垃圾收集的范围都是整个新生代或者老年代,而G1不再是这样,他将整个java堆划分为多个大小相等的独立区域,保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分Region(不连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为他可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First的来由),这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1的实现:

Region不可能是孤立的,一个对象分配在某个Region中不可能只和本Region中的对象引用,而是可以和多个Java堆中任意对象发生引用关系,那么做可达性分析的时候不可能再把整个java堆来扫一遍来确保对象是否存活。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机是利用Remember Set来避免全对扫描的,G1中每个Region都有一个与之对应Remember Set,虚拟机发现在对Reference 类型的数据进行写操作时,会产生一个Write Barrier 暂停中断写操作,检查Reference 引用的对象是否处于不同的Region之间(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region 的RememberSet之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set之中。当进行内存回收时,在GC更节点的枚举范围中加入Remember Set即可保证不对全堆扫描也不会有遗漏。

G1的运行步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
    初始标记阶段仅仅是标记一下GC Roots 能关联到的对象,并且修改 TaMs的值,让下一阶段用户程序并发执行时,能在正确的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
    并发标记阶段是从GCRoots开始对堆中对象进行可达性分析,找出存活的对象,这一阶段耗时很长,但可与用户程序并发执行
    最终标记阶段是为了修正在并发标记期间因用户程序操作而导致标记产生变动的那一部分记记录,虚拟机将这段时间对象变化记录在线程Remember Set logs里面,最终标记阶段需要把Remember Set Logs 的数据合并到Remember Set中,这阶段需要停顿线程,但是可并行执行,最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,
发布了124 篇原创文章 · 获赞 9 · 访问量 2472
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章