JVM调优

了解 JVM 中内存管理的原理后,下一步就是如何调优了。

JVM参数

-D、-X、-XX 区别

其一是标准参数(-),所有的 JVM 实现都必须实现这些参数的功能,而且向后兼容;(包括-D 等)JVM 的标准参数都是以”-“开头,通过输入”java -help”或者”java -?”,可以查看 JVM 标准参数列表
其二是非标准参数(-X),默认 jvm 实现这些参数的功能,但是并不保证所有 jvm 实现都满足,且不保证向后兼容;但是列出来的都是当前可用的。
其三是非 Stable 参数(-XX),此类参数各个 jvm 实现会有所不同,将来可能会随时取消,需要慎重使用;

栈大小

  • Xss512k:用来设置每个线程的堆栈大小;

堆空间结构调整

堆空间结构调整

  • Xmx4g:JVM 最大允许分配的堆内存,按需分配;
  • Xms4g:JVM 初始分配的堆内存,一般和 Xmx 配置成一样以避免每次 gc 后 JVM 重新分配内存;
  • XX:MetaspaceSize=64m 初始化元空间大小;
  • XX:MaxMetaspaceSize=128m 最大化元空间大小。

Metaspace 建议不要设置,一般让 JVM 自己启动的时候动态扩容就好了,没必要自己去设置。如果不动态加载 class ,当启动起来的时候,一般是很少有变化的。
从这个角度我们可以认为我们的 JVM 内存的大小是堆+Metaspace+io(运行时产生的大小)。

  • -Xms[g|m|k]
  • -Xmx[g|m|k] 堆大小
  • -Xmn[g|m|k] 年轻代大小
  • -XX:NewRatio=老年代/新生代 比例
  • -XX:MaxMetaspaceSize=[unit] 元空间
  • -XX:NewSize=[unit] 初始年轻代大小
  • -XX:SurvivorRatio= # 代表分代回收算法新生代中 Eden:Survivor 的比例,注意 Survivor 是有两块的,如果 Eden:Survivor = 3,则新生代将被分割为 5 份,Eden 占 3 份

堆的最大和最小限制,网上很多资料都将二者设置成一样的值,我觉得这是不好的习惯,因为 JVM 只有在内存使用量达到-Xms 的值时才会开始 gc,设置成一样的值也就是说只有在 JVM 使用完内存后才会开始 gc,这会导致最大暂停时间偏长,用户体验不好,当然设置成一样也可以减轻堆伸缩带来的压力。当然这些都是直观的看法,根据 [3] 的说法,这个调整策略和 JVM 中使用的垃圾回收算法相关,如果是 IBM JVM(采用 sweep-compact),设置不一样较好;如果是 Sun JVM(采用分代回收),设置成一样较好。

选择哪个 GC 收集器

  • -XX:+UseSerialGC
  • -XX:+UseParallelGC
  • -XX:+USeParNewGC
  • -XX:+UseG1GC
  • -XX:-UseConcMarkSweepGC
    对老生代采用并发标记交换算法进行 GC

GC 的时候生成文件

  • -XX:+UseGCLogFileRotation 用文件循环策略
  • -XX:NumberOfGCLogFiles=10 最大文件数
  • -XX:GCLogFileSize=50M 文件大小
  • -Xloggc:/home/user/log/gc.log 位置

OOM后排错

  • -XX:+HeapDumpOnOutOfMemoryError 打出 dump 文件当发生 out of memory
  • -XX:HeapDumpPath=./java_pid.hprof 文件名
  • -XX:OnOutOfMemoryError=”< cmd args >;< cmd args >” 发生异常的时候执行的命令
  • -XX:+UseGCOverheadLimit 在 GC 之前

Java Management Extensions

Java 管理扩展,打开 JMX 端口,就可以用标准的端口来监控 server 端的 jvm 了。
-Djava.rmi.server.hostname=
-Dcom.sun.management.jmxremote.port=
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

-XX:+PrintFlagsFinal
-XX:+PrintFlagsInitial

生产环境参数设置

  1. 实际生产中很少去设置 gc 相关的详细参数,一般只要把 thread dump 处理好(及异常的时候生成 demp 文件)和 jmx 端口打开;
  2. 能设置好几个分代的内存空间就不错了。这个可以通过 jvm 的监控来设置根据 cpu 和 gc 的情况;
  3. 因为随着 JVM 的版本的升级,jvm 垃圾回收会也来越智能,但是我们必须要了解这些,因为面试的时候大牛为了显摆自己会问这些问题。
  4. 根据我们上面介绍的就算你设置+XX 有的时候也不一定有用,说不定哪个小版本里面就失灵了。jdk1.8 关于 gc 的最多开启+XX:+UseConcMarkSweepGC。

