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

一、垃圾收集算法

1.哪些内存需要被回收?什么时候回收?如何回收?

程序计数器、虚拟机栈、本地方法栈的生命周期和线程一样,不需要考虑回收,那么需要回收的就是方法区和堆。什么时候回收呢?当然是后续不再使用的时候!怎么回收?后续的全部内容基本都在解决怎么回收的问题,主要是对堆的回收,至于方法区主要是对废弃的常量和不再使用的类型进行回收,不是重点关注的地方。

2.怎么判定对象不再被使用?

(1)引用计数算法:无法解决对象互相依赖的问题。

(2)可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径成为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。

(3)可作为GC Roots的对象包含以下几种(划重点,要考)

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,如线程执行的方法是用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,如类的引用类型静态变量。
  • 在方法区中常量引用的对象,如字符串常量池里的引用。
  • 在本地方法栈中JNI(Native方法)引用的对象。
  • 虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 对堆局部回收时,关联区域的对象也要作为GC Roots。

3.引用分类:强引用、软引用、弱引用、虚引用

垃圾收集器对这些引用的处理是不同的

4.对象被回收的过程

经历2次标记,第一次是可达性分析后没有与GC Roots相连,第2次判断对象是否有必要执行finalize()方法,如果对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,那么虚拟机将这2种情况都视为没有必要执行。如果对象有必要执行finalize()方法,就会被加入F-Queue队列中,垃圾收集线程会扫描这个队列,并执行每一个对象的finalzie()方法,最后对队列中的对象进行第二次标记,如果对象在finalzie()方法中没有拯救自己,那么对象就会被垃圾收集器回收。

5.分代收集理论

(1)弱分代假说:绝大多数对象都是朝生夕灭的。

