深入理解java虚拟机 第2章(三):OutOfMemoryError异常

时间:2023-01-01 23:53:58

出程序计数器之外,运行时数据区的几块内存区域均会出现OutOfMemoryError异常。

1,java堆溢出

java堆的作用是创建实例对象,分配内存。为防止内存越用越少,java使用了GC,垃圾回收机制,将内存中失效的对象内存空间进行回收,如果当java堆中充满大量对象,堆内存空间不够,则会出现OOM异常。

深入理解java虚拟机 第2章(三):OutOfMemoryError异常

对于该示例代码则是不断产生对象,充斥java堆,

深入理解java虚拟机 第2章(三):OutOfMemoryError异常

一般遇到堆内存OOM,排查方式:
检查虚拟机堆内存参数(-Xms,-Xmx);

代码上检查是否存在某些对象的生命周期过长,持有状态时间过长,减少程序运行期间内存消耗;

2,java栈溢出

栈容量只有参数-Xss设定,对于栈异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出
    *Error异常

  • 如果虚拟机在扩展栈时,无法申请到足够内存,则抛出OOM异常。

测试使用-Xss参数减小栈内存容量。
深入理解java虚拟机 第2章(三):OutOfMemoryError异常

深入理解java虚拟机 第2章(三):OutOfMemoryError异常

在单线程下,无论是栈帧太大还是栈容量较小,均抛出StaticError异常。

如果是建立多线程导致的内存溢出,在不减少线程数的情况下,只能通过减少最大堆和减少栈容量,来获取更多的线程。

package chapter2;

/**
* VM Args :-Xss2m //谨慎,以防造成系统假死
* @author admin
*
*/

public class JavaVMStackOOM {
public void stackLeakByThread() throws OutOfMemoryError{
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
// System.out.println("new Thread : " + Thread.currentThread().getName());
}

}
}).start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
try {
oom.stackLeakByThread();
} catch (Throwable e) {
System.out.println("new Thread : " + Thread.currentThread().getName());
System.out.println(e.getMessage());
throw e;
}

}
}

容易造成jvm死机。。反正我测试时是死机了。。

3,方法区和运行时常量池溢出

jdk1.7之前存在“永久代”的问题(GC扩展至方法区,可以管理这部分内存,但是永久代有-XX:MaxPermSize上限限制,容易造成内存溢出)

package chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* VM Args: -XX:PermSize=10m -XX:MaxPermSize=10m
*
* @author admin
*
*/

public class RuntimeConstantPoolOOM {

/**
* 方法区存放了类的信息,对于该区域的内存溢出测试可通过创建大量的类填满方法区,直到溢出
* jdk1.7会一直循环下去,因为常量池移除了永久代,不受-XX:MaxPermSize限制
*/

private static void intern1() {
// 使用list保持对常量池的引用,防止GC回收常量池
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
System.out.println("list.size : "+list.size());
}
}

/**
* jdk1.6 : intern()把首次遇到的字符串实例化复制到永久代(字符串常量池),返回的也是永久代中字符串实例的引用,
* 而由StringBuilder创建的字符串实例在java堆上,返回的不是同一个引用。
*
* jdk1.7+: intern()实现不会再复制实例,只是在常量池中记录首次出现的字符串引用,
* 因此intern返回的引用和StringBuilder创建的字符串实例是同一个。
*/

private static void intern2() {
String str1 = new StringBuilder("计算机").append("书籍").toString();
System.out.println(str1.intern() == str1);

// java字符串已经在StringBuilder.toString之前存在,常量池已经存在引用,因此返回为false。
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

}

public static void main(String[] args) {
intern1();
// intern2();
}
}

方法区用于存放Class的相关信息(类名,访问修饰符,常量池等。。),对于该区域的测试思路是运行时产生大量的class信息去填满方法区,直到溢出。