深度学习JVM(1):Java内存区域

1.运行时数据区

Java虚拟机在执行任务的时候往往会将它所管理的内存分为多个区域,而每个不同的区域都有各自的用途以及创造销毁时间。

1.1程序计数器(PC,Program Counter Register)

通用定义
程序计数器是CPU中的一个小型寄存器,用于存储下一个要执行的指令的地址。
Java中的表现
程序计数器是一个较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变计数器的值来选取下一条执行指令。分支,循环,跳转,线程恢复等功能都需要依赖计数器来完成。也是唯一一个在虚拟机中没有规定内存溢出的区域。
另外,如果线程正在执行Java方法,那么计数器里面存储的即是正在执行的虚拟字节码指令地址。如果是本地方法,计数器值为undefined。

Java方法:用Java语言编写,编译成字节码后由jvm解释执行或者jit编译后执行
本地方法:用其他语言(通常是C或者C++)编写,编译成与特定操作系统和硬件相关的机器码

1.2Java 虚拟机栈(Java Virtual Machine Stack)

Java虚拟机栈,早期也叫Java栈。每个线程在创建的时候都会相应创建一个虚拟机栈。同样,每个线程在结束的时候栈空间也会被回收,即Java虚拟栈与线程拥有相同生命周期。
在Java虚拟栈中,每调用一个Java方法,虚拟机栈中便会存储一个栈帧(Stack Frame)。每个方法在调用到执行完成的这个过程,同样也是栈帧进栈出栈的一个过程。

栈帧中存储着局部变量表,操作数栈,动态链接,方法正常退出或者异常退出的定义等

《Java虚拟规范》对Java虚拟机栈规定了两类异常状况:
1.线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
2.如果Java虚拟机栈容量可以动态扩展,当栈扩展无法申请到足够内存时抛出OutOfMemoryError

1.3本地方法栈(Native Method Stack)

本地方法栈与Java虚拟机栈相似,不同之处是本地方法栈为本地方法服务,即Java调用非Java的接口。
在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。

1.4堆(Heap)

Java堆是Java内存管理的核心区域,该区域被所有线程共享,用于存储绝大部分的对象实例。
《Java虚拟机》规范规定,Java堆可以处于物理上不连续的内存空间,但是逻辑上需要是连续的。在实现时,Java堆既可以是固定大小,也可以扩展,主流虚拟机都是可扩展的(通过-Xmx或者-Xms控制),如果堆中没有内存完成实例分配,抛出OutOfMemoryError异常。

1.5方法区(Method Area)

方法区同样被各个线程共享,用于存储元(Meta)数据,即被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。
JDK8之前使用永久代(Permanent Generation)实现方法区,JVM在启动时指定固定内存,容易造成内存溢出,而JDK8采用了元空间(Metaspace)实现方法区,使用本地内存,支持动态增长,提供更为灵活的内存管理机制。此外,元空间在内存分配和回收以及数据管理等方面也有优势。

1.6运行时常量池(Run-Time Constant Pool)

运行时常量池是方法区的一部分,Class文件中除了版本号,字段,方法,超类,接口等信息以外,还有一项很重要的信息就是常量池。常量池用于存放编译期生成的字面量和符号引用,比一般符号表存储的信息更加宽泛。

2.Java程序内存

Java程序内存 = JVM内存 + 本地内存
本地内存 = 元空间 + 直接内存

2.1本地内存(Native Memory)

本地内存即JVM进程中所占用的不受JVM管理的内存区域,该区域是操作系统直接分配给JVM进程的,不属于Java堆

2.2直接内存(Direct Memory)

直接内存是一种特殊的本地内存,它通过Java NIO(new I/O)包中的类来分配使用。它允许Java程序绕过Java堆直接访问操作系统的内存。故此直接内存的分配不受Java堆大小的限制,但是还是会受到本机总内存和处理器寻址空间的限制。
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域。

3.内存溢出异常

3.1内存溢出和内存泄漏的区别

内存溢出:OutOfMemory,指程序在申请内存,没有足够的内存空间供其使用
内存泄漏:Memory leak,指程序在申请内存,无法释放已申请的内存空间,内存泄漏最终导致内存溢出

3.2栈溢出

当线程请求的栈深度超过虚拟机允许的最大深度时,栈溢出,抛出StackOverfLowError异常,抛出该异常主要有以下几种原因
1.递归调用过深
由于系统会不断在栈中保存递归时产生的变量,在递归调用过于深的情况下,会造成栈溢出,导致递归无法返回

public class RecursiveStackOverflow {
    public static void recursiveMethod(int n) {
        System.out.println("Call " + n);
        recursiveMethod(n + 1);  // 无限递归
    }

    public static void main(String[] args) {
        recursiveMethod(1);
    }
}

2.栈内存设置过小
JVM允许参数(-Xss)设置栈的大小。如果设置栈的大小过小,即便正常的递归或者方法调用也会导致栈溢出。
3.局部变量过多

public class LocalVariablesStackOverflow {
    public static void methodWithManyLocals() {
        int var1, var2, var3, ..., var1000;  // 大量局部变量
        // 方法体
        methodWithManyLocals();  // 递归调用
    }

    public static void main(String[] args) {
        methodWithManyLocals();
    }
}

4.方法调用链过长

public class LongCallChainStackOverflow {
    public static void method1() { method2(); }
    public static void method2() { method3(); }
    public static void method3() { method4(); }
    // ... 假设有很多这样的方法
    public static void method1000() { 
        throw new RuntimeException("End of chain");
    }

    public static void main(String[] args) {
        method1();
    }
}

3.3运行时常量池溢出

运行时常量池(Runtime Constant Pool)溢出通常是由于大量动态生成的常量或方法引用占用了过多内存,最终导致内存耗尽并抛出 java.lang.OutOfMemoryError: PermGen space(JDK 7 及以前)或 java.lang.OutOfMemoryError: Metaspace(JDK 8 及以后)。较为常见的情形有大量的字符串通过intern()方法加入到字符串常量池或者大量动态生成的类和方法加入到常量池,进而耗尽内存

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值