JVM(二):垃圾收集器,内存分配策略

确定对象是否存活

在堆里存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收之前,需要确定哪些对象还“存活”,哪些对象已经“死亡”。常用的算法有两种:引用计数法可达性分析算法两种

引用计数法

给对象添加一个引用计数器,当对象被引用时,计数器+1,当引用失效时,计数器-1;当任何时刻计数器为0时,说明对象不可能再被使用。

引用计数法的实现简单,效率也高,在大部分场景都是一个不错的算法。但有一个致命的问题:无法解决对象之间循环引用的问题。现在的虚拟机基本都不是使用引用计数法来判断对象是否存活。

可达性分析算法

以一系列叫做"GC Roots"的对象作为起始点,向下进行搜索,搜索经过的路径叫做引用链。当一个对象到GC Roots之间没有任何引用链相连,说明此对象是不会再被使用的。优点类似于图论算法。

可作为GC Roots的对象一般包括以下几种:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中静态变量引用的对象

引用

强引用

强引用是在程序代码中普遍存在的,类似于Object a=new Object()这样的就是强引用。只要强引用还存在,那么与之关联的对象就永远不会被垃圾回收器回收

软引用

软引用描述的是一些还有用但是非必需的对象。与软引用关联的对象一般情况下不会被回收,只有当系统即将发生内存溢出异常时,才会将这些对象划入回收范围进行二次回收,若二次回收之后依然没有足够的内存,此时就会抛出内存溢出异常

弱引用

弱引用描述的同样时有用但是非必需的对象,但是它的强度比软引用更弱一点。被弱引用关联的对象只能生存到下一个垃圾收集发生之前。也就是说,当垃圾收集器工作时,所有只与弱引用关联的对象都会被回收

虚引用

虚引用是强度最弱的一种引用关系。虚引用的存在与否对对象是否存活不构成任何影响。为对象设置虚引用的唯一目的就是在这个对象被垃圾收集器回收时得到一个系统通知。

回收方法区

方法区中也是有垃圾产生的,只是回收方法区的性价比比较低,所以虚拟机规范中并没有要求一定要在方法区实现垃圾收集。而方法区的垃圾收集主要分为两部分:废弃常量无用的类

废弃常量

回收废弃常量与回收堆中的实例非常相似。以字符串来例,假设一个字符串“abc”已经进入了方法区常量池,但此时程序代码中没有任何一个引用指向这个字符串,那么这个字符串就会被视为无用的,可以被清理。

无用的类

一个类满足以下三种条件就可以被认为是无用的:

  • 该类的所有实例都已经回收,Java堆中不存在该类的实例
  • 该类相关的ClassLoader已经被回收
  • 该类的Class对象没有在任何地方被引用,也就是说无法通过反射访问该类

垃圾收集算法

标记清除法(mark-swap)

首先标记出所有需要回收的对象,在标记完成后统一进行清除。

缺点:

1.效率问题:标记和清除这两个过程的效率都不高

2.有内存碎片产生:标记清除后会产生大量不连续的内存碎片,如果后续需要一个大对象,可能会因内存不足提前引起一次垃圾收集

复制算法(copy)

将内存按容量划分为大小相等的两块,每次只使用其中的一块,当一块内存使用完了,将还存活的对象复制到另一块上,然后把已经使用过的内存块全部清理掉

优点:没有内存碎片的问题

缺点:每次只能使用50%的内存,内存利用率不高

所以现在的收集器如果使用复制算法的话,基本都不会使用这种划分为大小相等的两块这样的做法。由于大多数对象的存活时间都很短,所以说一种典型的做法就是将内存划分为一块较大的Eden区和两块较小的survivor区。每次只使用Eden区和其中的一块Survivor区,当发生垃圾回收的时候,将存活的对象一次性复制到另一个survivor区中,然后对原先的两个区域进行清理。若复制到survivor区时,survivor的空间不足以存放所有的存活对象,此时就出发老年代的分配担保

标记整理法(mark-compact)

同样是对所有需要回收的对象进行标记,在标记完成后,将所有存活的对象朝一端移动,然后清除掉边界以外的所有对象。

优点:解决了内存碎片的问题

缺点:涉及对象在物理地址上的移动,效率较低

分代收集算法

根据对象存活年龄将内存划分为几块,常见的分法是分成新生代和老年代。因为新生代中的对象存活率较低,所有可以次采用复制算法;而老年代中的对象存活率高,且没有额外的空间来做分配担保,所以可以采用标记清理或者标记整理法

GC触发的时机

安全点

