Java 虚拟机在执行 Java 程序的过程当中会把它管理的内存划分为若干个不一样的数据区域。它们各有用途,有些随着虚拟机进程的启动一直存在(堆、方法区),有些则随着用户线程的启动和结束而创建和销毁(程序计数器、虚拟机栈、本地方法栈)。html
《Java 虚拟机规范》中规定 Java 虚拟机管理的内存包括如下几个区域:java
下面简要分析各个区域的特色。数组
程序计数器(Program Counter Register),能够看作当前线程所执行的字节码的行号指示器(其实就是记录代码执行到了哪里)。特色以下:缓存
主要做用:记录线程执行到了哪里。bash
Java 虚拟机栈(Java Virtual Machine Stacks):Java 方法执行的线程内存模型。jvm
每一个方法被执行时,虚拟机栈都会建立一个栈帧(Stack Frame)用于存储局部变量表、操做数栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。其中局部变量表包括:布局
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示,其中 long 和 double 占用两个槽,其余类型占用一个槽。局部变量表所需内存空间在编译期完成分配,当进入一个方法时,该方法须要在栈帧中分配多大的局部变量空间是彻底肯定的,运行期间不会改变其大小。性能
虚拟机栈的特色:ui
线程私有;spa
生命周期与线程相同;
两类异常
主要目的:Java 方法执行的线程内存模型。
本地方法栈(Native Method Stacks)与 Java 虚拟机栈做用相似。两者区别:
异常与 Java 虚拟机栈相同。
主要目的:Native 方法执行的线程内存模型。
对多数应用来讲,Java 堆(Java Heap)是 JVM 管理的内存中最大的一块。
惟一目的:存放对象实例(【几乎全部】的对象实例都在这里分配内存)。
《Java 虚拟机规范》描述:全部对象实例及数组都应在堆上分配。
而从实现角度看,因为即便编译技术(尤为是逃逸分析技术的日渐强大),"栈上分配"等手段使得对象并不是彻底在堆上分配。
特色:
PS: "新生代"、"老年代"、"Eden 区"等一系列对堆的区域划分,只是部分垃圾收集器的一些共性或设计风格,而非虚拟机的固有内存布局,更非《Java 虚拟机规范》的划分。
将 Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
方法区(Method Area):用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,该区域也是线程共享的。又称"非堆"。
与方法区联系密切的一个概念是"永久代",下面简要介绍。
"永久代(Permanent Generation)",能够理解为 JDK 1.8 以前 HotSpot 虚拟机对《Java 虚拟机规范》中"方法区"的实现。从 JDK 1.六、1.7 到 1.8+,HotSpot 虚拟机的运行时数据区变迁示意图以下:
HotSpot VM JDK 1.6 的运行时数据区示意图以下:
JDK 1.7 中,将 1.6 中永久代的字符串常量池和静态变量等移到了堆中,以下(虚线框表示已移除):
而到了 JDK 1.8,则彻底废弃了"永久代",改用了在本地内存中实现的"元空间(Metaspace)",将 JDK 1.7 中永久代剩余的部分(主要是类型信息)移到了元空间,以下(虚线框表示已移除):
从上面几张图能够看出永久代和元空间的主要区别有如下两点:
存储位置不一样
存储内容不一样:元空间存储的是「类型信息」(即类的元信息),而永久代除了类型信息,还包括「字符串常量池」和「静态变量」等(能够理解为元空间是永久代拆分出来的一部分)。
那么问题来了:为何要把永久代替换为元空间呢?
缘由大概有如下几点:
运行时常量池(Runtime Constant Pool)是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述外信息,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
相比于 Class 文件常量池的一个重要特性是「动态性」,运行期间也能够将新的常量放入池中(例如 String 类的 intern() 方法)。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也非《Java 虚拟机规范》定义的内存区域。但该部份内存被频繁使用(例如 NIO),并且可能致使 OutOfMemoryError。
$ java -version java version "1.8.0_191" Java(TM) SE Runtime Environment (build 1.8.0_191-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
$ java -version java version "1.7.0_80" Java(TM) SE Runtime Environment (build 1.7.0_80-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
public class HeapOOM { public static void main(String[] args) { List<Object> list = new ArrayList<>(); while (true) { list.add(new OOMObject()); } } static class OOMObject { } }
# 设置堆空间大小为 20M -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid39807.hprof ... Heap dump file created [27773554 bytes in 0.342 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) ...
public class StackOverflowError { private int stackLength = 1; private void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JvmStackOverflow sof = new JvmStackOverflow(); try { sof.stackLeak(); } catch (Throwable ex) { // 注意这里是 Throwable,而非 Exception (Error 不是 Exception) System.out.println("stack length: " + sof.stackLength); throw ex; } } }
因为 HotSpot 虚拟机不区分 Java 虚拟机栈和本地方法栈。所以 -Xoss
参数(设置本地方法栈大小)并无做用,栈空间只能由 -Xss
参数。
# Java 虚拟机栈大小 -Xss160K
stack length: 772 Exception in thread "main" java.lang.StackOverflowError at com.jaxer.example.JvmStackOverflow.stackLeak(JvmStackOverflow.java:11) at com.jaxer.example.JvmStackOverflow.stackLeak(JvmStackOverflow.java:12) ...
public class RuntimeConstantPoolOOM { static String baseStr = "string"; public static void main(String[] args) { List<String> list = new ArrayList<>(); while (true) { String s = baseStr + baseStr; baseStr = s; list.add(s.intern()); } } }
JDK 1.8 参数及异常:
# 最大堆空间为 10M,永久代为 10M (为便于观察,打印了启动命令和 GC 信息) -Xmx10m -XX:PermSize=10m -XX:MaxPermSize=10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10m; support was removed in 8.0 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3332) ...
JDK 1.7 参数及异常信息:
# 设置永久代大小为 10M -XX:PermSize=10m -XX:MaxPermSize=10m
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2367) ...
package com.jaxer.example.cglib; public class OOMObject { }
使用 CGLib 生成代码:
public class PermGenOOM { public static void main(String[] args) { try { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invoke(o, objects); } }); enhancer.create(); } } catch (Throwable t) { t.printStackTrace(); } } }
JDK 1.8 参数及异常:
# 设置元空间大小为 10M -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
java.lang.OutOfMemoryError: Metaspace at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) ...
JDK 1.7 参数及异常信息:
# 设置永久代大小为 10M -XX:PermSize=10m -XX:MaxPermSize=10m -XX:+PrintGCDetails
Exception in thread "main" Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
此处的异常没法被捕获,Debug 模式断点以下:
能够看到,这里实际仍是永久代(PermGen space)OOM 异常。
public class DirectMemoryOOM { private static final int _1M = 2014 * 1024; public static void main(String[] args) { List<ByteBuffer> list = new ArrayList<>(); while (true) { ByteBuffer buffer = ByteBuffer.allocateDirect(_1M); // java.lang.OutOfMemoryError: Direct buffer memory // ByteBuffer buffer = ByteBuffer.allocate(_1M); // java.lang.OutOfMemoryError: Java heap space list.add(buffer); } } }
# 设置堆内存最大为 20M,直接内存最大为 10M -Xmx20m -XX:MaxDirectMemorySize=10m
java.lang.OutOfMemoryError: Direct buffer memory
本文主要分析了《Java 虚拟机规范》中规定的 Java 虚拟机管理的运行时内存区域,并以 HotSpot 虚拟机为例,分析了 JDK 1.7 和 1.8 内存溢出的状况。主要内容总结以下图:
PS: 一些虚拟机参数以下
# 设置堆空间大小 -Xms20m -Xmx20m # 设置虚拟机栈空间大小 -Xss160K # 设置永久代大小 -XX:PermSize=10m -XX:MaxPermSize=10m # 设置元空间大小 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m # 打印 GC 日志 -XX:+PrintGCDetails # 打印命令行参数 -XX:+PrintCommandLineFlags # 堆栈信息 -XX:+HeapDumpOnOutOfMemoryError