JVM(四)垃圾回收机制

2020-07-20  本文已影响0人  hadoop_a9bb

1.GC基本概念

在java中,程序员是不需要显式的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中有一个垃圾回收线程,它是优先级低的,在正常情况下是不会执行的,只有在虚拟机空闲或当堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

垃圾回收器(Gabage Collection)负责:
1.分配内存。
2.保证所有正在被引用的对象还存在于内存中。
3.回收执行代码已经不再引用的对象所占的内存。

1.可以主动垃圾回收吗?

每个Java应用程序都有一个Runtime类实例,java.lang.System.gc()只是调用了java.lang.Runtime.getRuntime().gc()的简写。这个命令只建议JVM安排GC运行,还有可能完全被拒绝。
GC本身是会周期性的自动运行的,由于JVM决定运行的时机,而且现在的版本有多种更智能模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化。

2.Stop-The-World(STW)

GC在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿,会给用户不良的体验。
可达性分析的时候为了确保快照一致性,需要对整个系统进行冻结,不可以出现分析过程中对象引用关系还在不断变化的情况,也就是stop the world。


2.如何皮判断对象是否可回收?

1.如何判断一个对象已失效

1.引用计数法:引用计数,很难解决循环引用问题,优点是效率高,算法简单。
2.可达性分析(JVM采用的算法):通过GC Roots对象作为起始点,搜索走过的路径(引用链),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。


可达性分析算法
2.什么可以当GC Roots?

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

3.四种引用类型
  • 1.强引用(Strong Reference):如Object boj = new Object();这类引用时Java程序中最普遍的。只要引用存在,垃圾收集器就无法回收。
  • 2.软引用(Soft Reference):还有用但非必须的对象,在系统发生内存溢出之前会将这些对象回收。如果回收还没有足够的内存,才会抛出内存溢出异常。
  • 3.弱引用(Weak Reference):非必须的对象,强度比软引用更弱,只能生存到下次垃圾回收之前,无论内存是否够用,都会回收。
  • 4.虚引用(Phantom Reference):最弱的引用关系,作用是跟踪垃圾回收过程。
4.不可达的对象并非“非死不可”

要真正宣告一个对象死亡,至少要经历两次标记过程:

1.对象在可达性分析后被发现了不可达,它将会被第一次标记并进行筛选,筛选条件是次对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,那么就没必要执行finalize()方法。如果被判定为有必要执行finalize()方法,那么此对象将会放置在F-Queuen队列中,并在稍后由一个虚拟机建立的、低优先级的Finalize线程去触发这个方法。

2.稍后GC将堆F-Queuen中的对象进行第二次小规模的标记,如果对象要在finalize()中拯救自己(如何拯救自己?在finalize()方法中,使用this关键字赋值给某个变量),只要重新与引用链上的任何一个对象建立关系即可,那么第二次标记时它将被移出"即将回收"集合。finalize()方法是对象逃脱死亡的最后一次机会,如果对象这时候还没有成功逃脱,那它就会真的被回收了。

5.方法区回收,如何判断一个类是无用类?
  • 1.该类的所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 2.加载该类的ClassLoader已经被回收了。
  • 3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类。
    虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是"可以",而并不是和对象一样不使用了就会必然被回收。

3.堆内存常见分配回收策略


4.虚拟机的GC过程

1.什么时候进行垃圾回收
  • 1.当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代,需要强调的是年轻代满指的Eden区满,Survivor区满不会引起GC。
  • 2.当年老代满时会引起Full GC,Full GC会同时回收年轻代、年老代
  • 3.当永久代满时,也会引发Full GC,会导致Class、Method等元信息卸载。
  • 4.手动调用Runtime().gc()或System.gc() 通常会触发一次Full GC。
2.Minor GC

新生代GC,指发生在新生代的垃圾收集动作。
因为Java对象大多朝生夕死,所以Minor GC非常频繁,回收速度也快。
每次垃圾回收都有大批对象死亡,只有少量存活,采用复制算法。
触发条件:Eden区满,触发Minor GC。

3.Full GC

Full GC速度一般比Minor GC慢10倍以上。
对象存活率高,没有额外的空间对其分配担保,采用标记-清除、标记-整理算法。

Full GC触发条件:

  • 1.调用System.gc()
  • 2.老年代空间不足
  • 3.方法区空间不足
  • 4.给survivor区做空间分配担保,并且担保内存大于可用内存。

发现虚拟机频繁full gc 怎么办
1.首先使用命令jstat -gccause 查看gc触发原因
2.如果是system.gc()查看哪里调用
3.如果是heap inspection(内存检查),可能是哪里执行了jmap -histo[:live]命令
4.如果是GC locker,可能是程序依赖的JNI库的原因。
5.内存泄漏或者分配内存太小

4.虚拟机的GC过程。

1.在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。

2.当Eden区满了时,minor GC被触发。

3.经过扫描与标记,存活的对象被复制到S0区,清空Eden。


第一次minorGC

