1.JVM内存溢出几种情况
- PCR 程序计数器:用于记录正在执行的虚拟机字节码指令的地址,也是虚拟机规范中唯一未定义内存溢出的【内存区域】
- Java虚拟机栈:每一个方法的执行都对应着一个StackFrame栈桢的入栈和出栈过程,StackFrame用于存储局部变量、操作栈、动态链接、方法出口等信息。这块内存区域定义了2种内存溢出场景:当线程请求的栈深度超过虚拟机规定的最大栈深度,就会产生 StackOverFlowError 即栈溢出的异常;当栈深度足够但线程申请不到足够内存是会产生 OutOfMemoryError 即内存溢出的情况
- Java堆:虚拟机内存管理最重要的区域,用于存放对象实例。当堆内存不够并且扩展内存失败时会产生 OutOfMemoryError
- 方法区:存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,当方法区内存分配不足时也会产生 OutOfMemory
- 运行时常量池:属于方法区一部分,存放编译期间生成的各种字面常量和符号引用,这部分内容等到类加载之后存放到方法区运行时常量池中。因为常量池具备动态性、运行期间也可以放入新的常量。因此在常量池无法申请到足够内存时,也会产生OutOfMemoryError
- 直接内存:JDK加入NIO机制,允许基于Channel通道方式,使用Native方法直接分配堆外内存,能在一些大文件读取时显著提升性能。因为不需要在Java堆和本地堆中来回复制数据。但是本机内存会有物理限制,比如机器内存就只有8G,一旦直接内存不够时,也会发生OutOfMemoryError
2.内存溢出情况模拟
2.1 栈内存溢出
package out.of.memory.test;
/**
* Java虚拟机栈:内存溢出情况模拟 StackOverFlowError + OutOfMemoryError
* 虚拟机参数设置为:-Xss2M 即栈内存分配2M
*
* @author yli
*/
public class StackTest {
public static void main(String[] args) {
callSelf(0);
// 调用很容易导致系统死机...
// new StackTest().oom();
}
/**
* StackOverFlowError:
* 当线程请求栈深度超过虚拟机规定最大深度,就会产生这种异常
* 我们知道每一次方法调用就是StackFrame栈桢入栈和出栈过程
* 那么只要无限次调用方法久很容易产生栈深度不够的情况
*
* 要模拟这种异常非常简单,很自然联想到[递归调用]就是这么一种情况
* 递归调用就是无限调用自己:如果没有合适的退出机制,就产生栈溢出!
*
* @param callCount
*/
private static void callSelf(int callCount) {
// 打印方法被调用多少次
callCount++;
System.out.println(String.format("被调用%s次!", callCount));
// 只需要无限调用自己,并且不退出调用即可模拟!
callSelf(callCount);
}
/**
* OutOfMemoryError:申请栈扩展内存不足导致内存溢出
* 不停的创建局部变量可以模拟
*/
private void oom() {
while(true) {
new Thread(new Runnable() {
@Override
public void run() {
print();
}
}).start();
}
}
private void print(){
while(true) {
System.out.println(Thread.currentThread());
}
}
}
2.2 堆内存溢出
package out.of.memory.test;
import java.util.ArrayList;
import java.util.List;
/**
* 堆内存溢出测试
* 虚拟机参数设置为:-Xms10M -Xmx20M 即初始堆内存10M,最大堆内存20M
* @author yli
*
*/
public class HeapTest {
static class User {
long[] numns = { 1l, 2l, 3l, 4l };
}
/**
* 不停创建对象,由于对象实例存储在堆内存
* 就能很容易模拟堆内存溢出!
*
* @param args
*/
public static void main(String[] args) {
List<User> list = new ArrayList<User>();
while(true) {
User u = new User();
list.add(u);
}
}
}
2.3 常量池内存溢出
package out.of.memory.test;
import java.util.ArrayList;
import java.util.List;
/**
* 虚拟机参数设置为:-XX:PermSize=2M -XX:MaxPersmSize=5M
*
* @author yli
*/
public class ConstantPoolTest {
/**
* 常量池内存溢出:只要无限构造常量即可模拟
* @param args
*/
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
String s = "常量池内存溢出:只要无限构造常量即可模拟";
int i=0;
while(true) {
list.add((s+String.valueOf(i++)).intern());
System.out.println(i);
}
}
}
2.4方法区内存溢出
package out.of.memory.test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* 方法区内存溢出测试
* -XX:PermSize=2M -XX:MaxPermSize=5M
* @author yli
*
*/
public class MethodAreaTest {
/**
* 方法区用于存储虚拟机加载的类信息(每一个类都有与之对应的class对象)
* 也存储常量、静态变量、动态编译后的代码等数据
* 常量存放在常量池,通过ConstantPoolTest测试:不停构造常量即可构造内存溢出
* 方法区内存溢出也可以加载大量类导致内存溢出
*
*
*
* @param args
*/
public static void main(String[] args) {
while(true) {
Enhancer en = new Enhancer();
en.setSuperclass(UserImpl.class);
en.setUseCache(false);
en.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
en.create();
System.out.println("created!");
}
}
}
2.5 直接内存溢出
package out.of.memory.test;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
* 直接内存溢出:通过设置最大直接内存,并且避免堆内存扩展
* 模拟直接内存溢出,Unsafe.allocateMemory 类似于c语言的 malloc函数分配内存
*
* @author yli
*
*/
public class DirectTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
Field f = Unsafe.class.getDeclaredFields()[0];
f.setAccessible(true);
Unsafe us = (Unsafe)f.get(null);
while(true) {
us.allocateMemory(_1MB);
System.out.println(true);
}
}
}