本文章根据周志明写的深入理解jvm第二章,以及个人的理解,查阅的资料而编写
以下内容不做说明,jdk指1.6,虚拟机指 hotspot
运行时数据区
根据《java虚拟机规范 SE7版》 规定,java虚拟机所管理的内存将会包括一下几个运行时数据区
1.程序计数器
程序计数器是一块较小的内存区域,用于当前执行程序的字节码的行号指示,如果java在执行 native 方法的时候,则这个计数器为空(undefined),该区域是唯一一个在 jvm虚拟机中没有规定任何 OOM 的区域。该区域为线程私有
2.java虚拟机栈
与程序计数器一样,该区域也是线程私有的,它的生命周期与线程相同,其包含局部变量表部分,局部变量表里面存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,long,double,float),对象的引用(reference类型,可能是指向对象地址的指定,也可能是指向一个代表对象的句柄或其他相关的对象位置)和 returnAddress 类型。
该区域规定了两个异常,StackOverflow 和 OOM,该区域可以手动设置大小,通过 -Xss 设置栈大小
3.本地方法栈
该区域作用与java虚拟机栈很像,只不过是用来为本地方法执行提供服务。HotSpot 虚拟机就将 java虚拟机栈和本地方法栈 合二为一了。该区域一样规定有 StackOverflow 和 OOM
4.java堆
一般来说,Java堆是java虚拟机中锁管理的内存中最大的一块。java堆是被所有线程所共享的,在虚拟机启动的时候创建。此区域的唯一目标就是存放对象的实例,几乎所有的对象是你都要在这里分配内存。(jvm规范规定),但是随着 JIT 编译器的发展与逃逸分析技术的逐渐成俗,栈上分配,标量替换优化技术将会导致一些微妙的变化,所以所有的对象都分配在堆上也不是那么绝对了。
java堆是gc管理的主要区域,从内存回收的角度来看,由于现在很多收集器基本上都采用分代收集算法,所以java堆可以细分为:新生代和老年代;从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区。
不论如何划分,都与存放的内容无关,无论哪个区域,存储的依然都是对象的实例,进一步的划分只是为了更好的回收内存。该区域规定有 OOM 异常,该区域可以手动设置大小(-Xms 设置最小堆内存,-Xmx设置最大堆内存)。
5.方法区
该区域在1.8之后被元数据区域(放在本地内存中,和直接内存一样)取代,所存储的数据也被其他的区域瓜分。
该区域存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。在hotspot 虚拟机上,该区域被称之为永久代。jdk1.7之前可以使用 -XX:PermSize=xx 和 -XX:MaxPermSize=xx 来设置最小和最大内存。该区域规定有 OOM
设置元数据区域大小:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=50m
6.运行时常量池
该区域在jdk1.8之后被存放到了堆中。
该区域是方法区的一部分,用于存储类的版本,字段,方法,接口等描述信息,还有常量池,用于存储编译期生成的各种字面量和符号引用。java语言不要求常量一定只有编译期才能产生,运行期间也可以将新的常量放入池中,比如说 String.intern() 方法将String 方法常量池。
7.直接内存
java1.4之后引入了 NIO,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在java堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。避免了在 java堆和 Navite堆中来回复制数据。提高了性能。
实战OOM异常
以下例子为 jdk1.8,hotspot 虚拟机
1.堆溢出
设置运行参数: -Xms20m 0Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:// (指定打印出来的 dump 文件的路径)
import java.util.ArrayList;
/**
* -Xms20m 0Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:// (指定打印出来的 dump 文件的路径)
* 设置 最小堆内存为20m,最大也为 20m,打印 oom 时候的内存堆转储快照
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
ArrayList<OOMObject> list = new ArrayList<>();
int i = 0;
while (true){
list.add(new OOMObject());
}
}
}
运行后控制台截图
使用 java自带的 visualVM 打开 .hprof 文件,
可以看到出现问题的线程 main
点击之后,可以看到出现问题的位置
点击上面的 类 可以发现,该oom 就是因为有过多的 OOMObject 才导致的
2.虚拟机栈和本地方法栈溢出
对于 hotspot来说,因为虚拟机栈和本地方法栈合二为一了,所以设置本地方法栈大小的 -Xoss 参数是没有效果的。栈容量只由 -Xss 来设置。关于虚拟机栈和本地方法栈,在java虚拟机规范中描述了两种异常,
1.请求深度超过虚拟机所允许的最大深度,则抛出 StackOverflowError (例如不结束的 递归)
2.如果虚拟机在扩展的时候无法申请到足够的内存,则 OOM
-Xss128k 设置虚拟机栈内存为 128k
/**
* -Xss128k 设置虚拟机栈内存为 128k
*/
public class StackOverflow {
int stackLength = 0;
public void stackLeak(){
this.stackLength ++;
stackLeak();
}
public static void main(String[] args) {
StackOverflow overflow = new StackOverflow();
try {
overflow.stackLeak();
} catch (Throwable t) {
t.printStackTrace();
System.out.println("stack length is " + overflow.stackLength);
//throw t;
}
}
}
3.运行时常量池OOM
由于jdk1.8之后运行时常量池被划分到了 堆中,所以此处只需要将堆内存设置小,就可以轻松的看到错误
/**
* 运行时常量池 oom ,该区属于方法区,jdk1.6之前属于永久区,在jdk1.7之后开始去永久代,所以该代码在 1.7之后就不会达到预期的效果了
* -XX:PermSize=10m -XX:MaxPermSize=10m
* jdk1.8 之后该区域属于堆,所以改变该区域大小通过改变堆就可以达到
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
int i = 0;
while (true){
list.add( String.valueOf(i+++"========================================================================================================================================================================================================================================================================================================================================================================================================================>").intern() );
}
// String str1 = new StringBuffer("计算机").append("软件").toString();
// System.out.println( str1.intern() == str1 );
//
// String str2 = new StringBuffer("ja").append("va").toString();
// System.out.println( str1.intern() == str1 );
}
}
java.lang.OutOfMemoryError: GC overhead limit exceeded Dumping heap to java_pid108728.hprof ... Exception in thread "main" Heap dump file created [21150118 bytes in 0.059 secs] java.lang.OutOfMemoryError: GC overhead limit exceeded
4.本机直接内存溢出
DirectMemory 容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,则默认与java堆最大值一样。
/**
* 直接内存 oom
* 直接内存,独立于运行时数据区之外,是 nio 使用的
* DirectMemory 容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,默认与Java堆最大值(-Xmx指定)一样
* -Xmx10m -XX:MaxDirectMemorySize=10m
*
* 由DirectMemory 导致的oom 一个明显的特征就是在 Heap Dump 文件中看不到明显的异常 ,而且 dump 文件还非常小
*/
public class DirectMemoryOOM {
private static final int _1m = 1024*1024;
public static void main(String[] args) throws IllegalAccessException {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true){
unsafe.allocateMemory(_1m);
}
}
}
由DirectMemory导致的内存溢出,一个明显的特征就是在 HeapDump 文件中不会看见明显的异常,而且 dump文件还很小。
JDK1.8 JVM运行时数据区域划分参考:https://blog.csdn.net/bruce128/article/details/79357870