JVM-垃圾收集器与内存分配策略

对象存活判定方法

引用计数算法

对象引用计算器,引用加1,失效减1。计数为0表示对象死亡。

JVM不采用,因为互相引用导致循环引用问题

可达性分析算法

GC Roots为起点,从这些起点开始向下搜索,经过的路径称为引用链。若一个对象到GC Roots之间没有任何引用链,则该对象是不可达的。

JVM-垃圾收集器与内存分配策略_第1张图片

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

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

在可达性分析过程中,对象引用类型会对对象的生命周期产生影响

JAVA中有这几种类型的引用:

  1. 强引用:只要该引用还有效,GC就不会回收
  2. 软引用:内存空间足够时不进行回收,在内存溢出发生前进行回收、用SoftReference类实现
  3. 弱引用:弱引用关联的对象只能存活到下一次GC收集、用WeakReference类实现
  4. 虚引用:无法通过虚引用获得对象实例,也不会对对象的生存时间产生影响、唯一目的:当该对象被GC收集时,收到一个系统通知。用PhantomReference类实现

对象被回收的过程

  • 当对象进行可达性分析没有与GC Roots相连的引用链,将会被第一次标记,并根据是否需要执行finalize()方法进行一次筛选,对象没有重写finalize()或者虚拟机已经调用过finalize(),都被视为不需要执行
  •  如果对象有必要执行finalize,会被放入到F-Queue队列中,并在稍后由虚拟机自动创建的低优先级的Finalizer线程去触发它,并不保证等待此方法执行结束
  • 如果对象在finalize()方法执行中,重新和GC Roots产生了引用链,则可以逃脱此次被回收的命运,但finalize()方法只能运行一次,所以并不能通过此方法逃脱下一次被回收
  • 不建议使用这个方法,建议大家完全忘掉这个方法的存在。

 方法区回收

主要包括废弃常量无用类的回收。

满足以下三个条件,类才可以被回收(卸载):

  • 类的实例都被回收,
  • 类的ClassLoader被回收,
  • 类的Java.Lang.Class对象没有在任何地方引用。

 HotSpot虚拟机通过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,需要虚拟机具备类卸载功能,避免方法区发生内存溢出。

垃圾收集算法

标记-清除(Mark-Sweep)

分为两个阶段

  • 标记:先标记出所有要回收的对象
  • 清除:统一进行对象的回收
JVM-垃圾收集器与内存分配策略_第2张图片 标记-清除算法

缺点

  • 效率问题:标记和清除的效率都不高。
  • 空间问题:会产生大量不连续的内存碎片,碎片太多会都导致大对象无法找到足够的内存,从提前触发垃圾回收。

复制(Copying)

把内存分成大小相同的两块,当一块的内存用完了,就把可用对象复制到另一块上,将使用过的一块一次性清理掉

JVM-垃圾收集器与内存分配策略_第3张图片 复制算法

缺点:浪费了一半内存

标记-整理(Mark-Compact)

标记后,让所有存活的对象移到一端,然后直接清理掉端边界以外的内存

JVM-垃圾收集器与内存分配策略_第4张图片 标记--整理算法

分代收集

把堆分为新生代和老年代

  • 新生代使用复制算法

将新生代内存分为一块大的Eden区和两块小的Survivor;每次使用Eden和一个Survivor,回收时将Eden和Survivor存活的对象复制到另一个Survivor(HotSpot的比例Eden:Survivor = 8:1)。可以通过JVM参数调整。

  • 老年代使用标记-清理或者标记-整理

HotSpot的算法实现

存活判定与收集算法仅是理论上,JVM实现时必须考虑效率,才能保证JVM高效运行。

枚举根节点

GC Roots的节点主要在全局性引用(常量,静态属性)与执行上下文(本地变量表)中,逐个检查消耗很多时间。

可达性分析对执行时间的敏感还表现在GC停顿上,GC时必须停顿所有Java执行线程(“STOP THE WORLD”)

 OopMap数据结构记录哪些位置是引用。类加载完成时或JIT编译时,GC时直接扫描OopMap。

安全点(Safepoint)

在OopMap协助下,HotSpot可以快速准确完成GC Roots枚举。

特定位置记录信息,称为安全点。程序执行时并非在所有地方都可以停顿下来开始GC,只有在到达安全点时才能暂停

安全点选定以程序“是否具有让程序长时间执行的特征”为标准。“长时间执行”的最明显特征是指令序列复用。例如方法调用,循环跳转,异常跳转。

如果在GC发生时让所有线程(不包括执行JNI的线程)到最近的安全点上再停顿下来。2种方案:

  • 抢先式中断(Preemptive Suspension)

首选中断所有线程,有线程未在安全点中断,恢复线程运行到中断点。(几乎不采用)

  • 主动式中断(Voluntary Suspension)

需要GC时,不中断线程,设置标志位,由线程主动轮询,发现中断标识时主动挂起。(轮询标识的地方和安全点重合)。

 

安全区域(Safe Region)