程序并不是在任何地方都能停顿下来进行GC,只有在到达安全的地方才能暂停,这些位置就叫做**安全点(safe point)**安。安全点的选定不能太少,太少容易导致GC等待时间太长,垃圾堆积占用内存;也不能设置得太短,频繁的GC也会增加开销。那么安全点一般在方法调用,循环跳转,异常跳转等指令上产生,因为这些指令更可能会长时间执行。

另一个需要考虑的问题就是如何确保GC发生时,所有线程都在安全点上?这里有两种方法:抢先式中断主动式中断

抢先式中断

当要发生GC时,直接将所有线程中断,如果有线程中断的地方不在安全点上,那么就恢复线程,等它跑到安全点上。现在几乎没有虚拟机采用这种方式

主动式中断

当要发生GC时,不直接操作线程,而是修改一个中断标志,当线程执行到安全点时,都会去轮询这个标志,如果该标志被修改为true,那么中断挂起。这样能确保每个线程都能在安全点发生GC

安全区域

因为中断标志需要主动轮询,所以对于那些正处于沉睡状态或者是中断状态的线程来说是不可实现的,并且虚拟机也不可能等到这些线程被重新调度然后再跑到安全点。

安全区域指的就是不会使引用关系发生变化的代码片段。当要发生GC的时候,如果线程处于安全区域,那么虚拟机就不会管这些线程了,因为它的执行不会导致引用关系的变化。但是线程离开安全区域时,还需要检查标记过程是否已经结束,若还没结束,则必须等待,直到收到离开的信号才能离开安全区域

垃圾收集器

现在一般都采用分代收集的算法,并分为新生代和老年代。所以其中的垃圾收集器也有所不同

新生代

Serial

单线程的垃圾收集器,在执行垃圾收集的时候,必须暂停所有工作线程(Stop the world)。采用复制算法

优点:实现简单,单线程,没有线程交互的开销。

缺点:必须暂停所有工作线程,有卡顿产生。

客户端应用适合使用Serial收集器,因为客户端应用一般较小,而且对象数量也比较少,垃圾收集耗费的时间也比较短,即使Serial会暂停工作线程,但这个卡顿对用户来说不太明显。所以客户端应用适合使用Serial

ParNew

其实就是Serial的多线程版本,同样会暂停工作线程,并且也采用复制算法。

可以和CMS一起配合工作(CMS无法和Parallel Scavenge配合使用)

优点:在多CPU环境下充分利用硬件优势

缺点:如果是在单CPU或者双CPU的环境下,性能可能比不上Serial,因为有线程交互的开销

Parallel Scavenge

同样也是一个多线程收集器,也是使用复制算法。但和前两种区别就是,Serial和ParNew关注停顿时间,而Parallel Scavenge关注的是吞吐量,吞吐量就是CPU运行用户代码时间与总时间的比值。

由于关注吞吐量,所以也有几个参数可以进行准确控制:

-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间。但不是越小越好,因为时间的缩短是要以新生代空间的减小为代价的。因为更小的空间,进行GC的时间也越短

-XX:GCTimeRatio 直接控制吞吐量。GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

还有一个很便捷的开关:

-XX:+UseAdaptiveSizePolicy 打开之后,不需要手动设置新生代大小,Eden区和Survivor区比例,晋升老年代对象年龄等参数,虚拟机会进行动态地调整

老年代

Serial Old

Serial Old是Serial的老年代版本,同样是一个单线程收集器,但采用的是标记i整理算法。

主要用途:

1.Client模式下,是一个好的选择

2.Server模式下,可以和Parallel Scavenge搭配使用

3.作为CMS收集器的后备预案

Parallel Old

是Parallel Scavenge的老年代版本,采用多线程和标记整理算法。这个收集器是在JDK1.6开始才提供的。在此之前,Parallel Scavenge只能和Serial Old搭配使用。而应用于服务端的话,Serial Old的单线程特点会拖累性能,所以未必能发挥出Parallel Scavenge的吞吐量优先这一性能。所以Parallel Old的出现,就能和Parallel Scavenge进行搭配,成为了真正意义上的“吞吐量优先”收集器。

CMS收集器

CMS(Cocurrent Mark Swap)是一种以获取最短停顿时间为目标的收集器,它使用标记整理算法。因为CMS的停顿时间很短,所以广泛应用于重视响应速度的服务中。

CMS收集器的运行过程主要包括四个步骤:

初始标记

初始标记仅仅标记那些与GC Roots直接关联的对象,虽然说这个过程会暂停所有工作线程,但是由于执行速度很快,所以停顿时间也很短

并发标记

