JVM—GC
GC
GC就是垃圾收集,java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收的目的。
对于GC来说,当对象被创建的时候,GC就开始监控这个对象的地址、大小及使用情况。
对象确定
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死亡了,需要被回收。
一般通过两种算法确定那些对象需要被回收:
引用计数器法
为每个对象创建一个引用计数(被引用,即由引用指向这个对象),有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。
当一个对象被当作垃圾收集时,它引用的任何对象的计数器的值都减一。
优点
引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点
需要额外的空间来存储计数器,难以检测出对象之间的循环引用。
可达性分析算法
可达性分析法也被称之为根搜索法。
可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。这两种情况被称为可达的:
对象属于根集中的对象。
根集指的是正在执行的java程序可以访问的引用变量的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。根集中的对象也就是GC Roots。
对象被一个可达的对象引用。
也就是说,从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。
在java中,可以被当做GC Roots的对象有:虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的常量引用的对象。
方法区中的类静态属性引用的对象。
本地方法栈中 JNI(Native 方法)的引用对象。
活跃线程(已启动且未停止的 Java 线程)。
当一个对象到根对象没有任何引用链相连,则成为这个对象是不可达的,也称为不可达对象,是可以被回收的。
二次标记
在可达性分析算法中,对象有两种状态,要么是可达的要么是不可达的,在判断一个对象可达性的时候就需要对对象进行标记。
- 开始进行标记前,需要先暂停线程,否则对象图如果一直在变化的话是无法真正去遍历它的。
- 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
- 在可达性分析算法中,要真正宣告一个对象死亡,至少要经过两次标记过程:
- 如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行
finalize()
。当对象没有覆盖finaliza()
方法,或finalize()
方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。 - 如果该对象被判定为有必要执行
finalize()
方法,那么这个对象将会被放置在一个名为F-Quene的队列中,并在稍后由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()
方法。这个方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Quene中的对象进行第二次小规模的标记, 如果要在finalize()
方法中成功拯救自己,只要在finalize()
中让该对象与引用链上的任何一个对象重新建立关联即可。而如果对象这时还没有关联到引用链上的任何一个对象,那么他就会被回收。
- 如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行
- GC 判断对象是否可达看的是强引用。
优点:
可以解决循环引用的问题,不需要占用额外的空间。
缺点:
多线程场景下,其他线程可能会更新已经访问过的对象的引用。
垃圾收集算法
在确定那些对象需要被回收后,接下来就需要垃圾收集器通过垃圾收集算法进行垃圾回收了。
标记清除算法
先标记Java堆中可回收的对象,然后直接进行回收操作。
分为标记和清除两个阶段。
首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。
优点
不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点
标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。
标记整理算法
与“标记-清除算法”的唯一区别就是在回收操作完成后,会将零散的空间碎片进行整理。
整理算法的清除过程与清出算法的稍有不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。
优点
经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点
GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
复制算法
常见的是将Java堆划分为一个Eden空间和两个Survivor空间,默认比例为8:1:1,每次使用一个Eden空间和一个Survivor空间。
复制算法是为了解决句柄的开销和堆碎片的回收。
它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。
优点
标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点
需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。即内存使用率不高,只有原来的一半。
分代收集算法
将Java堆分为新生代和老年代,一般来说新生代采用“复制算法”,而老年代则采用“标记-清除算法”或者“标记-整理算法”,具体则要根据JVM实际实现,不同的虚拟机实现可能不同。
在jdk1.7前还有永久代,永久代的GC是绑定在老年代一起的。
新生代又被进一步划分为Eden和Survivor区,其中Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。
创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
当前主流虚拟机都采用分代收集。
垃圾收集器
其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge。
回收老年代的收集器包括Serial Old、Parallel Old、CMS。
有用于回收整个Java堆的G1收集器。
垃圾回收线程相对于工作线程是独立的,当需要执行垃圾回收时,会先停止工作线程,然后通知垃圾回收线程执行。
新生代垃圾收集器
Serial
串行垃圾回收器,采用单线程的方式进行收集,采用的是复制算法,在GC线程执行时,系统不允许工作线程打扰。这个过程中应用程序会进入暂停状态,即Stop-the-world。
STW这个过程对用户不可见,用户仅感知到系统卡顿了一会。STW时间的长短是衡量性能的指标。
单核的系统下,不存在线程之间的交互,这种可以提高效率。
PraNew
并行垃圾回收器,采用的是多线程的方式,使用了多个GC线程,也采用复制算法,可以看做是Serial的多线程版本。
单核情况下,系统无法发挥多线程的优势,效率会比Serial差。
Parallel Scavenge
新生代并行收集器,相对PreNew,追求高吞吐亮,同样采用复制算法,又称为吞吐亮优先收集器。
Serial与PargNew比较关注STW时间,而Parallel Scavenge更关注吞吐量。
吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
。
侧重点
高吞吐量
GC 的总时间越短,系统的吞吐量则越高。换句话说,高吞吐量则意味着,STW 的时间可能会比正常的时间多一点,也就更加适合那种不存在太多交互的后台的系统,因为对实时性的要求不是很高,就可以高效率的完成任务。
短STW
STW 的时间短,则说明对系统的响应速度要求很高,因为要跟用户频繁的交互。因为低响应时间会带来较高的用户体验。
老年代垃圾收集器
Serial Old
Serial Old是Serial的老年代版本,使用的标记-整理算法。
Serial Old是给client模式下的JVM使用。
Parallel Old
是Parallel Scavenge的老年代版本,同样是多线程的,采用标记整理算法。
特性与Parallel Scavenge相似,同样是吞吐量优先。
CMS(Concurrent Mark Swee)
采用标记清除算法,重点关注于最短的STW时间。
它的过程分为4步:
- 初始标记:标记从GCRoots出发能够关联到的所有对象,此时需要STW,但是不需要很多时间。
- 并发标记:多线程对所有对象通过GC Roots Tracing进行可达性分析,这个过程较为耗时。这个阶段程序仍在执行。
- 重新标记:重新标记是为了修正在并发标记阶段,发生错误的一些数据。并发标记过程中,程序仍在运行,有些对象的状态可能会发生变化,所以需要重新标记,这个过程需要STW。
- 并发清除:标记完成后进行清除。
优点
并发收集,低STW。
将标记阶段,以流水线的方式拆分为3端,将耗时最长的阶段,与程序并发执行,仅需要两个很少的停顿阶段,降低STW时间,达到近似并发的目的。
初始标记<重复标记<并发标记
缺点
缺点也很明显:
对CPU资源很敏感,CPU资源很少时,系统占用很多,GC就占用很少,吞吐量就很低。
无法处理浮动垃圾。
浮动垃圾就是在并发标记的时候产生的垃圾,这些垃圾只能在下一次GC时清除,如果预留的内存空间不足保存浮动垃圾,就会产生Full GC。
基于标记清除算法,一堆问题。
堆垃圾收集器
G1收集器
G1全称Garbage First。G1收集器基于标记整理算法实现,相对于前面的垃圾回收器,G1收集器在实现高吞吐的同时尽可能减少STW时间。
G1收集器特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
- 分代收集:打破了原有的分代模型,将堆划分为一个个区域。
- 空间整合:与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。
G1收集器打破了以往将收集范围固定在新生代,老年代的模式,G1将堆划分为一个个小的Region块(区域大小相同的内存单元)。
每个Region被标记了E、S、O 和 H,这些区域在逻辑上被映射为Eden,Survivor、老年代和大对象区。存活的对象从一个区域转移(即复制或移动)到另一个区域,区域被设计为并行收集垃圾,可能会暂停所有应用线程。
Humongous区域(标记H区)是为了那些存储超过50%标准Region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动 Full GC。
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Region作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
G1收集器工作过程:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
- 并发标记:是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记:是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。类似于CMS的重复标记,但是这里可以并发执行。
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1的回收模式可以分为两种:
- Young GC:在分配一般对象(非巨型对象)时,当所有Eden区域使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。
- Mixed GC:当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些Old区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1没有Full GC概念,需要Full GC时,调用Serial Old GC进行全堆扫描。