JVM(上) — 垃圾回收算法
📋 本文节选自《JVM 面试题(上)》,返回 查看全部章节
14、谈谈对 OOM 的认识
除了程序计数器,其他内存区域都有 OOM 的风险。
栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效
堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错
方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等
直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请
15、什么情况发生栈溢出?
-Xss可以设置线程栈的大小,当线程方法递归调用层次太深或者栈帧中的局部变量过多时,会出现栈溢出错误 java.lang.StackOverflowError
16、你有哪些手段来排查 OOM 的问题?
增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域
使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用
17、遇到过元空间溢出吗?
元空间在本地内存上,默认是没有上限的,不加限制出了问题会影响整个服务器的,所以也是比较危险的。-XX:MaxMetaspaceSize 可以指定最大值。
一般使用动态代理的框架会生成很多 Java 类,如果占用空间超出了我们的设定最大值,会发生元空间溢出。
18、遇到过堆外内存溢出吗?
Unsafe 类申请内存、JNI 对内存进行操作、Netty 调用操作系统的 malloc 函数的直接内存,这些内存是不受 JVM 控制的,不加限制的使用,很容易发生溢出。这种情况有个显著特点,dump 的堆文件信息正常甚至很小。 -XX:MaxDirectMemorySize 可以指定最大直接内存,但限制不住所有堆外内存的使用。
19、谈谈你知道的垃圾回收算法
判断对象是否可回收的算法有两种:
Reference Counting GC,引用计数算法
Tracing GC,可达性分析算法
JVM 各厂商基本都是用的 Tracing GC 实现
大部分垃圾收集器遵从了分代收集(Generational Collection)理论。 针对新生代与老年代回收垃圾内存的特点,提出了 3 种不同的算法:
1、标记-清除算法(Mark-Sweep) 标记需回收对象,统一回收;或标记存活对象,回收未标记对象。 缺点:
大量对象需要标记与清除时,效率不高
标记、清除产生的大量不连续内存碎片,导致无法分配大对象
2、标记-复制算法(Mark-Copy) 可用内存等分两块,使用其中一块 A,用完将存活的对象复制到另外一块 B,一次性清空 A,然后改分配新对象到 B,如此循环。 缺点:
不适合大量对象不可回收的情况,换句话说就是仅适合大量对象可回收,少量对象需复制的区域
只能使用内存容量的一半,浪费较多内存空间
3、标记-整理算法(Mark-Compact) 标记存活的对象,统一移到内存区域的一边,清空占用内存边界以外的内存。 缺点:
移动大量存活对象并更新引用,需暂停程序运行
20、谈谈你知道的垃圾收集器
Serial 特点:
JDK 1.3 开始提供
新生代收集器
无线程交互开销,单线程收集效率最高
进行垃圾收集时需要暂停用户线程
适用于客户端,小内存堆的回收
ParNew 特点:
是 Serial 收集器的多线程并行版
JDK 7 之前首选的新生代收集器
第一款支持并发的收集器,首次实现垃圾收集线程与用户线程基本上同时工作
除 Serial 外,只有它能与 CMS 配合
Parallel Scavenge 特点:
新生代收集器
标记-复制算法
多线程并行收集器
追求高吞吐量,即最小的垃圾收集时间
可以配置最大停顿时间、垃圾收集时间占比
支持开启垃圾收集自适应调节策略,追求适合的停顿时间或最大的吞吐量
Serial Old 特点:
与 Serial 类似,是 Serial 收集器的老年代版本
使用标记-整理算法
Parallel Old 特点:
JDK 6 开始提供
Parallel Scavenge 的老年代版
支持多线程并发收集
标记-整理算法
Parallel Scavenge + Parallel Old 是一个追求高吞吐量的组合
CMS 特点:
标记-清除算法
追求最短回收停顿时间
多应用于关注响应时间的 B/S 架构的服务端
并发收集、低停顿
占用一部分线程资源,应用程序变慢,吞吐量下降
无法处理浮动垃圾,可能导致 Full GC
内存碎片化问题
G1 特点:
JDK 6 开始实验,JDK 7 商用
面向服务端,JDK 9 取代 Parallel Scavenge + Parallel Old
结合标记-整理、标记-复制算法
首创局部内存回收设计思路
基于 Region 内存布局,采用不同策略实现分代
不再使用固定大小、固定数量的堆内存分代区域划分
优先回收价收益最大的 Region
单个或多个 Humongous 区域存放大对象
使用记忆集解决跨 Region 引用问题
复杂的卡表实现,导致更高的内存占用,堆的 10%~20%
全功能垃圾收集器
追求有限的时间内最高收集效率、延迟可控的情况下最高吞吐量
追求应付内存分配速率,而非一次性清掉所有垃圾内存
适用于大内存堆
Shenandoah 特点:
追求低延迟,停顿 10 毫秒以内
OpenJDK 12 新特性,RedHat 提供
连接矩阵代替记忆集,降低内存使用与伪共享问题出现概率
ZGC 特点:
JDK 11 新加的实验性质的收集器
追求低延迟,停顿 10 毫秒以内
基于 Region 内存布局
未设分代
读屏障、染色指针、内存多重映射实现可并发的标记-整理算法
染色指针和内存多重映射设计精巧,解决部分性能问题,但降低了可用最大内存、操作系统受限、只支持 32 位、不支持压缩指针等
成绩亮眼、性能彪悍
21、生产环境用的什么JDK?如何配置的垃圾收集器?
Oracle JDK 1.8
JDK 1.8 中有 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,默认使用 Parallel Scavenge + Parallel Old。
Serial 系列是单线程垃圾收集器,处理效率很高,适合小内存、客户端场景使用,使用参数 -XX:+UseSerialGC 显式启用。
Parallel 系列相当于并发版的 Serial,追求高吞吐量,适用于较大内存并且有多核CPU的环境,默认或显式使用参数 -XX:+UseParallelGC 启用。可以使用 -XX:MaxGCPauseMillis 参数指定最大垃圾收集暂停毫秒数,收集器会尽量达到目标;使用 -XX:GCTimeRatio 指定期望吞吐量大小,默认 99,用户代码运行时间:垃圾收集时间=99:1。
CMS,追求垃圾收集暂停时间尽可能短,适用于服务端较大内存且多 CPU 的应用,使用参数 -XX:+UseConcMarkSweepGC 显式开启,会同时作用年轻代与老年代,但有浮动垃圾和内存碎片化的问题。
G1,主要面向服务端应用的垃圾收集器,适用于具有大内存的多核 CPU 的服务器,追求较小的垃圾收集暂停时间和较高的吞吐量。首创局部内存回收设计思路,采用不同策略实现分代,不再使用固定大小、固定数量的堆内存分代区域划分,而是基于 Region 内存布局,优先回收价收益最大的 Region。使用参数 -XX:+UseG1GC 开启。
我们生产环境使用了 G1 收集器,相关配置如下 -Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:G1HeapRegionSize 未指定
核心思路:
每个内存区域设置上限,避免溢出
堆设置为操作系统的 70%左右,超过 8 G,首选 G1
根据老年代对象提升速度,调整新生代与老年代之间的内存比例
等过 GC 信息,针对项目敏感指标优化,比如访问延迟、吞吐量等
22、如何查看 JVM 当前使用的是什么垃圾收集器?
-XX:+PrintCommandLineFlags 参数可以打印出所选垃圾收集器和堆空间大小等设置
如果开启了 GC 日志详细信息,里面也会包含各代使用的垃圾收集器的简称
23、如何开启和查看 GC 日志?
常见的 GC 日志开启参数包括: -Xloggc:filename,指定日志文件路径 -XX:+PrintGC,打印 GC 基本信息 -XX:+PrintGCDetails,打印 GC 详细信息 -XX:+PrintGCTimeStamps,打印 GC 时间戳 -XX:+PrintGCDateStamps,打印 GC 日期与时间 -XX:+PrintHeapAtGC,打印 GC 前后的堆、方法区、元空间可用容量变化 -XX:+PrintTenuringDistribution,打印熬过收集后剩余对象的年龄分布信息,有助于 MaxTenuringThreshold 参数调优设置 -XX:+PrintAdaptiveSizePolicy,打印收集器自动设置堆空间各分代区域大小、收集目标等自动调节的相关信息 -XX:+PrintGCApplicationConcurrentTime,打印 GC 过程中用户线程并发时间 -XX:+PrintGCApplicationStoppedTime,打印 GC 过程中用户线程停顿时间 -XX:+HeapDumpOnOutOfMemoryError,堆 oom 时自动 dump -XX:HeapDumpPath,堆 oom 时 dump 文件路径
Java 9 JVM 日志模块进行了重构,参数格式发生变化,这个需要知道。
GC 日志输出的格式,会随着上面的参数不同而发生变化。关注各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量、用户线程停顿时间。
借助工具可视化工具可以更方便的分析,在线工具 GCeasy;离线版可以使用 GCViewer。
如果现场环境不允许,可以使用 JDK 自带的 jstat 工具监控观察 GC 情况。
24、JVM 监控与分析工具你用过哪些?介绍一下。
jps,显示系统所有虚拟机进程信息的命令行工具 jstat,监视分析虚拟机运行状态的命令行工具 jinfo,查看和调整虚拟机参数的命令行工具 jmap,生成虚拟机堆内存转储快照的命令行工具 jhat,显示和分析虚拟机的转储快照文件的命令行工具 jstack,生成虚拟机的线程快照的命令行工具 jcmd,虚拟机诊断工具,JDK 7 提供 jhsdb,基于服务性代理实现的进程外可视化调试工具,JDK 9 提供 JConsole,基于JMX的可视化监视和管理工具 jvisualvm,图形化虚拟机使用情况的分析工具 Java Mission Control,监控和管理 Java 应用程序的工具
MAT,Memory Analyzer Tool,虚拟机内存分析工具 vjtools,唯品会的包含核心类库与问题分析工具 arthas,阿里开源的 Java 诊断工具 greys,JVM进程执行过程中的异常诊断工具 GCHisto,GC 分析工具 GCViewer,GC 日志文件分析工具 GCeasy,在线版 GC 日志文件分析工具 JProfiler,检查、监控、追踪 Java 性能的工具 BTrace,基于动态字节码修改技术(Hotswap)实现的Java程序追踪与分析工具
下面可以重点体验下: JDK 自带的命令行工具方便快捷,不是特别复杂的问题可以快速定位;
阿里的 arthas 命令行也不错;
可视化工具 MAT、JProfiler 比较强大。
25、JIT 是什么?
Just In Time Compiler 的简称,即时编译器。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是 JIT。
26、谈谈双亲委派模型
Parents Delegation Model,这里的 Parents 翻译成双亲有点不妥,类加载向上传递的过程中只有单亲;parents 更多的是多级向上的意思。
除了顶层的启动类加载器,其他的类加载器在加载之前,都会委派给它的父加载器进行加载,一层层向上传递,直到所有父类加载器都无法加载,自己才会加载该类。
双亲委派模型,更好地解决了各个类加载器协作时基础类的一致性问题,避免类的重复加载;防止核心API库被随意篡改。
JDK 9 之前
启动类加载器(Bootstrp ClassLoader),加载 /lib/rt.jar、-Xbootclasspath
扩展类加载器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加载 /lib/ext、java.ext.dirs
应用程序类加载器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加载 CLASSPTH、-classpath、-cp、Manifest
自定义类加载器
JDK 9 开始 Extension ClassLoader 被 Platform ClassLoader 取代,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader
类加载代码逻辑
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
} return c;
}27、列举一些你知道的打破双亲委派机制的例子。为什么要打破?
JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。
OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。