(2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

(3)跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

根据分代收集理论(1)和(2),应该将堆划分为不同的区域,并将对象根据年龄分配到不同的区域存储(新生代、老年代),从而可以针对不同的区域使用不同的垃圾回收算法,具体来说,针对新生代对象的特点,大多数对象都是朝生夕灭的,那么可以采用标记-复制算法,只需要复制存活的对象就可以了,可以节省时间;针对老年代对象的特点,大多数对象都是存活的,就要用标记-清除算法,把那些不再使用的对象清除掉就可以。

针对(3),为了避免扫描整个老年代,我们只需要在新生代里面建立一个全局的数据结构(记忆集),记忆集把老年代划分为若干个小块,标识出老年代的哪一块内存存在跨代引用,当发生Minor GC时,把包含跨代引用的小块内存里面的对象加入到GC Roots进行扫描。

6.垃圾回收类型:Minor GC(新生代收集)、Major GC(老年代收集)、Full GC(堆和方法区收集)

7.标记-清除算法、标记-复制算法、标记-整理算法

(1)标记-清除算法

分为“标记”和“清除”两个阶段,其中标记阶段前面第4点“对象被回收的过程”已经讲过,即先标记需要回收的对象,标记完成后再统一回收掉所有被标记的对象。

缺点:

  • 执行效率不稳定:当大量对象需要被回收时,那么标记和清除就很耗时。
  • 内存空间碎片化,内存分配更复杂,从而影响程序吞吐量。

优点:

  • 垃圾收集时停顿时间相比标记-整理算法更短(ps:这也是关注延迟的CMS垃圾收集器采用这个算法的原因)

(2)标记-复制算法(简称为复制算法)

将内存分为大小相等的2块,每次只使用其中一块,每次内存回收时标记存活的对象,复制到另一块,再将本块一次性清理。

缺点:

  • 如果存活的对象很多,复制开销就比较大
  • 空间浪费:可用内存只有一半

优点:

  • 无空间碎片

优化版:Appel式回收

  • 将新生代分为Eden和2个Survivor空间,每次只是用一个Eden和一个Survivor(默认大小为8:1)
  • 空间分配担保:当一个Survivor不足以容纳所有存活对象时,这些对象直接进入老年代

(3)标记-整理算法

标记需要回收的对象,将所有存活的对象向内存空间一端移动,然后直接清理掉边界以外的内存。

缺点:

  • 停顿时间长:因为每次都要移动很多对象并更新所有引用这些对象的地方

优点:

  • 吞吐量更大:内存分配相比标记-清除算法更简单(注:此处的吞吐量是应用程序和收集器的效率总和)

8.算法实现细节

(1)根节点枚举

需暂停用户线程,不需要检查方法区和虚拟机栈的栈帧中的局部变量表,而是直接从OopMap获得。

(2)安全点

背景:可能导致引用关系变化,或者说导致OopMap内容变化的指令非常多,不可能为每一条指令都生成对应的OopMap,否则需要大量的额外存储空间,那怎么办呢?那就在特定的位置记录,我们把这些位置称为安全点,开始垃圾收集以后,应用线程只能在这些安全点停下来,然后垃圾收集线程去扫描OopMap。特地的位置究竟是什么位置呢?

安全点的选取:

  • 不能太少以至于让收集器等待时间过长,也不能太频繁以至于过分增大运行时的内存负荷
  • 选取标准:让程序长时间执行(指令序列复用)
  • 方法调用、循环跳转、异常跳转都属于指令序列的复用,所以只有具有这些功能的指令才会产生安全点。

如何让用户线程在安全点停顿?

  • 抢先式中断:系统把所有用户线程中断,如果发现某个线程不在安全点上就恢复这线程,一会再中断,直到线程执行到安全点。(没有虚拟机采用)
  • 主动式中断:设置一个中断标志,用户线程主动轮询这个标志,一旦发现标志为真就自己在最近的安全点上主动挂起。

(3)安全区域

背景:线程处于阻塞或者睡眠状态时无法响应垃圾收集的中断事件,所以引入安全区域来解决这个问题。安全区域是引用关系不会发生变化的代码片段,在区域中任意地方开始垃圾收集都是安全的。

(4)记忆集与卡表

解决的问题:GC Roots扫描时,Java堆不同区域存在的跨代引用

记忆集:是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。

卡表:是记忆集的具体实现,定义了记忆集的记录精度(卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针)、与堆内存的映射关系等。

(5)写屏障

用来维护卡表元素,在赋值前或者赋值后更新卡表。

(6)并发的可达性分析

为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?

三色标记(黑、灰、白),当且仅当以下2个条件同时满足时会产生“对象消失”的问题:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  • 赋值器删除了全部从灰色对象到白色对象的直接或间接引用

解决并发扫描时对象消失问题,只需要破坏其中一个条件就行。

方案:

  • 增量更新:破坏的是第一个条件,插入黑色对象到白色对象的引用时,记录下来,并发扫描结束后,以黑色对象为根重新扫描一次。(插入黑->白)
  • 原始快照:破坏的是第二个条件,删除灰色对象到白色对象的引用时,记录下来,并发扫描结束后,以灰色对象为根重新扫描一次。(删除灰->白)

 二、垃圾收集器

组合:

  • Serial+Serial Old(适用于客户端模式)
  • Serial+CMS(JDK 9取消)
  • ParNew+Serial Old(JDK 9取消)
  • ParNew+CMS(强交互场景,JDK 9官方不再推荐,更推荐G1)
  • Parallel Scavenge+Serial Old(在Parallel Old出现之前的搭配,现在不推荐)
  • Parallel Scavenge+Parallel Old(吞吐量优先垃圾收集器,JDK 8默认组合)
  • G1(跨越新生代和老年代,JDK 9默认)

1.Serial收集器(Serial/Serial Old)

 

算法:新生代(Serial)采取复制算法,老年代(serial Old)采取标记-整理算法

缺点:垃圾收集时,暂停所有用户线程

优点:

  • 简单高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
  • 更高的单线程收集效率:前提是在单核处理器或处理器核心数较少的环境,Serial收集器无线程交互开销

使用场景:虚拟机运行在客户端模式下的默认新生代收集器

2.ParNew收集器

新生代收集器,本质是Serial收集器的多线程并行版本

算法:复制算法

缺点:

  • 垃圾收集时,暂停所有用户线程
  • 单核环境下,效率比Serial差

优点:多线程收集,减少STW(Stop The World)停顿时间。

使用场景:配合CMS收集器一起工作,是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)默认新生代收集器。