调优工具

对 JVM 的监控可以分为以下几个方面:

  • 内存状况分析(GC)
  • 线程状态分析

关于 GC 的监控,比较重要的有三个方面:

  • 各个区的容量,主要是堆中新生代与老年代的内存分配。
  • Full GC、Young GC 发生的次数,原则上尽量避免发生 Full GC,Young GC 能少则少。
  • 当前系统的内存比、CPU 使用率。

jps

jps 列出正在运行的虚拟机进程
包括 PID、主类、jar 包等

1
jps -ml

jinfo

Java 配置信息工具,并且支持运行时动态修改部分参数
查看某些配置值或开关是否打开:

1
2
jinfo -flag MaxTenuringThreshold 8737
jinfo -flag PrintGCDetails 8737

打开开关

1
2
jinfo -flag MaxTenuringThreshold=10 8737
jinfo -flag +PrintGCDetails 8737

查看虚拟机的默认配置参数还可以在运行时打开虚拟机的 PrintFlagsFinal 开关

1
java -XX:+PrintFlagsFinal Test

jstat

常用选项

1
jstat -gcutil # 垃圾收集统计数据

统计数据列含义:

数据列 描述 支持的 jstat 选项
S0C Survivor0 的当前容量 -gc -gccapacity -gcnew -gcnewcapacity
S1C S1 的当前容量 -gc -gccapacity -gcnew -gcnewcapacity
S0U S0的使用量 -gc-gcnew
S1U S1 的使用量 -gc-gcnew
EC Eden 区的当前容量 -gc -gccapacity -gcnew -gcnewcapacity
EU Eden 区的使用量 -gc -gcnew
OC old 区的当前容量 -gc -gccapacity -gcnew -gcnewcapacity
OU old区的使用量 -gc-gcnew
PC 方法区的当前容量 -gc-gccapacity -gcold -gcoldcapacity -gcpermcapacity
PU 方法区的使用量 -gc -gcold
YGC Young GC 次数 -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
YGCT Young GC 累积耗时 -gc -gcnew -gcutil -gccause
FGC Full GC次数 -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
FGCT Full GC 累积耗时 -gc-gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
GCT GC 总的累积耗时 -gc -gcold -gcoldcapacity -gccapacity -gcpermcapacity -gcutil -gccause
NGCMN 新生代最小容量 -gccapacity -gcnewcapacity
NGCMX 新生代最大容量 -gccapacity -gcnewcapacity
NGC 新生代当前容量 -gccapacity -gcnewcapacity
OGCMN 老年代最小容量 -gccapacity -gcoldcapacity
OGCMX 老年代最大容量 -gccapacity -gcoldcapacity
OGC 老年代当前容量 -gccapacity -gcoldcapacity
PGCMN 方法区最小容量 -gccapacity -gcpermcapacity
PGCMX 方法区最大容量 -gccapacity -gcpermcapacity
PGC 方法区当前容量 -gccapacity -gcpermcapacity
PC 方法区的当前容量 -gccapacity -gcpermcapacity
PU 方法区使用量 -gccapacity -gcold
LGCC 上一次 GC 发生的原因 -gccause
GCC 当前 GC 发生的原因 -gccause
TT 存活阀值,如果对象在新生代移动次数超过此阀值,则会被移到老年代 -gcnew
MTT 最大存活阀值,如果对象在新生代移动次数超过此阀值,则会被移到老年代 -gcnew
DSS survivor 区的理想容量 -gcnew

jmap

1
2
jmap -histo pid -F
jmap -dump:format=b, file=heap.bin pid

JConsole

JVisualVM

GC日志

关于输出 GC 日志的参数有以下几种:

  • -XX:+PrintGC 输出 GC 日志
  • -XX:+PrintGCDetails 输出 GC 的详细日志
  • -XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
  • -XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
  • -Xloggc:../logs/gc.log 日志文件的输出路径

比如对 PrintGCDetails 这个参数:

1
2
3
4
5
6
7
8
9
public class GCLogTest {
public static void main(String[] args) {
int _1m = 1024 * 1024;
byte[] data = new byte[_1m];
// 将data置null让其可被回收
data = null;
System.gc();
}
}