4.在下一次minorGC中,Eden区和上面情况一致,存活的对象被复制到survivor区,Eden区清空,然而在Survivor区S0的存活的数据复制到S1,Eden区存活的数据也复制到S1,并且对象年龄对应增加。清空Eden和S0区域,过程如下图:


minorGC常规收集

5.再下一次重复上一步过程,这次survivor的两个区对换,年龄继续增加。


minorGC常规收集

6.在经过几次minorGC后,当存活对象年龄到达阈值(参数配置,默认15,下图中为8),就会从年轻代晋升到老年代。


对象晋升

7.随着不断minorGC,不断会有新的对象被晋升到老年代


对象晋升

8.上面基本覆盖了整个年轻代所有的回收过程。最终老年代的空间在回收时将会被清除,整理。


年轻代:Eden区是连续空间,且Survivor总有一个为空,采用复制算法,这种基于大部分对象存活周期很短的情况下,效率极高,但是它也只是在这种情况下,如果在老年代采用复制算法,则非常不适合。

老年代:老年代存储的对象比年轻代多,而且不乏大对象,老年代一般采用标记-清除、标记-整理算法。-XX:+HandlePromotionFailure(允许担保失败)

永久代:存放静态文件,元信息,Java类,方法等。回收有两种:常量池中的常量,无用的类信息。永久代回收对内存没有显著的影响,但是有些使用cglib、动态代理、反射、String.intern会造成永久代有大量的无用数据。永久代回收并不是必须的。可以通过参数设置是否进行回收。常量的回收很简单,没有引用了就被回收。类需要满足:堆中无实例、类加载器被回收、Class信息无引用。

5.回收算法

1.按照回收策略分

1.标记清除(mark-sweep)
该算法分为标记和清除两个阶段,首先标记出所有需要回收的对象(可达性分析),在标记完成后统一清理掉所有与被标记的对象。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。


标记-清除算法

缺点:
1.效率不高,标记和清除阶段效率都不高。
2.产生空间碎片,空间碎片太多,导致分配大对象时无法找到连续内存触发垃圾回收。

2.复制(Copying)
此算法把内存分为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前区域,把正在使用中的对象复制到另外一个区域。


复制算法

优点:

缺点:

3.标记整理(Mark-Compact)
该算法分为标记和清除两个阶段:首先标记出所有需要回收的对象(可达性分析),在标记完成后让所有存活对象都移动到一端,然后清理掉边界以外的内存。此算法结合了标记-清除 和 复制 两个算法优点。


标记整理算法

优点

缺点

2.按分区的方式分

1.增量收集(Incremental Collecting)
实时垃圾回收算法,即在应用进行的同时进行垃圾回收。
2.分代收集(Generational Collecting)
把对象分为新生代,老年代、持久带,对不同区域使用不同算法进行回收。
为什么要分代?
新生代:每次回收大量对象死亡,使用复制算法,只付出少量对象复制成本就可以完成GC
老年代:存活几率高,没有额外空间进行担保,必须选择(标记-清除、标记整理)进行GC。

3.按系统线程分(回收器类型)

6.垃圾收集器

垃圾收集器是一个守护线程,优先级低,其在当前系统空闲或堆中无用数据较多时触发。
HotSpot虚拟机所有收集器及组合,如下图:


组合关系
新生代收集器
老年代收集器
收集器总结
收集器相关参数
新生代还是老年代收集器

1.新生代收集器:Serial ParNew Parallel Scavenge
2.老年代收集器:Serial Old, Parallel Old CMS

吞吐量优先

吞吐量优先:Parallel Old,Parallel scavenge
响应时间优先:CMS G1

串行并行并发

串行:Serial ,Serial Old
并行:ParNew ,parallel scavenge,parallel Old
并发:CMS ,G1

算法

复制:serial,parNew,parallel scavenge,G1
标记清除:CMS
标记整理:serial Old,parallel Old,G1

1.Serial收集器

Serial(串行)收集器是最基本、发展历史最悠久的收集器。JDK1.3前是HotSpot新生代收集的唯一选择。
特点:串行(单线程),新生代,复制,STW,响应速度优先
适用环境:单CPU环境下Client模式


Serial收集器
2.ParNew收集器

ParNew收集器是多线程版本的Serial。除了Serial之外,目前只有它能够与CMS收集器配合工作。
特点:并行(多线程)、新生代、复制、STW、响应速度优先
应用场景:多CPU环境下在Server模式下与CM配合


ParNew收集器
3.Parallel Scavenge收集器

Parallel Scavenge垃圾收集器与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。JDK1.8Server模式默认收集器。
特点:并行(多线程)、新生代、复制、STW、吞吐量优先
应用场景:在后台运算而不需要太多交互的任务


parallel scavenge收集器

Parallel Scavenge与CMS区别
Parallel Scavenge关注吞吐量,CMS关注响应时间。Parallel Scavenge不能配合CMS使用。

Parallel Scavenge与ParNew区别
一个重要区别是Parallel Scavenge有自适应策略。

4.Serial Old收集器

Serial收集器的老年代版本
特点:串行(单线程),老年代、标记-整理,STW,响应速度优先
应用场景:单CPU环境下的Client模式,CMS后备预案(并发收集发生“Concurrent Mode Failure”)。


