Java 内存区域与内存溢出异常(三)

实战:OutOfMemoryError 异常

参考:《深入理解Java虚拟机》-jvm高级特性与最佳实现(周志明著)

之前的两篇中介绍Java虚拟机中各个运行时内存区域的作用,这节中通过人为异常的方式验证各个运行时区存储的内容

一、Java堆溢出

Java堆中用于存储对象的实例,所以只要不断创建对象,并且保证GC Roots到对象之间有可达路径(保证对象有引用,而不会被GC回收)来避免垃圾回收机制清除这些对象。那么在数量达到最大堆容量的限制后就会产生内存溢出。

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

/**
 * 测试堆内存溢出
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\app\file\java-log\
 *
 * 启动参数设置Java堆大小为20m,不可扩展,通过设置-XX:HeapDumpOnOutOfMemoryError 参数
 * 可以让虚拟机在出现内存溢出时Dump出当前的内存堆转存储快照
 */
public class HeapOOM {
    static class OOMObject{

    }
    public static void main(String[] args) {
        // list 引用保证GC Roots 到对象之间有可达路径
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true){
            list.add( new OOMObject());
            System.out.println("list中添加了"+list.size()+"个对象");
        }
    }
}



// 异常堆栈
……
list中添加了810325个对象
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7576.hprof ...
Heap dump file created [28502312 bytes in 0.088 secs]
Disconnected from the target VM, address: '127.0.0.1:53242', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.xiaozhameng.jvm.HeapOOM.main(HeapOOM.java:21)

当出现Java堆内存溢出时,异常堆栈信息java.lang.OutOfMemoryError: 会跟着进一步提示:Java heap space

要解决这个区域的异常,一般的手段是先通过内存映象分析工具对Dump出来的堆转存储快照进行分析,重点是要确认堆内存中的对象是否是必要的,也就是要确定到底出现了内存泄漏还是内存溢出

可以使用堆转存快照分析工具(如MAT)进行分析,定位内存溢出的问题所在,这里不再赘述,后面有时间再罗列

二、虚拟机栈和本地方法栈溢出

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

2、如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

对于第一点,可以通过设置大量的局部变量,增大本方法中的本地变量表的长度,最终抛出StackOverFlowError。

可以通过尝试不断创建线程的方式使得虚拟机栈发生OutOfMemoryError异常,但是这样产生的内存溢出与栈空间是否足够大没有任何联系,或者更加准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易发生内存溢出。

其原因是操作系统分配给每个进程的内存时有限制的,虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为操作系统内存减去Xmx(最大堆内存),再减去最大方法区容量(MaxPermSize),程序计数器的内存消耗很小,可以忽略。如果虚拟机进程耗费的内存不算在内。剩下的内存就由虚拟机栈和本地方法栈瓜分,每个线程分配到的容量越大,可以建立的线程数就越少。

这一点需要特别注意,如果是因为建立过多线程导致内存栈内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆内存。或者减少栈容量来获取更多的线程。

package com.xiaozhameng.jvm;

import org.junit.Test;

/**
 * Java 虚拟机栈和本地方法栈溢出测试
 *
 * 在HotSpot虚拟机中,虚拟机栈和本地方法栈合二为一,因此对于HotSpot虚拟来说,栈容量的大小设置只取决于 -Xss参数设置,关
 * 于虚拟机栈和本地方法栈,Java虚拟机规范中定义了两种异常
 * 1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
 * 2、如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
 */
public class JavaVMStackOFE {

    private int stackLenth = 1;

    /**
     * VM Agars : -Xss128k
     * 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
     *
     * ---------- 异常堆栈
     * 方法中已经有935个局部变量
     * Disconnected from the target VM, address: '127.0.0.1:57839', transport: 'socket'
     *
     * java.lang.StackOverflowError
     * 	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
     * 	at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
     * 	at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
     * 	at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
     * 	at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
     * 	at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
     * 	at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
     * 	at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
     * 	at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
     * 	at java.io.PrintStream.write(PrintStream.java:526)
     * 	at java.io.PrintStream.print(PrintStream.java:669)
     * 	at java.io.PrintStream.println(PrintStream.java:806)
     * 	at com.xiaozhameng.jvm.JavaVMStackOFE.testStack_StackOverFlowError(JavaVMStackOFE.java:24)
     *
     */
    @Test
    public void testStack_StackOverFlowError(){
        stackLenth ++;
        System.out.println("方法中已经有"+stackLenth+"个局部变量");
        testStack_StackOverFlowError();
    }

    /**
     * 空方法
     */
    private void dontStop(){
        while (true){
        }
    }

    /**
     * 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
     */
    public void testStack_OutOfMemoryError(){
        while (true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    /**
     * 执行该方法有较大的风险,可能会导致操作系统假死
     * @param args
     */
    public static void main(String[] args) {
        new JavaVMStackOFE().testStack_OutOfMemoryError();
    }
}

三、方法区和运行时常量池溢出

方法区存放的Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。关于这个区域的测试,思路是运行时产生大量的类去填满方法区

运行时常量池属于方法区的一部分,关于这个区域的测试,可以借助String.intern() 方法,它的作用是,如果字符串常量池中已经包含此字符串,则返回常量池中这个字符串对象;否则将此String字符串包含的对象添加到常量池中并返回对象的引用。

四、本机直接内存溢出:DirectMemory容量的设置可以通过-XX:MaxDirectMemorySize 设定,若不指定,则默认跟堆最大内存一直。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值