注意事项:默认开启的收集线程数和处理器核心数相同,可以使用-XX:ParallelGCThreads参数限制垃圾收集的线程数。

3.Parallel收集器(Parallel Scavenge/Parallel Old)

 

 

算法:新生代(Parallel Scavenge)采取复制算法,老年代(Parallel Old)采取标记-整理算法

特点:

  • 吞吐量优先,吞吐量 = 用户线程时间/(用户线程时间 + GC线程时间)
  • 自适应调节策略(-XX:+UseAdaptiveSizePolicy):慎用!该功能默认开启,由于吞吐量优先,导致新生代survivor空间不断变小,当一次垃圾回收不足以存放存活的对象时,这些对象直接进入老年代,从而导致更快出现Full GC。

使用场景:后台运算、不需要太多交互的分析任务

4.CMS收集器(Concurrent Mark Sweep)

是一种以获取最短回收停顿时间为目标的收集器。

算法:标记-清除

整个过程分为4个步骤:

  • 初始标记(扫描GC Roots,Stop the World)
  • 并发标记(扫描对象图,与用户线程并发)
  • 重新标记(Stop the World)
  • 并发清除(与用户线程并发)

优点:并发收集、低停顿

缺点:

  • 对处理器资源敏感:默认启动的回收线程数=(处理器核心数+3)/4,当处理器核心数不足4个时,处理器负载就很高。
  • 无法处理“浮动垃圾”,可能出现“Concurrent Mode Failure”并发失败而导致Full GC。(通过-XX:CMSInitiatingOccupancyFraction设置CMS收集器的触发阈值)
  • 空间碎片问题(-XX:UseCMSCompactAtFullCollection:默认开启,当Full GC时进行空间碎片的合并整理;-XX:CMSFullGCsBeforeCompaction:设置经过多少次不整理空间的Full GC后,下一次进入Full GC前会先进行碎片整理,默认值为0)

5.Garbage First收集器(G1)

JDK7及以后的版本可以使用。

算法:整体基于标记-整理算法,局部(两个Region之间)基于标记-复制算法。

目标:延迟可控的情况下获得尽可能高的吞吐量

特点:面向局部收集、基于Region的内存布局形式。

停顿时间模型:支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒。

Region内存布局:把Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden、Survivor空间,或者老年代空间。

内存回收:

  • 面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
  • Region是单次回收的最小单元,每次垃圾收集都是Region的整数倍
  • G1维护了一个回收的优先级列表,每次根据用户设定允许的收集停顿时间(-XX:MaxGCPauseMillis,默认值200毫秒,不能设置太低,否则每次回收的Region就比较少,垃圾慢慢堆积会出现Full GC)优先处理回收价值收益最大的那些Region。

G1收集器要解决的问题:

(1)跨Region引用:记忆集,有更高的内存占用负担,G1至少要耗费大约相当于Java堆容量10%~20%的额外内存来维持收集器工作。

(2)并发标记阶段如何保证收集线程和用户线程互不干扰地运行?原始快照、每个Region有2个TAMS指针。

(3)怎样建立起可靠的停顿预测模型?以衰减均值为理论基础来实现,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准差值、置信度等统计信息。

Region大小:-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。

大对象的存储:

  • 超过Region容量一半的对象判定为大对象
  • 使用连续的Humongous区域存储

回收步骤:

  • 初始标记(扫描GC Roots,Stop the World)
  • 并发标记(扫描对象图,与用户线程并发)
  • 最终标记(Stop the World)
  • 筛选回收(Stop the World)

6.CMS与G1对比

G1优点:

  • 可指定最大停顿时间
  • 不会产生内存空间碎片,有利于程序长时间运行

G1缺点:内存占用、程序运行时的额外执行负载(需要写前屏障跟踪引用变化)比CMS高

场景对比:

CMS适合小内存

G1适合大内存(6G~8G以上)

三、垃圾收集器日志

日志输出:[uptime][level][tags]日志信息

uptime:虚拟机启动到现在经过的秒数,level:日志级别,tags:日志输出的标签集

1.查看GC基本信息(-XX:+PrintGC)

2.查看GC详细信息(-XX:+PrintGCDetails)

