在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。 这里通过例子初步了解一些最基本的与自动内存管理子系统相关的HotSpot虚拟机参数,来验证一下OOM的异常。
注意:不同虚拟机,甚至版本不一样,相关的配置可能也会不一样。这里使用 OracleJDK8里的Hotspot虚拟机来做实验。
一、Java堆溢出 1、什么是堆Dump堆Dump是反应Java堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。 一般在内存不足、GC异常等情况下,我们就会怀疑有内存泄露。这时我们就可以制作堆Dump来查看具体情况。
2、若出现 java.lang.OutOfMemoryError: GC overhead limit exceeded 异常官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
JVM加一个参数:-XX:-UseGCOverheadLimit 禁用这个检查,进一步就是 java.lang.OutOfMemoryError: Java heap space。
1)内存泄漏(Memory Leak)
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。即内存泄漏的堆积最终会导致内存溢出。
2)内存溢出(Memory Overflow)
内存溢出指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
4、Java堆溢出实例配置参数说明:
-Xms参数:设置堆的最小值
-Xmx参数:设置对的最大值
-XX: +HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
-XX:HeapDumpPath=${目录}参数:表示生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=目录/java_heapdump.hprof。
-XX:-UseGCOverheadLimit参数:禁用限制GC的运行时间检查(生产环境不要动它)。
注意:如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就是内存溢出,我们应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否有向上调整的合理空间。
实例一:内存泄漏导致内存溢出
//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/E/JVM/heap_dump.hprof -XX:-UseGCOverheadLimit
public class HeapDemo {
public static void main(String[] args) {
// 把对象放到集合中,避免GC回收掉
Set set = new HashSet();
while (true) {
set.add(new HeapTest());
}
}
static class HeapTest{}
}
由于HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的。栈的容量大小只能由-Xss 参数设置。
在《Java虚拟机规范》中,对虚拟机栈和本地方法栈的内存区域规定了两类异常状况:
1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2. 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
注意:《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,HotSpot虚拟机的选择是不支持动态扩展,所以:
在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常。只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,依然会出现OOM异常。
在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
1、栈溢出实例配置参数说明:
-Xss 参数:设置栈的容量大小
-Xoss参数:设置本地方法栈大小(实际上是没有任何效果的)。
实例一:单线程,减少栈容量空间(通过循环方法不停地创建栈帧,栈帧里的局部变量表里放基本类型)或者定义很多局部变量,把栈帧里的局部变量表撑大。
//-Xss128k
public class StackDemo {
private int var = 1;
public void stackLeak(){
var++;
stackLeak();
}
public static void main(String[] args) {
StackDemo stackDemo = new StackDemo();
stackDemo.stackLeak();
}
}
由于运行时常量池是方法区的一部分(一块内存区域),所以这两个区域的溢出测试可以放到一起进行。
String类的 intern()方法:当调用 intern 方法时,如果常量池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到常量池中,并返回此 String 对象的引用。
1、方法区溢出实例在JDK 6或更早之前的HotSpot虚拟机中,String常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX: MaxPermSize限制永久代的大小,即可间接限制其中String常量池的容量。
-XX:PermSize参数:设置Perm(俗称方法区/永久代)的最小值
-XX: MaxPermSize参数:设置Perm(俗称方法区/永久代)的最大值
在JDK 8中在永久代移除了,使用元空间来代替。字符串常量池和运行时常量池放在了Java堆里。元空间里只存储类和类加载器的元数据信息了。
-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
实例一:使用JDK 6使用JDK 6用 -XX:PermSize和 -XX: MaxPermSize限制永久代的大小。
//-XX:PermSize=5M -XX:MaxPermSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set set = new HashSet();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
如果使用JDK 7或更高版本的JDK来运行实例一,结果是不会重现JDK 6中的溢出异常,程序会执行下去。
实例二:使用JDK 7+使用JDK 7或更高版本的JDK用 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N参数限制元空间容量执行上面代码。
//-XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set set = new HashSet();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
由于字符串常量池放在了Java堆里,程序会执行下去。
实例三:使用JDK 7+使用JDK 7或更高版本的JDK用-Xms -Xmx参数限制Java堆容量执行上面代码。
//-Xms5M -Xmx5M -XX:-UseGCOverheadLimit
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set set = new HashSet();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
静态常量池主要用于存放两大类常量:字面量和符号引用量。如类名、访问修饰符、常量池、字段描述、方法描述等。
运行时常量池将class文件中的常量池载入到运行时常量池中(内存中),并保存在方法区中。
所以,对于这部分区域的实例,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。
实例四:使用 CGLIB工具类来操作字节码生成新的类。来填满元空间,直到溢出为止。
//-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
public class CGlibDemo {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloWorld.class);
enhancer.setUseCache(false); //关掉缓存
//不改变HelloWorld源码的基础上,在hello方法前后做增强
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("hello方法执行前...");
Object rs = methodProxy.invokeSuper(o, args);
System.out.println("hello方法执行后...");
return rs;
}
});
//动态创建代理类
HelloWorld helloWorld = (HelloWorld) enhancer.create();
helloWorld.hello();
}
}
}
class HelloWorld {
public void hello() {
System.out.println("hello world");
}
}
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。但是Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。sun.misc.Unsafe类,一般应用开发者不会用到这个类。
因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe:allocateMemory()。
参数说明:
-XX:MaxDirectMemorySize参数:设置直接内存的容量大小
JVM堆内存大小可以通过-Xmx来设置,同样的direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),在程序中中可以获得-XX:MaxDirectMemorySize的设置的值。
实例一:使用unsafe分配本机内存//-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryDemo {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
//通过反射获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
// 设置可访问权限,可以获取此类的私有成员变量的value
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
参考文章:
- 内存泄露与内存溢出:https://blog.csdn.net/qq_37933128/article/details/126969220
- Cglib及其基本使用:https://www.cnblogs.com/xrq730/p/6661692.html
- JVM -XX: 参数介绍:https://blog.csdn.net/xyw591238/article/details/51911394
- JVM源码分析之MetaspaceSize和MaxMetaspaceSize的区别:https://blog.csdn.net/duqi_2009/article/details/102097093
—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。