引:之前面对JVM运行内存的分析,总会提到出现OutMemoryError异常,接下来我们详细看下常出现这种异常的现场。
Java堆溢出
我们通过限制Java堆的大小为20MB,不可扩张(将堆得最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump(备份)出当前的内存堆转储快照以便时候分析处理。
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14//VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
static class OOMobject {
}
public static void main(String[] args) {
List<OOMobject> list = new ArrayList<>();
while (true) {
list.add(new OOMobject());
}
}
}
运行结果:1
2
3
4
5java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid81611.hprof ...
Heap dump file created [27573572 bytes in 0.121 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)...
我们很容易在输出结果中看到Java heap space OOM出现在堆中。我们要解决这个区域的异常主要是通过内存映像分析工具(很多)对Dump出来的堆转储快照进行分析,重点确认是内存泄漏(Memory leak)还是内存溢出(Memory Overflow)。
内存泄漏:被分配的内存的对象不会被回收,永久占据内存。
解决方法:通过工具查看泄漏对象到GC Roots的引用链。
内存溢出:无法申请到内存。
解决方法:检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长的情况,尝试减少程序运行期的内存消耗。
虚拟机栈和本地方法栈溢出
我们说过HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,所以设置本地方法栈大小是无效的,栈容量只由-Xss参数设定,在Java虚拟机规范中描述了两种异常
- StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出此异常。
- OutOfMemoryError异常:如果虚拟机在扩展栈使无法申请到足够的内存空间,将抛出此异常。
我个人想如果单线程中栈的内存大小等于总内存大小,那么上面两种异常应该是等价的吧,但是基本上是不可能的。所以单线程中出现得基本上都是StackOverflowError异常。
单线程代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14// VM args: -Xss128k
public class HeapOOM {
static class OOMobject {
}
public static void main(String[] args) {
List<OOMobject> list = new ArrayList<>();
while (true) {
list.add(new OOMobject());
}
}
}
运行结果:1
2
3stack lenth:18855
Exception in thread "main" java.lang.StackOverflowError
at com.todorex.demo.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
这里抄一下书的结论:在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
多线程代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// VM args: -Xss2M
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
javaVMStackOOM.stackLeakByThread();
}
}
我在自己的机器上没有运行出来,可能需要点时间,不过机器变卡了,我想其实这里解释一下就好,它应该会抛出OutOfMemoryError异常。
解释:首先操作系统分给每个进程的内存是有限制的,所以总的方法栈的大小也是有限制的,但是每个线程都需要方法栈,所以线程建立的越多,剩余的方法栈内存就越小,一直创建线程,进程所拥有的内存终将被耗尽,到最后就会抛出OutOfMemoryError异常。
注意:线程数和方法栈大小是成反比的,所以在开发多线程的应用时应该特别注意,如果不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。
方法区(元空间)和运行时常量池溢出
运行时常量池溢出
在之前的博客也提到过在jdk1.6以及之前运行时常量池是放在方法区中的,存的是对象,所以可以设置虚拟机参数-XX:PermSize和-XX:MaxPermSize来限制方法区的大小,来模拟常量池溢出,但是jdk1.7及以后运行时常量池被移除了方法区,常量池存储的不再是对象,而是对象的引用,真正的对象存储在堆中,我们改变虚拟机参数为:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError运行下面程序:1
2
3
4
5
6
7
8
9
10
11// VM args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:1
2
3java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to java_pid10818.hprof ...
Heap dump file created [25172419 bytes in 0.251 secs]
上面结果提示GC开销超过限制,默认的话,如果你98%的时间都花在GC上并且回收了才不到2%的空间的话,虚拟机就会抛这个异常。
其实我们之前也提起过在JDK1.8及以后,字符串常量池从永久代移到到元空间中,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集,-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集,具体验证代码如下:1
2
3
4
5
6
7
8
9
10
11// VM args: -XX:MetaspaceSize=4M -XX:MaxMetaspaceSize=4M
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:1
2Error occurred during initialization of VM
OutOfMemoryError: Metaspace
关于这个字符串常量池的实现问题,还真的会出现一个很意思的问题或者说是一个很奇怪的问题。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// jdk:1.8
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
String str3 = new StringBuilder("ma").append("in").toString();
System.out.println(str3.intern() == str3);
}
}
运行结果:1
2
3true
false
false
jdk1.6中,intern()方法会把首次遇到的字符串实例复制在永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用。而在jdk1.7及以后,intern()的实现不会再复制实例,只是在常量池中记录首次出现得实例的引用,因此intern()返回的由StringBuilder创建的那个字符串是同一个实例,而关于上面的运行结果,我想java和main之前都是在字符串常量池中都有他的引用了,所以返回的都是false。
方法区溢出
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等,基本的思路是运行时产生大量的类去填充方法区,下面是借助CGLib(cglib和asm的依赖有个坑,选择cglib2.2,asm3.1亲测可用)来操作字节码运行时生成大量的动态类,这种场景在Spring,Hibernate中经常出现,需要多注意,本人使用的JDK1.8,所以测试的是方法区的变迁元空间,代码实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// VM args:-XX:MetaspaceSize=4M -XX:MaxMetaspaceSize=4M
public class JavaMethodAreaOOM {
public static void main(String[]args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
OOMObject oomObject = (OOMObject) enhancer.create();
oomObject.sayHi();
}
}
static class OOMObject{
public void sayHi(){
System.out.println("hi");
}
}
}
结果输出:1
2Error occurred during initialization of VM
OutOfMemoryError: Metaspace
这类异常经常出现在web应用中,需要多注意。
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,在《深入理解Java虚拟机》中用了以下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14// VM args:-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
但是在自己电脑中没有运行成功,反而让自己的电脑死机了,这个地方还是没有弄懂??????,希望懂的大佬给我点支持。
这个异常在使用NIO中可能会出现,所以在使用的时候需要多注意。
总结
在总结得过程中,知道了各个内存区域可能会出现OOM的情况,重要的是了解了方法区在jdk1.6到1.7到1.8的变迁,有兴趣的人可以深入了解。