3.查看GC前后的堆、方法区可用容量变化(-XX:+PrintHeapAtGC)

4.查看GC过程中用户线程并发时间以及停顿的时间(-XX:+PrintGCApplicationConcurrentTime、-XX:+PrintGCApplicationStoppedTime)

 

四、虚拟机性能监控、故障处理工具

1.基础故障处理工具

(1)jps:虚拟机进程状况工具

可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier),使用频率较高,因为其他JDK工具需要用到它查询到的LVMID。

命令格式:jps [options] [hostid]

options选项如下:

-q:只输出LVMID,省略主类的名称

-m:输出虚拟机进程启动时传递给主类main()函数的参数

-l:输出主类的全名,如果进程执行的的是JAR包,则输出JAR路径

-v:输出虚拟机进程启动时的JVM参数

hostid为RMI注册表中注册的主机名,查询开启了RMI服务的远程虚拟机时才会用到。

(2)jstat:虚拟机统计信息监视工具

可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。

命令格式:jstat [option   vmid   [ interval  [s|ms]   [count] ]  ]

参数interval和count代表查询间隔和次数,如果省略说明只查询一次。

选项option代表用户希望查询到的虚拟机信息,主要分为3类:类加载、垃圾收集、运行期编译状况。

-class:监视类加载、卸载数量、总空间以及类装载所耗费的时间

-gc:监视Java堆状况,包括Eden区、2个survivor区、老年代、永久代等的容量,已用空间,垃圾收集时间合计等信息

-gccapacity:监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间

-gcutil:监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比

-gccause:与-gcutil功能一样,但是会额外输出导致上一次垃圾收集产生的原因

-gcnew:监视新生代垃圾收集状况

 -gcnewcapacity:监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间

-gcold:监视老年代垃圾收集状况

-gcoldcapacity:监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间

-gcpermcapacity:输出永久代使用到的最大、最小空间

-compiler:输出即时编译器编译过的方法、耗时等信息

-printcompilation:输出已经 被即时编译的方法

(3)jinfo:Java配置信息工具

jinfo的作用是实时查看和调整虚拟机各项参数。

命令格式:jinfo [option] pid

jinfo -flag可以查询参数的默认值(JDK 6以上的版本也可以用java -XX:+PrintFlagsFinal查看参数默认值)

jinfo -sysprops可以打印虚拟机进程的System.getProperties()的内容

JDK 6之后可以在运行期修改部分参数的值(-flag[+/-]name或者-flag name=value)

(4)jmap:Java内存映像工具

jmap命令用于生成堆转储快照(一般成为heapdump或dump文件),还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。

其他获取堆转储快照文件的方法:

  • -XX:HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件
  • -XX:HeapDumpOnCtrlBreak参数可以使用【Ctrl】+【Break】键让虚拟机生成堆转储快照文件
  • Linux系统下通过kill -3命令发送进程退出信号“恐吓”一下虚拟机,也能拿到堆转储快照

命令格式:jmap [option] vmid

option选项如下:

-dump:生成Java堆转储快照。格式为-dump:[live,]format=b,file=<filename>,其中live子参数说明是否只dump出存活的对象

-finalizerinfo:显示在F-Queue中等待Finalizer线程执行finalize方法的对象

-heap:显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等

-histo:显示堆中对象统计信息,包括类、实例数量、合计容量

-permstat:以ClassLoader为统计口径显示永久代内存状态

-F:当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照

(5)jhat:虚拟机堆转储快照分析工具(比较简陋,一般不用,而是用功能更强大的VisualVM或者Eclipse Memory Analyzer、IBM HeapAnalyzer等工具)

JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。

(6)jstack:Java堆栈跟踪工具

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时就可以通过jstack来查看各个线程的调用堆栈,从而分析问题。

命令格式:jstack [option] vmid

option选项如下:

-F:当正常输出的请求不被响应时,强制输出线程堆栈。

-l:除堆栈外,显示关于锁的附加信息

-m:如果调用到本地方法的话,可以显示C/C++的堆栈

2.可视化故障处理工具

JHSDB

JConsole

VisualVM

Java Mission Control

posted @ 2020-09-17 11:49  万丈天涯  阅读(130)  评论(0编辑  收藏  举报