java虚拟机

Java虚拟机:垃圾收集机制

2019-05-25  本文已影响2人  ZebraWei

版权声明:本文为斑马君学习总结文章,转载请注明出处!

一、垃圾回收

上篇博客介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
GC需要完成的3件事情:

二、如何判定对象为垃圾对象

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

引用计数算法

在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1.该算法缺陷是难解决对象之间相互循环引用的问题。
public class Main {

private Object instance;

public Main() {
    byte[] m = new byte[20 * 1024 *1024];
}

public static void main(String[] args) {
    
    Main m1 = new Main();
    
    Main m2 = new Main();
    
    m1.instance = m2;
    m2.instance = m1;
    
    m1 = null;
    m2 = null;
    
    System.gc();
}
VM arguments参数设置:-verbose:gc -XX:+PrintGCDetails

从运行结果中可以清楚看到,GC日志中包含“707K->476K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

这个算法的基本思路就是通过一些列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在java语言中,可作为GC Roots的对象包括下面几种:

再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong
Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱。

三、如何回收

回收策略:标记-清除算法

算法分为”标记“和”清除“两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

该算法主要有两个不足的问题:

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

回收策略:复制算法

该算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

活着的对象复制到另外一块上面 已使用过的内存空间一次清理掉 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。 当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

回收策略:标记-整理算法和分代收集算法

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

“标记-清除”算法:不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

标记-整理算法 “分代收集算法:根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间。对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
四、垃圾收集器

新生代收集器-Serial收集器

是最基本、发展历史最悠久的收集器。曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。,收集几十兆甚至一两百兆的新生代,停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。

Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)

新生代收集器-ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余与Serial收集器完全一样。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

新生代收集器-Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法的并行收集器,Parallel Scavenge 收集器使用两个参数控制吞吐量。
直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒。但是线程编程每5秒收集一次,每次停顿70毫秒,停顿时间下降的同时,吞吐量也下降了。

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

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。收集器有一个参数- XX:+UseAdaptiveSizePolicy当这个参数打开之后,就不需要手动指定新生代的大小,Eden和Survivor区的比例,晋升老年代对象等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式成为GC自适应的调节策略。

老年代收集器 - CMS收集器

由于垃圾回收时,都需要暂停用户线程,CMS(Concurrent Mark Sweep)收集器是一种以 获取最短停顿时间 为目标的收集器,重视服务的响应速度,希望系统停顿时间最短,能给用户带来良好的体验。
CMS收集器是基于"标记-清除"算法实现的,它的运作过程比较复杂,整个过程分为四个步骤:

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

优点:CMS是一款优秀的收集器,主要优点:并发、低停顿。

缺点:

全区域的垃圾回收器 - G1收集器
G1垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的region块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存后,还会做内存压缩。
G1垃圾回收器回收region的时候基本不会Stop The World,从整体来看是基于标记-整理算法,从局部(两个region之间)来看基于复制算法。

一个region有可能属于Eden、Survivor或者Tenured内存,图中的E表示Eden区,S表示Survivor区、T表示Tenured区、空白就是未使用的空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超出一个region大小的50%的对象。
年轻代垃圾收集
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法,把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃圾收集
对于老年代的垃圾收集,G1(Garbage First)也分为四个阶段,基本与CMS垃圾收集器一样,但是略有不同。

吞吐量

吞吐量就是CPU 运行用户代码的时间 与 CPU总消耗时间 的比值。

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

假设虚拟机总共运行了100分钟,其中垃圾收集花了一分钟 吞吐量就是99%,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务。

上一篇 下一篇

猜你喜欢

热点阅读