Serial Old收集器
5.Parallel Old收集器

Parallel Old收集器是parallel scavenge收集器的老年版本JDK1.6开始提供。JDK1.8默认老年代回收器。
特点:并行(多线程),老年代,标记-整理,STW,吞吐量优先
应用场景:在后台运行而不需要太多的交互。


parallel Old收集器
6.CMS(Concurrent Mark Sweep)收集器

CMS是HotSpot在JDK5推出的第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作。
特点:并发(多线程),老年代,标记-清除算法(不进行移动,产生内存碎片)收集过程中不需要暂停用户线程,以最短停顿时间为目标。
应用场景:与用户交互多的场景,重视相应速度和用户体验的应用。


CMS回收器

CMS并非没有暂停,而是用短暂停来替代串行标记整理算法的长暂停。
CMS GC收集周期分为四步完成:

1.初始标记


初始标记

这个阶段任务是标记老年代中被GC roots直接可达的对象,和GC root s直接可达的年轻代对象 中 引用的老年代对象,这个接断是STW发生的阶段。
特点:单线程执行,需要Stop The World;仅把GC roots直接关联的对象给标记下,数量较少,所以速度非常快。

2.并发标记


并发标记

这个阶段通过从初始标记阶段找到的标记对象开始,遍历老年代同时标记所有存活对象(可达性分析)
特点:这个阶段与应用程序共同运行,其他线程仍可继续工作。此处时间较长,但不停顿。
并不保证可以标记所有存活对象,因为程序在运行期间可能会更改引用。

3.执行预清理(Concurrent Pre clean)

4.执行可终止预清理(Concurrent Abortable Pre clean)

5.重复标记
这个阶段是第二次STW,这个阶段目的是标记在老年代中被标记的所有存活下来的对象。
在并发标记过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。修正并发期间产生变动的那一部分对象标记。
此处执行并发标记,用户线程不并发,所以依然是"Stop The World",且停顿比初始标记稍长,但远比并发标记短。

6.并发清除


并发清除

7.并发重置(Concurrent Reset)
重置CMS内部数据结构,为下一个周期做准备。

CMS回收过程1,达到安全点开始回收
CMS回收过程2,初始标记单线程
CMS回收过程3,并发,执行可达性分析
CMS回收过程4,重新标记,并行,STW
CMS回收过程5,并发清除,并发
CMS回收过程6,重置线程,CMS垃圾回收线程恢复到最开始状态

缺点:

7.G1收集器

G1(garbage First)名称的由来是G1跟踪各个Region里面的垃圾的价值大小(回收所获得的空间大小以及回收所需时间的比值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

G1收集器与前面的垃圾收集器有很大不同,它把新生代、老年代的内存划分取消了。这样我们再也不用单独的空间对每个代进行设置。不用担心每个代内存是否足够。


原JDK堆内存划分不使用

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过这些区域一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程方式,将存活对象拷贝到老年代或Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另一个区域,完成清理工作。这就意味着。在正常处理的过程中,G1完成了堆的压缩,这样就不会有CMS的内存碎片问题。

G1内存堆区域结构

G1中,还有一种特殊的区域,叫Humongous区域,如果一个对象占用的空间超过了分区容量的50%以上,G1收集器就认为这是巨型对象,这些巨型对象默认直接会被分配在老年代,但如果它是一个短期存在的巨型对象,就会对垃圾收集产生负面影响。为了解决这个问题,G1划分了一个Humongous区,它专门存放巨型对象,如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

1.特点
G1除了降低停顿之外,还能建立可预测的停顿时间模型。

2.应用场景
如果追求高响应,那么G1可续选择,如果追求吞吐量,G1并没有什么特别的好处。

3.运行过程
可分为4个步骤(与CMS较为相似)


G1收集过程
  • 1.初始标记:标记GC Roots能直接关联到的对象。速度很快。需要STW (OopMap)
  • 2.并发标记:进行可达性分析,从刚才产生的集合中标记出存活对象。耗时较长,并行。并不能保证标记出所有存活对象。
  • 3.最终标记:和CMS相同,为了修正并发标记期间因用户程序继续运作而导致及标记产生变动部分。也需要STW,停顿时间比初始标记长,但时间远比并发标记短。并行。
  • 4.筛选回收:
    1.首先排序各个Region回收的价值和成本。然后根据用户期望的GC时间来制定回收计划,最后按计划回收。回收时采用复制算法,从多个Region复制存活对象到堆上的另一个Region空间,并在此过程中压缩和释放内存。并发。
8.ZGC

在JDK11中,加入了实现性质的ZGC,它平均回收耗时不到2毫秒。是一款低停顿高并发的收集器。
ZGC几乎在所有地方并发执行,除了初始标记的是STWde .

ZGC的设计目标
1.TB级别的堆内存管理。
2.最大GC Pause 不高于10ms。
3.最大吞吐率损耗不高于15%。
4.关键点:GC Pause不会随着堆增加而增大。

上一篇 下一篇

猜你喜欢

热点阅读