程序不执行时,即没有分配到CPU时间,线程处于Sleep或者Blocked状态,线程无法响应JVM中断,JVM也不会等待线程重新获取CPU时间。对于这种情况采用安全区域来解决。

安全区域是指在一段代码片段中,引用关系不会发生变化,在这个区域任何地方开始GC都是安全的,可看作扩展安全点。

在线程执行到安全区域时,首先标识自己已进入安全区域,GC时就不考虑这些线程了,在线程离开安全区域时,要检查系统是否已完成根节点枚举(或整个GC),如果完成了,线程就继续执行,否则就必须等待接收到可以离开安全区域的信号。

垃圾收集器

收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

JVM-垃圾收集器与内存分配策略_第5张图片

如果两个收集器之间存在连线说明他们之间可以搭配使用

Serial 收集器

这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。

JVM-垃圾收集器与内存分配策略_第6张图片

新生代采用复制算法

老年代采用标记-整理

ParNew 收集器

可以认为是 Serial 收集器的多线程版本。

JVM-垃圾收集器与内存分配策略_第7张图片

并行:Parallel,指多条垃圾收集线程并行工作,此时用户线程处于等待状态

Parallel Scavenge 收集器

这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

参数:

  • -XX:MaxGCPauseMillis :最大GC停顿时间(毫秒)
  • -XX:GCTimeRation:吞吐量大小(0~100)

作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

Serial Old 收集器

JVM-垃圾收集器与内存分配策略_第8张图片

Serial 收集器的老年代版本,单线程,使用标记-整理

这个收集器的主要意义:给Client模式下的虚拟机使用。

Server模式下,它还有2大用途:

  • 与Parallel Scavenge收集器搭配使用
  • 作为CMS收集器后备预案,在发生Concurrent Mode Failure时使用。

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记-整理

JVM-垃圾收集器与内存分配策略_第9张图片

CMS(Concurent Mark Sweep) 收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记-清除 算法实现。

JVM-垃圾收集器与内存分配策略_第10张图片

运作步骤:

  1. 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
  2. 并发标记(CMS concurrent mark):进行 GC Roots Tracing
  3. 重新标记(CMS remark):修正并发标记期间的变动部分
  4. 并发清除(CMS concurrent sweep)

缺点:

  • 对 CPU 资源敏感。

默认回收线程数:(CPU数量+3) /4

  • 无法收集浮动垃圾

第一次标记后出现的未处理垃圾称为浮动垃圾,不能等到老年代填满了再收集,如果预留内存无法满足用户线程需要,会产生Concurrent Mode Failure异常。

  • 标记-清除算法带来的空间碎片

更多细节参考图解 CMS 垃圾回收机制原理

G1 收集器

JVM-垃圾收集器与内存分配策略_第11张图片

优点

  • 并行与并发:通过并发方式让Java程序继续运行
  • 分代收集
  • 空间整合:全局上看采用标记-整理,从局部(2个Region之间)基于复制算法
  • 可预测停顿:建立可停顿模型,让使用者指定在长度M时间段内,GC不超过N。

其他收集器的范围在某个代,G1不再这样,它将整个内存分为多个大小相等的独立区域(Region),虽然还保留新生代,老年代,但是新生代,老年代不再物理隔离,都是一部分Region(不需要连续)的结合。

运作步骤:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

理解GC日志

阅读 GC 日志是处理 JVM 内存问题的基础技能,它只是一些人为确定的规则,每一种收集器的日志形式都是由它们自身的实现所决定的,但 JVM 设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如下面两段典型的日志:

33.125: [GC [DefNew: 3324K->152K (3712K), 0.0025925secs] 3324K->152K (11904K), 0.0031680 secs]
100.667: [FullGC [Tenured: 0K->210K (10240K), 0.0149142secs] 4603K->210K (19456K), [Perm: 2999K->2999K (21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]

数字 “33.125” 和 “100.667” 表示从 JVM 启动以来经过的秒数

“[GC” 和 “[Full GC” 说明了这次垃圾收集的停顿类型,而不是用来区分新生代 GC 还是老年代 GC 的。如果有 “Full”,说明这次 GC 是发生了 “Stop The World” 的,例如下面这段新生代收集器 ParNew 的日志也会出现 “[Full GC” (一般是因为出现了分配担保失败之类的问题,导致 STW)。如果是调用 System.gc() 方法所触发的 GC,那么在这里将显示 “[Full GC (System)”。

[Full GC 283.736: [ParNew: 261599K->261599K (261952K),0.0000288 secs]

“[DefNew”、”[Tenured”、”[Perm” 表示GC发生的区域,这里显示的区域名称与使用的 GC 收集器是密切相关的,例如上面样例所使用的 Serial 收集器中的新生代名为 “Default New Generation”,所以显示的是 “[DefNew”。
如果是 ParNew 收集器,新生代名称就会变为 “[ParNew”。
如果是 Parallel Scavenge 收集器,新生代名称就会变为 “PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的 “3324K->152K (3712K)” 即 “GC 前该内存区域已使用容量->GC 后该内存区域已使用容量 (该内存区域总容量)”。
而在方括号之外的 “3324K->152K (11904K)” 即 “GC 前 Java 堆已使用容量->GC 后 Java 堆已使用容量 (Java堆总容量)”。

再往后,”0.0025925 secs” 表示该内存区域 GC 所占用的时间,单位是秒

有的收集器会给出更具体的时间数据,如 “[Times: user=0.01 sys=0.00,real=0.02 secs]”,这里面的 user、sys 和 real 与 Linux 的 time 命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间 (Wall Clock Time)。

CPU 时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘 I/O、等待线程阻塞,而 CPU 时间不包括这些耗时,但当系统有多 CPU 或者多核的话,多线程操作会叠加这些 CPU 时间,所以读者看到 user 或 sys 时间超过 real 时间是完全正常的。
 

垃圾搜集器参数

垃圾搜集器选择参数

名称 说明 备注
UseSerialGC 开启此参数使用serial & serial old搜集器(client模式默认值)。  
UseParNewGC 开启此参数使用ParNew & serial old搜集器(不推荐)。  
UseConcMarkSweepGC 开启此参数使用ParNew & CMS(serial old为替补)搜集器。  
UseParallelGC 开启此参数使用parallel scavenge & parallel old搜集器(server模式默认值)。  
UseParallelOldGC 开启此参数在年老代使用parallel old搜集器(该参数在JDK1.5之后已无用)。  

JVM各个内存区域大小相关参数

名称 说明 备注
Xms 堆的初始值。默认为物理内存的1/64,最大不超1G。  
Xmx 堆的最大值。默认为物理内存的1/4,最大不超1G。  
Xmn 新生代的大小。  
Xss 线程栈大小。  
PermSize 永久代初始大小。默认为物理内存的1/64,最大不超1G。  
MaxPermSize 永久代最大值。默认为物理内存的1/4,最大不超1G。  
NewRatio 新生代与年老代的比例。比如为3,则新生代占堆的1/4,年老代占3/4。  
SurvivorRatio 新生代中调整eden区与survivor区的比例,默认为8,即eden区为80%的大小,两个survivor分别为10%的大小。  

垃圾搜集器性能通用参数

名称 说明 备注
PretenureSizeThreshold 晋升年老代的对象大小。默认为0,比如设为10M,则超过10M的对象将不在eden区分配,而直接进入年老代。  
MaxTenuringThreshold 晋升老年代的最大年龄。默认为15,比如设为10,则对象在10次普通GC后将会被放入年老代。  
DisableExplicitGC 禁用System.gc()。  

并行搜集器参数

名称 说明 备注
ParallelGCThreads 回收时开启的线程数。默认与CPU个数相等。  
GCTimeRatio 设置系统的吞吐量。比如设为99,则GC时间比为1/1+99=1%,也就是要求吞吐量为99%。若无法满足会缩小新生代大小。  
MaxGCPauseMillis 设置垃圾回收的最大停顿时间。若无法满足设置值,则会优先缩小新生代大小,仍无法满足的话则会牺牲吞吐量。  

并发搜集器参数

名称 说明 备注
CMSInitiatingOccupancyFraction 触发CMS收集器的内存比例。比如60%的意思就是说,当内存达到60%,就会开始进行CMS并发收集。  
UseCMSCompactAtFullCollection 在每一次CMS收集器清理垃圾后送一次内存整理。  
CMSFullGCsBeforeCompaction 设置在几次CMS垃圾收集后,触发一次内存整理。  

内存分配与回收策略

对象优先在 Eden 分配

对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。

JVM-垃圾收集器与内存分配策略_第12张图片

Minor GC和Full GC

  • 新生代 GC (Minor GC)。发生在新生代的垃圾回收动作,频繁,速度快。Java对象大多都具备快速消亡特性。
  • 老年代 GC (Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

大对象直接进入老年代

大对象指需要大量连续内存空间的对象,最典型的是很长的字符串以及数组。

-XX:PretenureSizeThreshold(只对Serial,ParNew有效)。大于此设置值的对象直接在老年代分配。

 

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

对象在Survivor中,每“熬过”一次Minor GC,年龄加1,当增加到一定程度(默认15),就会晋升到老年代。-XX:MaxTenuringThreshold设置此值。

 

动态对象年龄判定

虚拟机并不是永远要求对象年龄达到MaxTenuringThreshold才晋升老年代,如果Survivor空间中相同年龄的所有对象大小超过Survivor的一半,年龄大于等于该年龄的对象直接进入老年代。

空间分配担保

在发生Minor GC前,虚拟机会先检查老年代最大可用连续空间是否大于新生代对象总空间,如果条件成立,那么Minor GC是安全的,如果不成立,虚拟机会查看HandlePromotionFailure设置是否允许担保。如果允许,则继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试一次Minor GC,否则进行Full GC。

 

 

 

你可能感兴趣的:(JVM)