这个是执行时间比较长的一个操作,它在初始标记的基础上标记出所有能和GC Roots对象关联到的对象。最大的特点就是这个操作无需暂停工作线程,所以就叫做并发标记。

重新标记

并发标记的时候,因为工作线程一直在执行,所以对象的引用关系很可能发生变化,重新标记是为了修正并发标记期间由于用户程序执行而导致的引用关系变化的记录。这个操作需要暂停工作线程。

并发清除

真正执行GC,这个操作无需暂停工作线程

CMS的缺点:

1.对CPU资源敏感。因为CMS最大的特点是某些操作可以和工作线程并发执行,所以它会占用一部分线程,导致总的吞吐量下降。CMS默认开启的线程数量是(CPU数量+3)/4,当CPU数量较少时,对工作线程的影响可能就变得很大。这是一个问题

2.无法处理浮动垃圾。因为CMS清除垃圾的操作是可以和工作线程并发执行的,所以在这期间工作线程产生的垃圾无法得到回收,这部分就是浮动垃圾。也正是因此,垃圾收集的时候还需要预留出一部分充足的空间给工作线程产生垃圾,所以说CMS不能像其他垃圾收集器一样,等到老年代几乎完全满了才触发GC,CMS默认设置是当老年代使用68%空间后就会激活GC,当然,如果老年代增长不是特别快,这个值可以适当调高。但是如果调得太高,没有预留出足够的空间存放工作线程产生的垃圾,就会产生一次“Concurrent Mode Failure”失败,此时就会临时启用Serial Old收集器来重新进行老年代的收集,此时的停顿时间就更长了。

3.产生内存碎片:因为是基于标记清理实现的,所以会产生大量内存碎片,若后续需要分配一个大对象,可能会由于内存不足分配失败,提前引起GC

其他

G1收集器

G1收集器是当前收集器的最前沿成果之一,它是面向服务端应用的垃圾收集器,它的优点有:

1.多线程:多线程不仅仅是指垃圾收集操作可以并发,它指的是垃圾收集线程和工作线程可以并发,就和CMS一样

2.分代收集:G1保存了分代概念,但它并没有像传统方法一样将内存划分为新生代和老年代两个区域。而是将内存划分为多个大小相等的区域,虽然依然有新生代和老年代的概念,但是它们之间不再是物理隔离的。

3.可预测的停顿:G1收集器一个很大的优势就是能让使用者明确指定在一个时间段内,垃圾收集工作不能超过一个限定的时间。实现方法是,G1会跟踪每一个区域内垃圾堆积的价值大小,并在后台维护一个优先列表,每次就可以选择价值最高的一块区域进行垃圾收集,保证了能在有限时间内达到尽可能高的效率

4.避免全堆扫描:G1收集器为每一个区域都维护有一个叫Remember set的列表,当发生引用操作时,G1会判断引用的对象是否在同一个区域内,若不在,那么就要在对应区域的Remember set上记录相关的引用信息。那么在进行标记的时候,就可以避免全堆扫描,同时不发生遗漏。

内存分配策略

大对象优先在Eden区进行分配

当Eden区没有足够空间时,会引起一次minor GC

大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,比如长字符串,或者数组。

为了避免大对象在新生代间进行多次复制,所以大对象要直接进入老年代

-XX:PretenureSizeThreshold参数可以设置,大于该值的对象直接在老年代分配。

长期存活的对象将进入老年代

虚拟机给每个对象维护一个年龄计数器,当对象在Eden区出生,经历第一次minor GC依然存活,并且可以被survivor区容纳的话,对象年龄被设为1,每熬过一次minor GC,年龄就+1.当年龄到达一个阈值,该对象就会晋升到老年代中,这个阈值可以通过-XX:MaxTenuringThreshold设置

动态对象年龄判定

在一些情况下,并不是一定要对象年龄超过阈值才能进入老年代。如果survivor区中,同年龄的所有对象大小超过了survivor内存的一半,年龄大于等于该年龄的对象可以直接进入老年代,无需等待达到阈值。

空间分配担保

在发生Minor GC之前,虚拟机首先会查看老年代的最大可用连续空间是否大于新生代中所有对象总空间,如果是,则认为这次minor GC是安全的。否则的话,如果允许担保失败的话,虚拟机再查看老年代中最大可用连续空间是否大于历次晋升老年代所用内存的平均值,若是的话,就尝试进行minor GC,尽管这次执行是有风险的,若执行失败,则发生 Full GC;若不允许担保失败,此时会直接发生Full GC。

发布了60 篇原创文章 · 获赞 7 · 访问量 3886

猜你喜欢

转载自blog.csdn.net/SCUTJAY/article/details/104412269
今日推荐