JVM内存结构
JVM的内存结构大致分为五个部分,分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。除此之外,还有由堆中引用的JVM外的直接内存。
下面将展开讲解这五个部分。
程序计数器
程序计数器(Program Counter Register),用于记录下一条JVM指令的执行地址(如果正在执行的是本地方法则为空)。例如下图中的JVM指令,当我执行到地址为0的指令时,程序计数器就会存下下一条指令的地址,也就是地址3。
要注意的是,程序计数器时线程私有的,每一个线程都有一个程序计数器,只有这么设计,当CPU因为时间片轮转等原因切换线程的时候,才能保存当前线程的执行进度。
Java 虚拟机可以同时支持多个执行线程。每个 Java 线程都有自己的 pc(程序计数器)寄存器。在任何时候,每个 Java线程都在执行单个方法的代码,即该线程的当前方法 。如果该方法不是本机方法,则 pc 寄存器包含当前正在执行的 Java 虚拟机指令的地址。如果线程当前正在执行的方法是native,那么Java虚拟机的pc寄存器的值是undefined。 Java 虚拟机的 pc 寄存器足够宽,可以容纳 returnAddress 或特定平台上的本机指针。
同时,程序计数器不会存在内存溢出,此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈
每个 Java 线程都有一个私有的 Java 虚拟机栈(Java Virtual Machine Stack),其生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个栈由多个栈帧(Frame)组成,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、常量池引用、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M:
java -Xss2M HackTheJava
JVM规范允许虚拟机栈具有固定大小或根据计算需要动态扩展和收缩。Java虚拟机规范中,对这个区域规定了两种异常状况:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,这种异常在无停止条件的递归情况下会发生;
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地(Native)方法服务。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
堆
Java堆(Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。几乎所有的对象实例和数组都在这里进行分配,是垃圾收集的主要区域(“GC 堆”)。
堆在实现时,可以是固定大小的,也可以时根据需要动态扩展的。堆的内存不需要是连续的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
新生代(Young Generation)
老年代(Old Generation)
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
方法区
方法区(Method Area)也是被所有线程共享的内存区域。它存储每个类的结构,存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法。
和堆一样不需要连续的内存,并且可以动态扩展,如果方法区中的内存无法满足分配请求,一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和字符串常量池等放入堆中。
JDK1.6及以前
JDK1.7
JDK1.8
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
例如下图,就是main函数进行System.out.println(“hello world”)时的JVM指令和运行时常量池。JVM指令后面的“#”地址对应着常量池里的类名、方法引用、字面量等的地址,从而成功执行指令。
字符串常量池
字符串常量池在1.6版本前放在方法区中,在1.7版本后放到了堆中。
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern方法,主动将串池中还没有的字符串对象放入串池
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池,会把串池中的对象返回
直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
OOM的类型
StackOverflowError
示例代码
-Xss256k
public class Demo1 {
private final AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
try {
demo1.stackOverflow();
} catch (Error e) {
System.out.println("num = " + demo1.num.get());
throw e;
}
}
private void stackOverflow() {
num.getAndIncrement();
stackOverflow();
}
}
错误信息
Exception in thread "main" java.lang.StackOverflowError
at com.cloudwise.demo.gc.Demo1.stackOverflow(Demo1.java:30)
at com.cloudwise.demo.gc.Demo1.stackOverflow(Demo1.java:30)
at com.cloudwise.demo.gc.Demo1.stackOverflow(Demo1.java:30)
解决方案
检查堆栈跟踪
仔细检查错误堆栈,并查找问题代码行号,从本例来看,明显是由错误的递归调用(Demo1.java:30)引起的。确定问题代码后,通过指定适当的终止条件来修复。
加大线程空间(-Xss)
如果优化过的程序仍然抛出StackOverflowError,则可以增加线程堆栈大小以允许更深的程序调用。
OutOfMemoryError: unable to create new native thread
示例代码
-Xss1m
-Xms30m
-Xmx50m
public class Demo2 {
public static void main(String[] args) {
int count = 0;
while (true) {
new Thread(() -> {
try {
TimeUnit.HOURS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t-" + count++).start();
}
}
}
错误信息
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at com.cloudwise.demo.gc.Demo2.main(Demo2.java:21)
解决方案
在操作系统级别增加线程数限制(不推荐)
使用线程池,具体标准可参考阿里巴巴Java开发手册第一章第7节并发处理。
谨慎使用线程池,及设置线程池参数,禁止无节制的创建线程
设置有意义的线程名称,方便问题回溯
OutOfMemoryError: Java heap space
示例代码
-Xms30m
-Xmx30m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/adams/Downloads/gc/demo3.hprof
public class Demo3 {
static int _1M= 1024 * 1024;
public static void main(String[] args) {
ArrayList<BigObj> list = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
list.add(new BigObj(_1M));
}
}
/**
* 每个对象申请1MB内存
*/
private static class BigObj {
private final byte[] bytes;
public BigObj(int length) {
this.bytes = new byte[length];
}
}
}
错误信息
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.cloudwise.demo.gc.Demo3$BigObj.<init>(Demo3.java:17)
at com.cloudwise.demo.gc.Demo3.main(Demo3.java:8)
解决方案
增加jvm分配内存(简单直接)
分析jvm堆快照,查找内存占用大的对象分布。定位到自定义对象后开始分析,
OutOfMemoryError: Metaspace / PermGen space
示例代码
-Xms30m
-Xmx30m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/adams/Downloads/gc/demo4.hprof
-XX:MetaspaceSize=10m
-XX:MaxMetaspaceSize=10m
public class Demo4 {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OomObj.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invoke(obj, args1));
enhancer.create();
}
}
private static class OomObj {
public OomObj() {
}
}
}
错误信息
jvisualvm演示
Heap
PSYoungGen total 9216K, used 326K [0x00000007fee00000, 0x00000007ff800000, 0x00000007ff800000)
eden space 8192K, 3% used [0x00000007fee00000,0x00000007fee51a18,0x00000007ff600000)
from space 1024K, 0% used [0x00000007ff700000,0x00000007ff700000,0x00000007ff800000)
to space 1024K, 0% used [0x00000007ff600000,0x00000007ff600000,0x00000007ff700000)
ParOldGen total 20480K, used 2708K [0x00000007fda00000, 0x00000007fee00000, 0x00000007fee00000)
object space 20480K, 13% used [0x00000007fda00000,0x00000007fdca5368,0x00000007fee00000)
Metaspace used 15206K, capacity 15300K, committed 15360K, reserved 1062912K
class space used 1313K, capacity 1364K, committed 1408K, reserved 1048576K
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
... 11 more
解决方案
因为该 OOM 原因比较简单(动态代理、jsp、脚本引擎编译等),解决方法有如下几种:
检查是否永久代空间或者元空间设置的过小,建议将MetaspaceSize和MaxMetaspaceSize设置成相同大小
检查代码中是否存在大量的反射操作
dump之后通过mat检查是否存在大量由于反射生成的代理类
放大招,重启JVM
OutOfMemoryError: Direct buffer memory
示例代码
-Xms30m
-Xmx30m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/adams/Downloads/gc/demo5.hprof
-XX:MaxDirectMemorySize=10m
public class Demo5 {
private static final int _1M= 1 << 20;
public static void main(String[] args) {
List<ByteBuffer> byteBuffers = new LinkedList<>();
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
byteBuffers.add(byteBuffer);
}
}
}
错误信息
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.cloudwise.demo.gc.Demo5.main(Demo5.java:18)
Heap
PSYoungGen total 9216K, used 411K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 5% used [0x00000007bf600000,0x00000007bf666e38,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen total 20480K, used 405K [0x00000007be200000, 0x00000007bf600000, 0x00000007bf600000)
object space 20480K, 1% used [0x00000007be200000,0x00000007be265728,0x00000007bf600000)
Metaspace used 3347K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 372K, capacity 388K, committed 512K, reserved 1048576K
解决方案
其dump文件有一个明显特点,就是文件很小,从dump文件中看不到明显的异常。如果项目中直接或间接的使用的NIO,可以考虑一下是不是这方面的原因。
java.lang.OutOfMemoryError:GC overhead limit exceeded
示例代码
-Xms30m
-Xmx30m
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/adams/Downloads/gc/demo6.hprof
-XX:+UseParallelGC
public class Demo6 {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(i++).intern());
}
} catch (Throwable t) {
System.out.println("i = " + i);
t.printStackTrace();
throw t;
}
}
}
错误信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.cloudwise.demo.gc.Demo6.main(Demo6.java:22)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.cloudwise.demo.gc.Demo6.main(Demo6.java:22)
解决方案
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
dump内存,检查是否存在内存泄露,如果没有,加大内存。
其他不常见OOM类型
java.lang.OutOfMemoryError: Requested array size exceeds VM limit:分配超大数组
java.lang.OutOfMemoryError: Out of swap space:swap溢出
java.lang.OutOfMemoryError: stack_trace_with_native_method:本地方法溢出
排查思路
获取heapdump文件
-XX:+HeapDumpOnOutOfMemoryError或jmap -dump:format=b,file=xxx.hprof <pid>
从日志或者dump文件中分析发生OOM的内存区域
分析内存对象,是否发生内存泄漏。分析问题对象到GC roots的引用关系,判断是否因为错误的对象生命周期引起。如果是,则优化代码(软引用等)。
调整对应的jvm参数,如加大内存、调整GC算法等
参考资料
手把手教你了解OOM | HeapDump性能社区
性能专题 | 由浅入深了解GC原理 | HeapDump性能社区