JVM垃圾回收器(三)

垃圾回收知识点

引用计数

给对象添加一个引用计数器,每当一个地方引用这个对象,这个计算器就加1.如果引用失效,那计算器就减1。如果计算器数量为0,那这个对象就是失效的。
但是如果2个对象虽然不用了,但是互相引用,就会导致互相的引用计数都不等于0,大致GC没法回收对象

优点:实现简单、判断效率高。
缺点:很难解决对象之间循环引用的问题。例如下面这个例子

可达性分析

JVM 在进行对象回收的时候,需要判断这个对象是否还在被使用,可以通过GC Roots Tracing 去叛变
可达性分析

GC Roots 对象包括如下几种:

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

优点:更加精确和严谨,可以分析出循环数据结构相互引用的情况;
缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

4种引用对象

引用类型 用法 描述
强引用 Object obj = new Object() 当有变量引用时,内存不足,JVM宁可抛出OutOfMemory 也不会去回收此对象
软引用 SoftReference softRef= new SoftReference(object) 用来描述有用但是非必须的对象,就是当内存快溢出的时候,才会回首软引用关联的对象
弱引用 WeakReference weakRef= new WeakReference(object) 用来描述一些非必须的对象 弱引用关联的对象只能存活到下一次垃圾回收之前,不管当前的内存是否足够,都会回收
虚引用 ReferenceQueue refQueue= new ReferenceQueue(object) 就是为了设置引用对象被GC 回收的时候 获得系统通知

垃圾回收算法

标记清除算法

分为标记和清除2个步骤,首先要标记处所有要回收的对象,标记完成后回收所有标记的对象
标记清除算法

缺点:一是标记和清除的效率都不高,二是 会产生大量的内存碎片

标记整理算法

标记整理算法和标记清除算法的步骤一样,只是后续步骤不是直接回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理端边界以外的内存
标记整理算法

复制算法

将内存按照容量大小分成相等的2块,每次只使用其中一块,每当一块内存使用完了以后就将存活的对象复制到另外一块上面,然后在把之前用过的内存空间一次性清理掉
复制算法
缺点:内存使用量会缩小为原来的一半,内存使用率降低

分代垃圾算法

目前的商业虚拟机都是采用分代收集的算法,根据对象存活周期的不同划分为几块区域,一般都是将java堆区 划分为新生代和老年代,这样的话可以根据各个年代的特点采用不同大算法

新生代中大都数对象都是朝生夕死的,能存活下来的知识少数,这个时候优选复制算法,只要复制少量存活的对象就可以完成清理工作。
在老年代中,对象的存活几率很高,没有额外的空间进行分配,所以采用标记-清除和标记-整理算法进行回收

在这里插入图片描述

JDK 垃圾回收器

默认的垃圾回器

  • 1.7 Parallel Scavenge+ Parallel Old
  • 1.8 Parallel Scavenge+ Parallel Old(G1 也可以用)
  • 1.9 G1

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、Parallel Old、 CMS

整堆收集器: G1

在这里插入图片描述相互连线了的才能互相配合,上面的是新生代收集器,下面的是老年代收集器

Serial

单线程的收集器,单线程的意义并不仅仅是指的是使用一个CPU或者一条收集指令去完成垃圾收工作,单线程是指在进行垃圾收集回收的时候必须停止其他工作线程,直至它收集工作的完成

特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

应用场景:适用于Client模式下的虚拟机。
Serial收集器

ParNew

ParNew是Serial收集器的多线程版本,除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。Server首选的新生代垃圾收集器

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。

ParNew收集器

Parallel Scavenge

Parallel Scavenge 它也是一个使用复杂算法的收集器,此收集器关注点和其他收集器不同,它的目标是达到一个可控制的的吞吐量

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。

吞吐量计算公式:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Parallel Scavenge收集器
垃圾收集停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge提供了两个参数用于精确控制吞吐量

  • -XX:MaxGCPauseMillis // 最大垃圾收集停顿时间(大于0毫秒数)
  • -XX:GCTimeRatio / 吞吐量大小(大于0且小于100的整数,吞吐量百分比)
  • -XX:+UseAdaptiveSizePolicy //内存调优委托给虚拟机管理。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、 晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

