Java之内存区域与内存溢出异常

1. 运行时数据区域

1.1 程序计数器(线程私有)

程序计数器是一块较小的内存空间,可以理解为是当前线程所执行的字节码文件的行号指示器,在分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖该计数器完成;

程序计数器是线程私有的数据区,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,目的是为了线程切换后能恢复到正确的执行位置;

程序计数器在线程执行的是一个本地(Native)方法时,其计数器的值为空;

程序计数器是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域;

实战案例:

package cn.jackiegu.jvm.study.memory;

public class ProgramCounterRegisterTest {

    public static void main(String[] args) {
        Thread thread0 = new Thread(new R0());
        Thread thread1 = new Thread(new R1());
        thread0.start();
        thread1.start();
    }
}

class R0 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 打上断点
            System.out.println("thread0: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class R1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 打上断点
            System.out.println("thread1: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

分别在R0和R1类的run方法中打上以线程方式暂停的断点,然后以Debug方式运行main函数;

pcr1

这里的Thread-0和Thread-1就是在main函数中创建的那两个线程;

pcr2

选择其中一个线程断点,然后下面显示的内容(红框里面的内容)可以理解为当前线程正执行的Java类和代码行号,也正是程序计数器所记录的内容,在线程切换时其内容也将随之改变;如:run:37, R1(cn.jackiegu.jvm.study.memory)表示当前线程正执行到cn.jackiegu.jvm.study.memory包下的R1类中的run方法里,代码行号37;

pcr3


1.2 Java虚拟机栈(线程私有)

Java虚拟机栈也是线程私有的数据区,其生命周期和线程相同;

Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量、操作数栈、动态连接、方法出口等信息;每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;

Java虚拟机栈中,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展(HotSpot虚拟机的栈容量是不可以动态扩展的),当栈扩展时无法申请到足够的内存会抛出OutOfMemory异常;

Java虚拟机栈中每个线程的堆栈大小可以通过-Xss进行设置;

实战案例:

测试一:

package cn.jackiegu.jvm.study.memory;

/**
 * VM Args: 不限制
 */
public class JavaVirtualMachineStackTest {

    private Integer number = 1;

    public void overflow() {
        this.number++;
        overflow();
    }

    public static void main(String[] args) {
        JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
        try {
            // StackOverflowError异常测试
            javaVirtualMachineStackTest.overflow();
        } catch (Error e) {
            System.out.println(javaVirtualMachineStackTest.number);
            System.out.println(e);
        }
    }
}
// 运行结果
11667
java.lang.StackOverflowError

测试二:

重载overflow方法,观察执行结果

package cn.jackiegu.jvm.study.memory;

/**
 * VM Args: 不限制
 */
public class JavaVirtualMachineStackTest {

    private Integer number = 1;

    public void overflow(String message) {
        this.number++;
        char[] chars = new char[message.length()];
        for (int i = 0; i < chars.length; i++) {
            chars[i] = message.charAt(i);
        }
        String newMessage = new String(chars);
        overflow(newMessage);
    }

    public static void main(String[] args) {
        JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
        try {
            // StackOverflowError异常测试
            javaVirtualMachineStackTest.overflow("java vm stack");
        } catch (Error e) {
            System.out.println(javaVirtualMachineStackTest.number);
            System.out.println(e);
        }
    }
}
// 运行结果
4696
java.lang.StackOverflowError

测试三:

设置线程堆栈大小为128k,观察执行结果

jvms1

package cn.jackiegu.jvm.study.memory;

/**
 * VM Args: -Xss128k
 */
public class JavaVirtualMachineStackTest {

    private Integer number = 1;
    
    public void overflow(String message) {
        this.number++;
        char[] chars = new char[message.length()];
        for (int i = 0; i < chars.length; i++) {
            chars[i] = message.charAt(i);
        }
        String newMessage = new String(chars);
        overflow(newMessage);
    }
    
    public static void main(String[] args) {
        JavaVirtualMachineStackTest javaVirtualMachineStackTest = new JavaVirtualMachineStackTest();
        try {
            // StackOverflowError异常测试
            javaVirtualMachineStackTest.overflow("java vm stack");
        } catch (Error e) {
            System.out.println(javaVirtualMachineStackTest.number);
            System.out.println(e);
        }
    }
}
// 运行结果
723
java.lang.StackOverflowError

总结:当方法内部定义的变量越多,线程请求的栈深度就越浅;同时线程的堆栈大小设置得越小,线程请求的栈深度也越浅;


1.3 本地方法栈(线程私有)

本地方法栈与Java虚拟机栈所发挥的作用是非常相似的,其区别只是Java虚拟机栈是为虚拟机执行Java方法服务,而本地方法栈是为虚拟机使用到的本地方法服务;

在HotSpot虚拟机中,Java虚拟机栈和本地方法栈是被合二为一的;

本地方法栈同Java虚拟机栈一样,在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常;


1.4 Java堆(线程共享)

Java堆是虚拟机所管理的内存中最大的一块,其是被所有线程共享的,并且在虚拟机启动时创建;

Java中”所有的“对象实例以及数组都应当在堆上分配;

Java堆是垃圾收集器管理的内存区域;

Java堆对于当前主流的Java虚拟机都是按照可扩展来实现的,可通过参数-Xms-Xmx分别设置初始内存大小和最大可用内存大小,如果在Java堆中没有内存完成实例分配,且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常;

实战案例:

package cn.jackiegu.jvm.study.memory;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * VM Args: -Xms32m -Xmx32m
 */
public class JavaHeapTest {

    private final Integer randomNumber;

    private final String randomString;

    public JavaHeapTest(Integer randomNumber) {
        this.randomNumber = randomNumber;
        this.randomString = String.valueOf(randomNumber);
    }

    public static void main(String[] args) {
        Random random = new SecureRandom();
        List<JavaHeapTest> list = new ArrayList<>();
        try {
            while (true) {
                list.add(new JavaHeapTest(random.nextInt(99999)));
            }
        } catch (Error e) {
            System.out.println(list.size());
            System.out.println(e);
        }
    }
}
// 运行结果
303979
java.lang.OutOfMemoryError: GC overhead limit exceeded

1.5 方法区(线程共享)

方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据;

方法区有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来;

永久代与元空间(Meta-space)是HotSpot虚拟机不同版本对方法区实现的方式:

  • 在Java8之前采用的是永久代实现,在内存溢出时会抛出java.lang.OutOfMemoryError:PermGen space异常,可通过-XX:PermSize-XX:MaxPermSize两个配置参数进行限制永久代大小;
  • Java8及之后采用元空间实现,在内存溢出时会抛出 java.lang.OutOfMemoryError:Metaspace异常,可通过-XX:MetaspaceSize-XX:MaxMetaspaceSize两个配置参数进行限制元空间大小;

当方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常;

运行时常量池

运行时常量池是方法区的一部分,在Class文件中有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中,同时由符号引用翻译出来的直接引用也存储在运行时常量池中;

运行时常量池并非预置入Class文件中的常量池的内容才能进入,运行期间也可以将新的常量放入池中,如String.intern()函数;

运行时常量池在无法申请到内存时会抛出OutOfMemoryError异常;

实战案例:

设置初始元空间和最大元空间大小为32m,观察执行结果

ma1

package cn.jackiegu.jvm.study.memory;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

/**
 * VM Args: -XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32m
 */
public class MethodAreaTest {

    public static void main(String[] args) {
        int i = 0;
        try {
            while (true) {
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOM.class);
                enhancer.setUseCache(false);
                enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o, args));
                enhancer.create();
            }
        } catch (Error e) {
            System.out.println(i);
            System.out.println(e);
        }
    }
}

class OOM {

}
// 运行结果
3176
java.lang.OutOfMemoryError: Metaspace

2. 非运行时数据区域

2.1 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域;它是在JDK1.4中新加入的NIO中引入的,是一种基于通道与缓冲区的 I/O 方式,它可以使用Native函数库直接分配堆外内存,然后通过一个储存在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作;

直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不指定,则默认与Java堆最大值(由-Xmx指定)一致;

package cn.jackiegu.jvm.study.memory;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class DirectMemoryTest {

    private static final long _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}
// 运行结果
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at cn.jackiegu.jvm.study.memory.DirectMemoryTest.main(DirectMemoryTest.java:22)

3. 备注

部分内容摘抄于《深入理解Java虚拟机》-周志明(第3版)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JackieGGu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值