JVM内存划分与内存溢出小结
1.将堆的最小值(初始值)-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展,-Xmn参数设置堆中新生代容量大小,对应地,如果堆不可自动扩展情况下,老年代容量=(-Xms/-Xmx)-(-Xmn)
2.通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析,-XX:HeapDumpPath=d:/a.dump配置Dump出位置
3.对于HotSpot虚拟机来说,-Xoss参数(设置本地方法栈大小)虽然存在,但是实际无效,栈容量只由-Xss参数设定
而关于此,Java虚拟机规范描述了两个异常:
a,如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常(栈溢出)。
b,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常(堆溢出:堆溢出-OutOfMemoryError:java heap space,方法区溢出- OutOfMemoryError: PermGen space)。
测试:
a,定义大量的本地变量,增加此方法帧中本地变量表的长度(符合上面情况a),结果抛出StackOverflowError异常。
b,使用-Xss参数减少栈内存容量(符合上面情况a),结果抛出StackOverflowError异常。
实验表明:
在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配时,虚拟机都将抛出StackOverflowError异常(都一个原因:栈帧所需>虚拟机容量)。
多线程下,才会进行扩展栈操作,才可能产生内存溢出异常。
原因:
操作系统给每个进程分配的内存有限制,如32位是2GB,那么虚拟机进程本身消耗内存不计算在内的情况下,虚拟机栈和本地方法栈得到的内存=2GB-Xmx(最大堆容量)-MaxPermSize(最大方法区容量) ,程序计数器消耗内存小,忽略不计。这是一个定值,那么如果每个线程分配到的栈容量越大,还可建立的线程就越少,线程越多扩展栈时就越容易产生内存溢出异常,而单线程不存在扩展栈操作,只会比较该线程请求的栈深度与虚拟机最大深度而是否抛出StackOverflowError异常。
总结:
对于开发时,出现StackOverflowError异常时,有错误堆栈可以阅读,比较容易找到问题所在,而且在虚拟机默认参数下,栈深度在大多情况下达到1000-2000没问题,对于正常方法调用(包括递归),这个深度已经完全够用了。
但是,如果是建立多线程导致内存溢出,要么减少线程数、要么更换64位虚拟机,否则,只能通过减少最大堆容量(为了加大栈空间)和减少栈容量(为了减少栈深度)来换取更多的线程。
备注:
A.栈帧:每个方法被执行时都会创建一个栈帧,用于存储局部变量表(方法内变量)、操作栈、动态链接、方法出口等信息。每个方法被掉调用到完成就对于一个栈帧在虚拟机栈中从入栈到出栈的过程。
B.运行时数据区包括:
1.虚拟机栈,本地方法栈:虚拟机栈和本地方法栈类似,一个执行Java方法(字节码),一个执行虚拟机的Native方法,也叫作本地方法(Java中声明的可调用的,使用C/C++实现的方法-JNI,HotSpot虚拟机直接将两者合并)。
2.堆:堆存放对象(包含新生代和老年代,设置新生代大小-XX:NewSize=n)。
3.程序计数器:记录执行代码行号。
4.方法区:存放被虚拟机加载的类信息、常量、静态变量等,包含运行时常量池。类信息除了版本、字段、方法、接口、常量池(存放字面量和符合引用),常量池在类加载时会放入运行时常量池。
4.通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小
测试:
通过上面jvm参数设置较小方法区大小(如10M),调用String.intern方法添加一个字符串到运行时常量池中,出现OutOfMemoryError: PermGen space异常。
实验表明:
运行时常量池属于方法区。因为上面异常是方法区溢出。
备注:
String.intern(str)方法是一个native方法,作用是如果运行时常量池中包含一个等于str的字符串,就返回池中这个字符串的String对象,否则将str添加到池中并返回其引用。
5.类被GC回收的判定条件非常苛刻,所以,在经常动态产生大量class的应用中,应主要类的回收状态,如使用了GClib字节码增强的应用、大量jsp或动态产生jsp文件的应用、基于OSGi的应用
源自:《深入理解Java虚拟机:JVM高级特性与最佳实践》