Serial Old

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。

特点:同样是单线程收集器,采用标记-整理算法。

应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。

Serial收集器

Parallel Old

Parallel Old 是Parallel Scavenge老年版本的垃圾收集器。使用多线程标记整理算法 这个收集器是从JDK1.6以后才开始提供

特点:多线程,采用标记-整理算法。

应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
Parallel Old收集器

Concurrent Mark Sweep

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目的的收集器。

CMS收集器的特点是:基于标记-清除算法实现。并发收集、低停顿

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器
整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
  2. 并发标记(CMS concurrnet mark)进行GC RootsTracing,找出存活对象且用户线程可并发执行。
  3. 重写标记(CMS mark) 改阶段是为了为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,改阶段的时间比初始标记长,并发标记短
  4. 并发清除(CMS concurrent sweep)清理垃圾对象,该阶段收集器线程是和用户线程并发执行

缺点

  • 使用的是标记清除算法,会产生内存碎片。导致大对象无法分配空间,不得不提前触发一次Full GC。
  • 对CPU资源非常敏感,CPU 越少对程序的影响越大。默认的回收线程是:(CPU核心数+4)/4 向上取整
  • 浮动垃圾 在并发清理的时候 线程还是在运行的 必须预留内存给用户线程,如果预留的空间不能满足用户线程会报一个Concurrent Mode Failure 会使用Serial Old 去进行垃圾回收

G1

G1算法将堆划分成若干得个区域,它任然是一个分代的收集器,不过这些区域的一部分包含新生代,新生代垃圾收集器任然采用暂停所有应用线程的方式,将存活的对象Copya到老年代或者幸存代。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成清理工作,这就意味这个,在正常处理过程中G1完成了堆的压缩,这样就不会有CMS内存碎片问题的的存在了,适合场景:服务端应用

G1特点

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
在这里插入图片描述

G1的运行步骤

G1收集器运行示意图
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

G1 的问题

G1为什么能建立可预测的停顿时间模型?

因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。

G1与其他收集器的区别:

其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。

G1收集器存在的问题:

Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。

G1收集器是如何解决上述问题的?

采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。

G1 提供了2中模式

YoungGC

  1. 根扫描。静态和本地对象被扫描
  2. 更新RS(Remembered Set)。找出老年代到年轻代的引用并更新RS
  3. 处理RS。检测从年2轻代指向年老代的对象
  4. 对象拷贝。拷贝存活的对象到survivor/old区域
  5. 处理引用队列。软引用,弱引用,虚引用处理

Mix GC

  1. 初始标记 该阶段仅仅只是标记一下GC Roots能直接关联到的对象。
  2. 并发标记 该阶段是从GCRoot开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  3. 最终标记 该阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
  4. 筛选回收 该阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。因为只回收一部分Region,所以时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

垃圾回收机制

对象优先分配到Eden区

大多数情况下对象是先被分配到Eden区的 当Eden区大小不够分配的时候就会触发一次MinorGC

大对象直接分配到老年代

对象是指需要大量连续内存的java对象,比如很长内容的字符串和很大的数组等,大对象会导致明明内存充足,但是还是会提前触发GC来获取连续的存储内存空间,开发中 我们要避免这种情况

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

虚拟机给没一个对象定义了一个年龄Age计算器,如果对象从出生在Eden区经历过一次MinorGC后任然存活就会转移到Survivor空间中,会将年龄+1,当年龄增加到一定程度(年龄15)就会晋升到老年区

动态年龄判断

虚拟机 并不是永远要求对象年龄达到MaxTenuringThreshold才能晋升到老年代,动态年龄判断是基于如下2点:

  • 如果在Survivor空间中存在相同年龄的所有对象大小的总和大于Survior空间的一半
  • 那么年龄大于或者等于该年龄的对象就可以直接进入老年代,无序等到MaxTenuringThreshold要求的年龄

