上文咱们学习了 GC 的理论基础,相信你们对 GC 的工做原理有了比较深入的认识,这一篇咱们继续趁热打铁,来学习下 GC 的实战内容,主要包括如下几点html
在开始实践以前咱们有必要先简单了解一下 JVM 参数配置,由于本文以后的实验中提到的 JVM 中的栈,堆大小,使用的垃圾收集器等都须要经过 JVM 参数来设置java
先来看下如何运行一个 Java 程序算法
public class Test {
public static void main(String[] args) {
System.out.println("test");
}
}
复制代码
指定这些 JVM 参数咱们就能够指定启动 JVM 进程以哪一种模式(server 或 client),运行时分配的堆大小,栈大小,用什么垃圾收集器等等,JVM 参数主要分如下三类shell
一、 标准参数(-),全部的 JVM 实现都必须实现这些参数的功能,并且向后兼容;例如 -verbose:gc(输出每次GC的相关状况)数组
二、 非标准参数(-X),默认 JVM 实现这些参数的功能,可是并不保证全部 JVM 实现都知足,且不保证向后兼容,栈,堆大小的设置都是经过这个参数来配置的,用得最多的以下缓存
参数示例 | 表示意义 |
---|---|
-Xms512m | JVM 启动时设置的初始堆大小为 512M |
-Xmx512m | JVM 可分配的最大堆大小为 512M |
-Xmn200m | 设置的年轻代大小为 200M |
-Xss128k | 设置每一个线程的栈大小为 128k |
三、非Stable参数(-XX),此类参数各个 jvm 实现会有所不一样,未来可能会随时取消,须要慎重使用, -XX:-option 表明关闭 option 参数,-XX:+option 表明要关闭 option 参数,例如要启用串行 GC,对应的 JVM 参数即为 -XX:+UseSerialGC。非 Stable 参数主要有三大类微信
参数示例 | 表示意义 |
---|---|
-XX:-DisableExplicitGC | 禁止调用System.gc();但jvm的gc仍然有效 |
-XX:-UseConcMarkSweepGC | 对老生代采用并发标记交换算法进行GC |
-XX:-UseParallelGC | 启用并行GC |
-XX:-UseParallelOldGC | 对Full GC启用并行,当-XX:-UseParallelGC启用时该项自动启用 |
-XX:-UseSerialGC | 启用串行GC |
参数示例 | 表示意义 |
---|---|
-XX:MaxHeapFreeRatio=70 | GC后java堆中空闲量占的最大比例 |
-XX:NewRatio=2 | 新生代内存容量与老生代内存容量的比例 |
-XX:NewSize=2.125m | 新生代对象生成时占用内存的默认值 |
-XX:ReservedCodeCacheSize=32m | 保留代码占用的内存容量 |
-XX:ThreadStackSize=512 | 设置线程栈大小,若为0则使用系统默认值 |
参数示例 | 表示意义 |
---|---|
-XX:HeapDumpPath=./java_pid.hprof | 指定导出堆信息时的路径或文件名 |
-XX:-HeapDumpOnOutOfMemoryError | 当首次遭遇OOM时导出此时堆中相关信息 |
-XX:-PrintGC | 每次GC时打印相关信息 |
-XX:-PrintGC Details | 每次GC时打印详细信息 |
画外音:以上只是列出了比较经常使用的 JVM 参数,更多的 JVM 参数介绍请查看文末的参考资料数据结构
明白了 JVM 参数是干啥用的,接下来咱们进入实战演练,下文中全部程序运行时对应的 JVM 参数都以 VM Args 的形式写在开头的注释里,读者若是在执行程序时记得要把这些 JVM 参数给带上哦多线程
有些人可能会以为奇怪, GC 不是会自动帮咱们清理垃圾以腾出使用空间吗,怎么还会发生 OOM, 咱们先来看下有哪些场景会发生 OOM并发
一、Java 虚拟机规范中描述在栈上主要会发生如下两种异常
/** * VM Args:-Xss160k */
public class Test {
private void dontStop() {
while(true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test oom = new Test();
oom.stackLeakByThread();
}
}
复制代码
运行以上代码会抛出「java.lang.OutOfMemoryError: unable to create new native thread」的异常,缘由不难理解,操做系统给每一个进程分配的内存是有限制的,好比 32 位的 Windows 限制为 2G,虚拟机提供了参数来控制 Java 堆和方法的这两部内存的最大值,剩余的内存为 「2G - Xmx(最大堆容量)= 线程数 * 每一个线程分配的虚拟机栈(-Xss)+本地方法栈 」(程序计数器消耗内存不多,可忽略),每一个线程都会被分配对应的虚拟机栈大小,因此总可建立的线程数确定是固定的, 像以上代码这样不断地建立线程固然会形成最终没法分配,不过这也给咱们提供了一个新思路,若是是由于创建过多的线程致使的内存溢出,而咱们又想多建立线程,能够经过减小最大堆(-Xms)和减小虚拟机栈大小(-Xss)来实现。
二、堆溢出 (java.lang.OutOfMemoryError:Java heap space)
主要缘由有两点
示例以下:
/** * VM Args:-Xmx12m */
class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}
复制代码
咱们指定了堆大小为 12M,执行 「java -Xmx12m OOM」命令就发生了 OOM 异常,若是指定 13M 则以上程序就能正常执行,因此对于因为大对象分配致使的堆溢出这种 OOM,咱们通常采用增大堆内存的方式来解决
画外音:有人可能会说分配的数组大小不是只有 210241024*4(一个 int 元素占 4 个字节)= 8M, 怎么分配 12 M 还不够,由于 JVM 进程除了分配数组大小,还有指向类(数组中元素对应的类)信息的指针、锁信息等,实际须要的堆空间是可能超过 12M 的, 12M 也只是尝试出来的值,不一样的机器可能不同
/** * VM Args:-Xmx4m */
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
复制代码
执行以上代码就会发生内存泄漏,第一次循环,map 里存有 10000 个 key value,但以后的每次循环都会新增 10000 个元素,由于 Key 这个 class 漏写了 equals 方法,致使对于每个新建立的 new Key(i) 对象,即便 i 相同也会被认定为属于两个不一样的对象,这样 m.containsKey(new Key(i)) 结果均为 false,结果就是 HashMap 中的元素将一直增长,解决方式也很简单,为 Key 添加 equals 方法便可,以下
@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}
复制代码
对于这种内存泄漏致使的 OOM, 单纯地增大堆大小是没法解决根本问题的,只不过是延缓了 OOM 的发生,最根本的解决方式仍是要经过 heap dump analyzer 等方式来找出内存泄漏的代码来修复解决,后文会给出一个例子来分析
三、java.lang.OutOfMemoryError:GC overhead limit exceeded
Sun 官方对此的定义:超过98%的时间用来作 GC 而且回收了不到 2% 的堆内存时会抛出此异常
致使的后果就是因为通过几个 GC 后只回收了不到 2% 的内存,堆很快又会被填满,而后又频繁发生 GC,致使 CPU 负载很快就达到 100%,另外咱们知道 GC 会引发 「Stop The World 」的问题,阻塞工做线程,因此会致使严重的性能问题,产生这种 OOM 的缘由与「java.lang.OutOfMemoryError:Java heap space」相似,主要是因为分配大内存数组或内存泄漏致使的, 解决方案以下:
四、java.lang.OutOfMemoryError:Permgen space
在 Java 8 之前有永久代(实际上是用永久代实现了方法区的功能)的概念,存放了被虚拟机加载的类,常量,静态亦是,JIT 编译后的代码等信息,因此若是错误地频繁地使用 String.intern() 方法或运行期间生成了大量的代理类都有可能致使永久代溢出,解决方案以下
五、 java.lang.OutOfMemoryError:Requested array size exceeds VM limit
该错误由 JVM 中的 native code 抛出。 JVM 在为数组分配内存以前,会执行基于所在平台的检查:分配的数据结构是否在此平台中是可寻址的,平台通常容许分配的数据大小在 1 到 21 亿之间,若是超过了这个数就会抛出这种异常
碰到这种异常通常咱们只要检查代码中是否有建立超大数组的地方便可。
六、 java.lang.OutOfMemoryError: Out of swap space
Java 应用启动的时候分被分配必定的内存空间(经过 -Xmx 及其余参数来指定), 若是 JVM 要求的总内存空间大小大于可用的本机内存,则操做系统会将内存中的部分数据交换到硬盘上
七、 Out of memory:Kill process or sacrifice child
为了理解这个异常,咱们须要知识一些操做系统的知识,咱们知道,在操做系统中执行的程序,都是以进程的方式运行的,而进程是由内核调度的,在内核的调度任务中,有一个「Out of memory killer」的调度器,它会在系统可用内存不足时被激活,而后选择一个进程把它干掉,哪一个进程会被干掉呢,简单地说会优先干掉占用内存大的应用型进程
解决这种 OOM 最直接简单的方法就是升级内存,或者调整 OOM Killer 的优先级,减小应用的没必要要的内存使用等等
看了以上的各类 OOM 产生的状况,能够看出:GC 和是否发生 OOM 没有必然联系!, GC 主要发生在堆上,而 从以上列出的几种发生 OOM 的场景能够看出,因为空间不足没法再建立线程,因为存在死循环一直在分配对象致使 GC 没法回收对象或一次分配大内存数组(超过堆的大小)等均可能致使 OOM, 因此 OOM 与 GC 并无太大的关联
接下来咱们来看下如何排查形成 OOM 的缘由,内存泄漏是最多见的形成 OOM 的一种缘由,因此接下来咱们以来看看怎么使用工具来排查这种问题,使用到的工具主要有两大类
一、使用 mat(Eclipse Memory Analyzer) 来分析 dump(堆转储快照) 文件
主要步骤以下
接下来咱们就来看看如何用以上的工具查看以下经典的内存泄漏案例
/** * VM Args:-Xmx10m */
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
while (true) {
list.add("OutOfMemoryError soon");
}
}
}
复制代码
为了让以上程序快速产生 OOM, 我把堆大小设置成了 10M, 这样执行 「java -Xmx10m -XX:+HeapDumpOnOutOfMemoryError Main」后很快就发生了 OOM,此时咱们就拿到了 hrof 文件,下载 MAT 工具,打开 hrof,进行分析,打开以后选择 「Leak Suspects Report」进行分析,能够看到发生 OOM 的线程的堆栈信息,明肯定位到是哪一行形成的
二、使用 jvisualvm 来分析
用第一种方式必须等 OOM 后才能 dump 出 hprof 文件,但若是咱们想在运行中观察堆的使用状况以便查出可能的内存泄漏代码就无能为力了,这时咱们能够借助 jvisualvm 这款工具, jvisualvm 的功能强大,除了能够实时监控堆内存的使用状况,还能够跟踪垃圾回收,运行中 dump 中堆内存使用状况、cpu分析,线程分析等,是查找分析问题的利器,更骚的是它不光能分析本地的 Java 程序,还能够分析线上的 Java 程序运行状况, 自己这款工具也是随 JDK 发布的,是官方力推的一款运行监视,故障处理的神器。咱们来看看如何用 jvisualvm 来分析上文所述的存在内存泄漏的以下代码
import java.util.Map;
import java.util.HashMap;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map<Key,String> m = new HashMap<Key,String>();
while(true) {
for(int i=0;i<10000;i++) {
if(!m.containsKey(new Key(i))) {
m.put(new Key(i), "Number:" + i);
}
}
}
}
}
复制代码
打开 jvisualvm (终端输入 jvisualvm 执行便可),打开后,将堆大小设置为 500M,执行命令 java Xms500m -Xmx500m KeylessEntry,此时能够观察到左边出现了对应的应用 KeylessEntry,双击点击 open
打开以后能够看到展现了 CPU,堆内存使用,加载类及线程的状况
注意看堆(Heap)的使用状况,一直在上涨
此时咱们再点击 「Heap Dump」
过一下子便可看到内存中对象的使用状况
能够看到相关的 TreeNode 有291w 个,远超正常状况下的 10000 个!说明 HashMap 一直在增加,自此咱们能够定位出问题代码所在!
三、使用 jps + jmap 来获取 dump 文件
jps 能够列出正在运行的虚拟机进程,并显示执行虚拟机主类及这些进程的本地虚拟机惟一 ID,如图示
拿到进程的 pid 后,咱们就能够用 jmap 来 dump 出堆转储文件了,执行命令以下
jmap -dump:format=b,file=heapdump.phrof pid
复制代码
拿到 dump 文件后咱们就能够用 MAT 工具来分析了。 但这个命令在生产上必定要慎用!由于JVM 会将整个 heap 的信息 dump 写入到一个文件,heap 比较大的话会致使这个过程比较耗时,而且执行过程当中为了保证 dump 的信息是可靠的,会暂停应用!
接下来咱们看看 GC 日志怎么看,日志能够有效地帮助咱们定位问题,因此搞清楚 GC 日志的格式很是重要,来看下以下例子
/** * VM Args:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseSerialGC -XX:SurvivorRatio=8 */
public class TestGC {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 这里会出现一次 Minor GC
}
}
复制代码
执行以上代码,会输出以下 GC 日志信息
10.080: 2[GC 3(Allocation Failure) 0.080: 4[DefNew: 56815K->280K(9216K),6 0.0043690 secs] 76815K->6424K(19456K), 80.0044111 secs]9 [Times: user=0.00 sys=0.01, real=0.01 secs]
以上是发生 Minor GC 的 GC 是日志,若是发生 Full GC 呢,格式以下
10.088: 2[Full GC 3(Allocation Failure) 0.088: 4[Tenured: 50K->210K(10240K), 60.0009420 secs] 74603K->210K(19456K), [Metaspace: 2630K->2630K(1056768K)], 80.0009700 secs]9 [Times: user=0.01 sys=0.00, real=0.02 secs]
二者格式其实差很少,一块儿来看看,主要以本例触发的 Minor GC 来说解, 以上日志中标的每个数字与如下序号一一对应
知道了 GC 日志怎么看,咱们就能够根据 GC 日志有效定位问题了,如咱们发现 Full GC 发生时间过长,则结合咱们上文应用中打印的 OOM 日志可能能够快速定位到问题
jstat 是用于监视虚拟机各类运行状态信息的命令行工具,能够显示本地或者远程虚拟机进程中的类加载,内存,垃圾收集,JIT 编译等运行数据,jstat 支持定时查询相应的指标,以下
jstat -gc 2764 250 22
复制代码
定时针对 2764 进程输出堆的垃圾收集状况的统计,能够显示 gc 的信息,查看gc的次数及时间,利用这些指标,把它们可视化,对分析问题会有很大的帮助,如图示,下图就是我司根据 jstat 作的一部分 gc 的可视化报表,能快速定位发生问题的问题点,若是你们须要作 APM 可视化工具,建议配合使用 jstat 来完成。
通过前面对 JVM 参数的介绍及相关例子的实验,相信你们对 JVM 的参数有了比较深入的理解,接下来咱们再谈谈如何设置 JVM 参数,
一、首先 Oracle 官方推荐堆的初始化大小与堆可设置的最大值通常是相等的,即 Xms = Xmx,由于起始堆内存过小(Xms),会致使启动初期频繁 GC,起始堆内存较大(Xmx)有助于减小 GC 次数
二、调试的时候设置一些打印参数,如-XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log,这样能够从gc.log里看出一些端倪出来
三、系统停顿时间过长多是 GC 的问题也多是程序的问题,多用 jmap 和 jstack 查看,或者killall -3 Java,而后查看 Java 控制台日志,能看出不少问题
四、 采用并发回收时,年轻代小一点,年老代要大,由于年老大用的是并发回收,即便时间长点也不会影响其余程序继续运行,网站不会停顿
五、仔细了解本身的应用,若是用了缓存,那么年老代应该大一些,缓存的HashMap不该该无限制长,建议采用LRU算法的Map作缓存,LRUMap的最大长度也要根据实际状况设定
要设置好各类 JVM 参数,还能够对 server 进行压测, 预估本身的业务量,设定好一些 JVM 参数进行压测看下这些设置好的 JVM 参数是否能知足要求
本文经过详细介绍了 JVM 参数及 GC 日志, OOM 发生的缘由及相应地调试工具,相信读者应该掌握了基本的 MAT,jvisualvm 这些工具排查问题的技巧,不过这些工具的介绍本文只是提到了一些皮毛,你们能够在再深刻了解相应工具的一些进阶技能,这能对本身排查问题等大有裨益!文中的例子你们能够去试验一下,修改一下参数看下会发生哪些神奇的现象,亲自动手作一遍能对排查问题的思路更加清晰哦
欢迎关注公众号与笔者共同交流哦^_^
参考
更多算法 + 计算机基础知识 + Java 等文章,欢迎关注个人微信公众号哦。