在 IDE 中设置 VM 参数-XX:+PrintGCDetails,再运行,可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
[GC (System.gc()) [PSYoungGen: 5051K->776K(38400K)] 5051K->784K(125952K), 0.0014035 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) Disconnected from the target VM, address: '127.0.0.1:55472', transport: 'socket'
[PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->684K(87552K)] 784K->684K(125952K), [Metaspace: 2980K->2980K(1056768K)], 0.0040080 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000)
from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
ParOldGen total 87552K, used 684K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
object space 87552K, 0% used [0x0000000740000000,0x00000007400ab0d0,0x0000000745580000)
Metaspace used 2988K, capacity 4568K, committed 4864K, reserved 1056768K
class space used 318K, capacity 392K, committed 512K, reserved 1048576K

第一行是 YoungGC,其结构如下图所示:
JVM-YoungGC日志
第二行是 FullGC,其结构如下图所示:
JVM-FullGC日志

  • GC日志开头的[GC[Full GC说明了这次垃圾收集的停顿类型,如果有Full,说明这次 GC 发生了Stop-The-World。因为是调用了System.gc()方法触发的收集,所以会显示[Full GC (System.gc()),不然是没有后面的(System.gc())的。
  • [PSYoungGen[ParOldGen是指 GC 发生的区域。
  • 在方括号中PSYoungGen:后面的5051K->776K(38400K)代表的是GC前该内存区域已使用的容量->GC后该内存区域已使用的容量(该内存区域总容量)
  • 在方括号之外的5051K->784K(125952K)代表的是GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量),注意已使用容量是减掉一个 Servivor 区、线程栈等区域后的大小。
  • 再往后的0.0014035 secs代表该内存区域 GC 所占用的时间,单位是秒。
  • 再后面的[Times: user=0.01 sys=0.00, real=0.00 secs],user 代表进程在用户态消耗的 CPU 时间,sys 代表进程在内核态消耗的 CPU 时间、real 代表程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。
  • 至于后面的eden代表的是 Eden 空间,还有fromto代表的是 Survivor 空间。

问题排查

OOM

有两种情况可能导致 OOM:

  1. 内存泄露(Memory Leak),某些对象是需要被释放的但是却由于某些原因释放不了,查看泄露对象对 GC Roots 的引用链,再追本溯源进行分析;
  2. 内存溢出(Memory Overflow),对象太多了导致堆放不下了,查看堆是否可以调大,或者某些对象活太久了。

产生 OutOfMemoryError 错误的具体原因有以下几种:

  • java.lang.OutOfMemoryError: Java heap space 表示 Java 堆空间不足。当应用程序申请更多的内存时,若 Java 堆内存已经无法满足应用程序的需要,则将抛出这种异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /*
    VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
    */
    public class HeapOOM {
    static class OOMObject {}

    public static void main(String[] args) {
    List<OOMObject> list = new ArrayList<>();
    while(true) {
    list.add(new OOMObject());
    }
    }
    }
  • java.lang.OutOfMemoryError: PermGen space,表示 Java 永久代(方法区)的空间不足。永久代用于存放类的字节码和常量池,类的字节码被加载后存放在这个区域,这和存放对象实例的堆区是不同的。大多数 JVM 的实现都不会对永久代进行垃圾回收,因此,只要类加载过多就会出现这个问题。一般的应用程序都不会产生这个错误,然而,对于 Web 服务器会产生大量的 JSP,JSP 在运行时被动态地编译为 Java Servlet 类,然后加载到方法区,因此,有很多 JSP 的 Web 工程可能会产生这个异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /*
    VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
    */
    public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
    // 使用List保持对常量池的引用,避免Full GC回收常量池
    List<String> list = new ArrayList<String>();
    for(int i = 0;; i++) {
    list.add(String.valueOf(i).intern());
    }
    }
    }
    使用 intern 测试运行时常量池是“永久代”的还是“元空间”的:
    1
    2
    3
    4
    String str1 = new StringBuilder("计算机").append("软件").toString();
    System.out.println(str1.intern() == str1);
    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2.intern() == str2);
    方法区溢出测试(使用 CGLib 动态生成类):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
    VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
    */
    public class MethodAreaOOM {
    static class OOMObject {}
    public static void main(String[] args) {
    while(true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(HeapOOM.OOMObject.class);
    enhancer.setUseCache(false);
    enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object o, Method method,
    Object[] objects, MethodProxy proxy) throws Throwable {
    return proxy.invokeSuper(o, args);
    }
    });
    enhancer.create();
    }
    }
    }
    jdk1.6 及之前版本,因为 HotSpot 实行“永久代”(PermGen),方法区保存到永久代中,虽然逻辑上属于堆,但是在这块空间上并没有实行 GC。在这种情况下,上面代码发生堆溢出是必然的。
    jdk1.7 及之后版本,因为“去永久代”,引入了“元空间”(Metaspace),这种策略使得大部分类元数据都保存在本地内存中,元空间使用一个全局空闲组块列表(一个大数组)表示,每创建一个类加载器都会从这个列表中获取一个自己的组块,用处当然是存储元信息(指针碰撞方式分配),当一个类加载器不再活动后,其所持有的组块列表也就返还给全局组块列表了,也就是说,类也是可能会被 GC 回收掉的。
    运行时常量池是分配于方法区的,所以可以这么认为:1.6 及之前常量是分配在一个“全局静态区”的,而 1.7 及之后则在堆中分配。
    运行时常量池导致的溢出不常见,上面的例子感觉也有点极端。
    方法区导致的溢出在实际应用中常见:一些框架 Spring、Hibernate 在对类进行增强时会使用到 CGLib 这类字节码技术;JVM 上的动态语言(Groovy)会持续创建类来实现语言的动态特性;拥有大量 JSP 页面或会动态生成 JSP 文件的应用(JSP 第一次运行时会被编译为 Servlet);基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。
  • java.lang.OutOfMemoryError: unable to create new native thread,本质原因是创建了太多的线程,而系统允许创建的线程数是有限制的。
  • java.lang.OutOfMemoryError:GC overhead limit exceeded,是并行(或者并发)垃圾回收器的 GC 回收时间过长、超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时抛出的这种异常,用来提前预警,避免内存过小导致应用不能正常工作。
  • 栈溢出
    可以先使用-Xoss 参数设置本地方法栈大小(在 HotSpot 中无效,因为它将两个栈合并了),-Xss 参数设置栈容量大小,设得稍微小一些都没有问题。
    实验非常简单,就是定义并调用一个无限递归的方法,在调用深度到达一定程度后就会报错,并且使用一个 stackLength 成员变量记录栈深度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /*
    VM Args: -Xss228k
    */
    public class StackSOF {
    private int stackLength = 1;

    public void stackLeak() {
    stackLength++;
    stackLeak();
    }
    public static void main(String[] args) {
    StackSOF oom = new StackSOF();
    try {
    oom.stackLeak();
    } catch (Throwable e) {
    System.out.println("stack length:" + oom.stackLength);
    throw e;
    }
    }
    }
    /*
    VM Args: -Xss2M
    */
    public class StackOOM {
    private void dontstop() {
    while(true) {}
    }
    public void stackLeakByThread() {
    while(true) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    dontstop();
    }
    }).start();
    }
    }
    public static void main(String[] args) {
    new StackOOM().stackLeakByThread();
    }
    }
    有两种可能的错误,第一种是线程请求的栈深度超出了虚拟机的允许范围,会产生 StackOverflowError 异常,第二种是虚拟机在扩展栈时无法申请到足够空间,会产生 OutOfMemoryError 异常。
    注意,一个 java 进程的内存容量是由操作系统决定的,Windows 下限制为 2GB,减去 Xmx(最大堆容量)、MaxPermSize(最大方法区容量)、程序计数器(很小)、虚拟机本身耗费的内存,剩下的就由虚拟机栈和本地方法栈瓜分了。
    前者比较容易找出错误,因为会有错误堆栈可以分析;
    后者往往是因为线程分配过多了,导致操作系统分配的内存用尽,事实上,每个线程的主要空间都被栈(虚拟机栈和本地方法栈)占用了,所以为了可以分配更多的线程,可以减少最大堆容量或者减少栈容量。
  • 本机直接内存溢出
    这个例子比较复杂,首先-XX:MaxDirectMemorySize 指定了直接内存大小(默认是-Xmx),然后越过了 DirectByteBuffer,反射获取 Unsafe 实例进行内存分配,allocateMemory 等价于 malloc
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
    */
    public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
    Field unsafeField = Unsafe.class.getDeclaredFields()[0];
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);
    while(true) {
    unsafe.allocateMemory(_1MB);
    }
    }
    }
    直接内存,或者说堆外内存,不是在 java 虚拟机规范中定义的存储区域,一般是不受虚拟机控制的,但是 NIO 提供了 Native 函数库可以直接分配堆外内存。
    由 DirectMemory 导致的内存溢出在 Heap Dump 中不会有明显的异常,如果 OOM 之后 Dump 文件很小,且程序中又直接或间接使用了 NIO,就可以考虑是这方面的问题。