==举一个例子:Survivor:10M MaxTenuringThreshold:10 当前Survivor 中存在 1个一岁的 A 1M 3个2岁的B 6M 1个三岁的C 1M 那这个时候B,C都可以直接进入到老年代,无序等到10岁 ==

注意:动态年龄判断和TargetSurvivorRatio有关系,只有当Survivor区中从小到大年龄的对象累加的大小大于 Survivo*TargetSurvivorRatio(默认50%)的时候,则这个年龄之上的对象都要晋升到老年代,方便Eden区到Survivo的效率

空间担保

尽量减少FullGC频率

注意:在Jdk1.6 update 24之后-XX:-HandlePromotionFailure 不再起作用,只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就执行MinorGC,否则执行FullGC

关于GC

Minor GC

Minor GC 用来清理新生代空间 Minor GC会非常频繁也非常快,因为很多对象都是朝生夕灭的
触发条件:当新生代没法给新对象分配空间了,比如Eden区满了

Major GC

Major GC 主要是为了清理老年代的,Major GC速度会比Minor GC慢很多。触发条件:老年代满了

Full GC

FullGC清理新生代+老年代(方法区)Full GC本身不会先进行Minor GC,可以配置Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,可以让CMS remark之前先进行一次Minor GC。

Full GC 触发条件:

  1. 手动的去调用 System.gc
  2. 老年代空间不足
  3. 方法区空间不足
  4. 经过MinorGC后,进行的对象大小大于老年代的可用空间。如:Eden+From到To复制的时候,对象的大小大于To区域大小,当把对象转到老年,这个时候老年代的空间小于对象大小。

JVM 一些设置参数

堆区的设置

  1. -Xmx:最大堆大小
  2. -Xms:初始堆大小
  3. -XX:NewSize 设置新生生代的最小空间
  4. -XX:MaxNewSize 设置新生代的最大空间+
  5. -Xmn:新生代大小 老年代大小可以用堆大小减去新生代大小算出 在JDK1.4以才支持 设置了以后NewSize=MaxNewSize=Xmn
  6. -XX:NewRatio:设置新生代和老年代的比值 如果是3 表示新生代和老年代的为1:3
  7. -XX:SurvivorRatio: 设置新生代中的Eden区和2个Survior区的比值。如果设置是3的话,那Eden和2个Survior区比值为3:1:1
  8. -XX:MaxTenuringThreshold 表示新生代转化为老年代的存活次数 如果是0 就直接到老年代中
  9. -XX:PretenureSizeThreshold=XX 是设置对象直接进入老年代的阈值,但是这个参数只是对Serial和ParNew回收器有效,对Parallel Scavenge收集器无效,此收集器一般不需要设置,如果需要建议使用ParNew+CMS的组合

非堆区的设置

  1. -XX:PermSize, -XX:MaxPermSize :设置方法区空间大小大小 1.8之前
  2. -XX:MetaspaceSize、-XX:MaxMetaspaceSize 分配设置元空间(MetaspaceSize)的最小和最大值 1.8之后
  3. -Xss:设置每个线程的栈区大小

收集器设置

  1. -XX:+UseSerialGC:设置串行收集器 老年代对应的是:Serial Old 适用场景:用户的桌面应用场景
  2. -XX:+UseParNewGC: Serial收集器的多线程版本 适用场景:Server首选的新生代收集器
  3. -XX:+UseParallelGC:设置并行收集器 适用场景:后台计算不需要太多交互
  4. -XX:+UseParalledlOldGC:设置并行老年代收集器 适用场景:用户的桌面应用场景
  5. -XX:+UseConcMarkSweepGC:设置老年代CMS收集器 适用场景:互联网站或者WEB服务端

垃圾回收统计信息

  1. -XX:+PrintGC
  2. -XX:+PrintGCDetails
  3. -XX:+PrintGCTimeStamps
  4. -Xloggc:filename
  5. -XX:+PrintTenuringDistribution survivor区中对象的年龄分布

备注:该文章参考了云析学院的课件和https://www.cnblogs.com/chenpt/p/9803298.html 这边文章

猜你喜欢

转载自www.cnblogs.com/burg-xun/p/12711998.html