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

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

1,java堆溢出

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

深入理解java虚拟机 第2章(三):OutOfMemoryError异常_第1张图片

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

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

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

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

2,java栈溢出

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

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

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

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

深入理解java虚拟机 第2章(三):OutOfMemoryError异常_第4张图片

在单线程下,无论是栈帧太大还是栈容量较小,均抛出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 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信息去填满方法区,直到溢出。

你可能感兴趣的:(jvm学习笔记)