分析内存

为排查内存泄露、young gc耗时过长等问题,我们需要分析内存结构。

判断死锁

使用 jstack 判断死锁,下面是一段测试用的死锁代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class DeadLockTest {

private static Object obj1 = new Object();
private static Object obj2 = new Object();

public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}

private static class Thread1 implements Runnable {

@Override
public void run() {
synchronized (obj1) {
System.out.println("Thread1 拿到了 obj1 的锁!");
try {
// 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println("Thread1 拿到了 obj2 的锁!");
}
}
}
}

private static class Thread2 implements Runnable {

@Override
public void run() {
synchronized (obj2) {
System.out.println("Thread2 拿到了 obj2 的锁!");
try {
// 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println("Thread2 拿到了 obj1 的锁!");
}
}
}
}
}

使用 jps 查看 Java 进程:

1
2
3
4
5
6
7
# jps

4098 Jps
2644
3991 Launcher
3992 DeadLockTest
2859 RemoteMavenServer

使用 jstack 查看 Java 进程中的所有线程:

1
2
3
# jstack 3992

结果见下图

JVM死锁

服务器 CPU 打满怎么排查

CPU 打满会导致服务器响应速度变慢甚至夯住,一般查看内存没有明显问题后我们就可以怀疑是有线程运行将 CPU 打满了。
1、查 CPU 占用率较高的进程
top 命令查进程占用 CPU
2、查该进程占用 CPU 最高的线程
top -H -p <查出的进程号>

