在测试JVM每个内存区域的内存溢出异常(OutOfMemoryError)之前,先来了解几个关于指定JVM内存区域的JVM参数:
- -Xms 和-Xmx : -Xms用于指定JVM运行时堆的初始化空间大小,而-Xmx用于指定JVM运行时堆的最大空间,当两个参数不一样时,在JVM启动时会将空间大小指定为-Xms指定的值,当空间不足时会扩展堆空间,直至堆空间达到-Xmx参数指定的大小。如果不想堆空间在运行时动态扩展,可将-Xms和-Xmx指定大小相同。
- -Xss:指定栈空间的大小。这里所说的栈空间大小不是指所有线程运行时的栈空间的总大小,而是为每一个线程分配的栈空间大小。
- -XX:MetaspaceSize和-XX:MaxMetaspaceSize:示例是基于JDK1.8所演示的,所以此处说明的参数就是指定元空间(方法区)空间大小参数的。两个参数也分别是指定初始化的元空间参数和最大元空间参数。
那么接下来就演示堆、栈和元空间(方法区)的OOM异常产生的情况。
堆内存的溢出
模拟堆内存溢出就需要知道堆内存中放置的是new出来的对象,同时对象还必须是不能被回收(也就是说被其他对象应用),那么随着对象的创建,当堆内存不能够容纳新增的对象时,就会抛出OutOfMemoryError异常,实例代码如下:
/**
* @Description: 堆内存溢出测试
* -Xmx64m -Xms64m -XX:+PrintGC
* @Author: binga
* @Date: 2020/8/26 14:24
* @Blog: https://blog.csdn.net/pang5356
*/
public class HeapOOMTest {
public static void main(String[] args) {
List<User> users = new ArrayList<>();
for (;;) {
users.add(new User());
}
}
}
class User {
private byte[] _data = new byte[1024]; // 1kb
}
运行一段时间后抛出如下异常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.binga.jvm.oom.User.<init>(HeapOOMTest.java:24)
at com.binga.jvm.oom.HeapOOMTest.main(HeapOOMTest.java:18)
在这里创建一个ArrayList保持对新建对象的引用,那么在GC时对象不会被回收,随着对象的创建,堆内存不足以容纳更多的对象时抛出OutOfMemoryError,且指定为Java heap space,也就是堆内存空间。
栈内存溢出
在《Java虚拟机规范》中描述了两种关于栈空间的异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError;
- 如果虚拟机的栈内存允许动态扩展,当扩展容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
首先来说第一中情况。了解栈空间结构的都知道,其结构是一个栈,栈中的元素是栈帧,而栈帧对应每一个方法,那么可以通过递归的调用,是为当前线程分配栈一直添加栈帧,从而导致线程的栈空间达到最大的深度。如下:
/**
* @Description: 栈内存溢出
* -Xmx64m -Xms64m -Xss128k -XX:+PrintGC
* @Author: binga
* @Date: 2020/8/26 14:34
* @Blog: https://blog.csdn.net/pang5356
*/
public class StackOOMTest1 {
public static int count = 0;
public static void main(String[] args) {
try {
recurse();
} catch (Throwable e) {
System.out.println(count);
e.printStackTrace();
}
}
public static void recurse() {
count++;
recurse();
}
}
运行结果如下:
1097
java.lang.StackOverflowError
at com.binga.jvm.oom.StackOOMTest1.recurse(StackOOMTest1.java:25)
at com.binga.jvm.oom.StackOOMTest1.recurse(StackOOMTest1.java:26)
at com.binga.jvm.oom.StackOOMTest1.recurse(StackOOMTest1.java:26)
可以看到抛出的是StackOverflowError。这符合刚才的第一种说法,不断的向栈中压入栈帧,从而导致栈达到做大深度而抛出StackOverflowError异常。当然还可以通过增加栈帧的大小从而来制造StackOverflowError,我们知道栈帧中包含局部变量表、操作数栈、动态链接和方法出口,这里很明显可以通过增加局部变量表来增大栈帧的占用空间,如下:
/**
* @Description: 栈内存溢出 通过增加栈帧的大小
* -Xmx64m -Xms64m -Xss128k -XX:+PrintGC
* @Author: binga
* @Date: 2020/8/26 22:46
* @Blog: https://blog.csdn.net/pang5356
*/
public class StackOOMTest2 {
public static int count = 0;
public static void main(String[] args) {
try {
recurse();
} catch (Throwable e) {
System.out.println(count);
e.printStackTrace();
}
}
public static void recurse() {
long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50;
count++;
recurse();
unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 =
unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 =
unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 = unused30 =
unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40 =
unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = 0;
}
}
运行结果如下:
98
java.lang.StackOverflowError
at com.binga.jvm.oom.StackOOMTest2.recurse(StackOOMTest2.java:30)
at com.binga.jvm.oom.StackOOMTest2.recurse(StackOOMTest2.java:31)
at com.binga.jvm.oom.StackOOMTest2.recurse(StackOOMTest2.java:31)
相比没有那么多局部变量的方法其栈的深度更小了。
接下来来看第二种情况,这种情况不是针对一个线程的栈空间了,而是指在随着线程的创建,不停的从内存中分配空间给每一个新创建的线程,那么当线程创建足够多,同时内存不足以分配空间给新的线程则会抛出OutOfMemoryError异常,实例代码如下:
/**
* @Description: 栈空间OutOfMemoryError
* -Xmx64m -Xms64m -Xss128k
* @Author: binga
* @Date: 2020/8/26 23:04
* @Blog: https://blog.csdn.net/pang5356
*/
public class StackOOMTest3 {
public static void main(String[] args) {
StackOOMTest3 stackOOMTest3 = new StackOOMTest3();
stackOOMTest3.stackWithThread();
}
private void dontStop() {
while (true) {
}
}
public void stackWithThread() {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
}).start();
}
}
}
需要注意的是,该段代码强烈不建议执行,回导致电脑卡死。这只是用于说明为每一个线程分配的空间是从直接内存中划分的。
元空间内存溢出
元空间主要用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、字符描述、方法描述等。所以针对于该空间的内存溢出测试,可以向其中一直加载类,这里借助于cglib来进行测试:
/**
* @Description: 方法区内存溢出测试
* -Xms512m -Xmx512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=64m
* @Author: binga
* @Date: 2020/8/26 14:42
* @Blog: https://blog.csdn.net/pang5356
*/
public class MethodAreaOOMTest1 {
public static void main(String[] args) {
try {
while (true) {
Thread.sleep(5);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Simple.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
class Simple {
public void doSomething() {
System.out.println("hello");
}
}
运行结果如下:
java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at com.binga.jvm.oom.MethodAreaOOMTest1.main(MethodAreaOOMTest1.java:32)
在异常抛出时指定了溢出的内存空间为Metaspace,证明是元空间内存溢出。元空间一般是用于存放加载的类信息的,针对需要加载很多类文件的程序,在配置元空间空间大小的参数时可以指定-XX:MetaspaceSize -XX:MaxMetaspaceSize两个参数相等,那么在程序启动过程中不至于频繁的由于元空间不足导致发生GC从而影响程序的启动效率。
上面测试的是不停的创建类信息并加载至元空间,从而导致元空间占用不停的增加,最后导致元空间的内存溢出,我们知道元空间存储了类元信息和常量池。这里需要着重说明的就是常量池(基于JDK1.8)。在JVM有三种常量池:
- 字符串常量池(也叫全局字符串池、string pool、string literal pool)。
- 运行时常量池(runtime constant pool)当程序运行到某个类时,class文件中的信息就会被解析到内存的方法区里的运行时常量池中。每个类都有一个运行时常量池。
- class文件常量池(class constant pool)class常量池是在编译后每个class文件都有的,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
这里来着着重说明一下字符串常量池,字符串常量池在JDK1.6及之前是位于方法区中的,并且使用-XX:PermSize和XX:MaxPermSize指定方法区的大小,从而间接的限定字符串常量池的大小。但是在JDK1.7之后呢,将字符串常量池迁移到了堆中,那么间接限制字符串常量池大小参数就编程了-Xms和-Xmx两个参数。但是到JDK1.8呢?就有疑惑了(当然是作者本人的疑惑),JDK1.8抛弃了方法区,引入了元空间,而又说元空间中包含类元信息、常量池等。只是通过这句话的描述,给人第一感觉就是字符串常量池也位于元空间之中。那么JDK1.8的字符串常量池究竟位于何处呢?可以通过String的intern方法,intern方法是判断调用该方法的字符串是在常量池中,没有则在常量池中创建一个新的字符串,字符串内容与调用这相同,同时返回常量池的字符串的引用,如果有则返回字符串常量池中的引用。示例如下:
/**
* @Description: 方法区溢出
* -Xms64m -Xmx64m -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
* @Author: binga
* @Date: 2020/8/26 15:19
* @Blog: https://blog.csdn.net/pang5356
*/
public class MethodAreaOOMTest2 {
public static void main(String[] args) throws InterruptedException {
Set<String> strs = new HashSet<>();
int count = 0;
while(true) {
strs.add(String.valueOf(count++).intern());
}
}
}
运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.HashMap.newNode(HashMap.java:1734)
at java.util.HashMap.putVal(HashMap.java:630)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at com.binga.jvm.oom.MethodAreaOOMTest2.main(MethodAreaOOMTest2.java:19)
通过报错并不能知道究竟是哪个区域内存溢出了,只是收到一个GC overhead limit exceeded异常,这个异常是指:
当前已经没有可用内存,经过多次GC之后仍然没能有效释放内存。众所周知,JVM的GC过程会因为STW,只不过停顿短到不容易感知。当引起停顿时间的98%都是在进行GC,但是结果只能得到小于2%的堆内存恢复时,就会抛出java.lang.OutOfMemoryError: GC overhead limit exceeded这个错误。那么如何避免这个错误,直接排除指定区域的提示呢?可以使用以下参数:
-Xms64m -Xmx64m -XX:MetaspaceSize=8m
-XX:MaxMetaspaceSize=8m -XX:-UseGCOverheadLimit
那么再次执行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3084)
at com.binga.jvm.oom.MethodAreaOOMTest2.main(MethodAreaOOMTest2.java:19)
这里抛出是堆内存溢出,当然也可以借助Java visualVM来查看整个过程,如下:
可以看到,Metaspace空间并没增长,而是堆内存已经涨满,所以可以证明JDK1.8的字符串常量池仍然位于堆中。当然针对于JDK1.6的可以使用上面的代码测试,但是需要增加-XX:PermSize和-XX:MaxPermSize两个参数指定方法区的大小。
示例代码