1
2
3
4
5
6
7
8
9
10
11
12
root@app02:~# top -H -p 1153
top - 21:04:21 up 227 days, 6:52, 1 user, load average: 0.78, 0.75, 0.85
Threads: 1007 total, 0 running, 1007 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.2 us, 0.8 sy, 0.0 ni, 95.8 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 49049564 total, 295072 free, 42779556 used, 5974936 buff/cache
KiB Swap: 8191996 total, 431488 free, 7760508 used. 4009280 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1487 root 20 0 19.158g 1.113g 6664 S 6.2 2.4 0:36.42 java
1153 root 20 0 19.158g 1.113g 6664 S 0.0 2.4 0:00.00 java
1155 root 20 0 19.158g 1.113g 6664 S 0.0 2.4 0:00.57 java
1160 root 20 0 19.158g 1.113g 6664 S 0.0 2.4 2:01.54 java

转成 16 进制:

1
2
hero@app02:~$ printf "%x\n" 1487
5cf

之后我们会用到这个值,因为 jstack 输出的 log 中使用十六进制表示线程编号。
3、输出

1
root@app02:~# jstack 1153 > test_jstack.txt

从结果中可以搜到上面给出的线程编码5cf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
11179 "New I/O worker #168" #299 daemon prio=5 os_prio=0 tid=0x00007f3a75127000 nid=0x5cf runnable [0x00007f37278f7000]
11180 java.lang.Thread.State: RUNNABLE
11181 at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
11182 at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
11183 at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79)
11184 at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
11185 - locked <0x00000000e4669320> (a sun.nio.ch.Util$2)
11186 - locked <0x00000000e4669310> (a java.util.Collections$UnmodifiableSet)
11187 - locked <0x00000000e46691e8> (a sun.nio.ch.EPollSelectorImpl)
11188 at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
11189 at org.jboss.netty.channel.socket.nio.SelectorUtil.select(SelectorUtil.java:68)
11190 at org.jboss.netty.channel.socket.nio.AbstractNioSelector.select(AbstractNioSelector.java:434)
11191 at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:212)
11192 at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:89)
11193 at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
11194 at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
11195 at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
11196 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
11197 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
11198 at java.lang.Thread.run(Thread.java:745)

SWAP 影响 GC

SWAP 和 GC 同时发生会导致 GC 时间很长,JVM 严重卡顿,甚至导致服务崩溃。
JVM 进行 GC 时,需要对相应堆分区的已用内存进行遍历;假如 GC 的时候,有堆的一部分内容被交换到 SWAP 中,遍历到这部分的时候就需要将其交换回内存,同时由于内存空间不足,就需要把内存中堆的另外一部分换到 SWAP 中去;于是在遍历堆分区的过程中,(极端情况下)会把整个堆分区轮流往 SWAP 写一遍。

QA

栈溢出和由栈引起的OOM有什么关系?

虽然都是由递归调用引起的,但是这两种异常引起的条件并不相同:

  1. 栈溢出(StackOverflowError)
    方法调用栈深度超出了虚拟机的允许范围。
  2. 栈引起的OOM
    虚拟机在扩展栈时无法申请到足够空间。