深入理解java虚拟机--Java内存区域和内存溢出异常

前言

对于Java程序员来说,在虚拟机自动内存管理的机制下,不需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出的问题。不过,如果在编写程序时没有合理的创建对象,就会造成内存泄漏或者溢出这样的问题,如果不了虚拟机内存的区域划分以及创建的对象时虚拟机对它的内存分配情况,那么排查这些错误就会比较困难。下面我们来具体介绍一下Java内存区域以及内存溢出的相关知识。

Java运行时数据区域

java虚拟机在程序运行时把它管理的内存划分为若干个数据区域,这些区域都有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。它包括一下几个运行时数据区域,如下图所示。

JVM运行时内存图

图1 java运行时数据区图

程序计数器

程序计数器是比较小的一块儿内存区域,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型了,字节码解释器就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个程序计数器来完成。
它属于线程隔离的数据区,因为虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个确定的时刻,一个处理器或者一个多核处理器的一个内核只能来执行一个线程中的指令。因此为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,所以程序计数器它是线程私有的

JVM虚拟机栈

虚拟机栈也是线程私有的,它和程序计数器的生命周期是相同的。虚拟机栈它描述的是java方法执行的内存模型:每个方法在执行的同时都换创建一个栈帧,用来存储局部变量表操作数栈动态链接方法出口等信息。每个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存储了编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象的引用(reference类型,它不是对象本身,它只是指向了对象起始地址的引用指针,或者指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)
在该区域中,规定了两种异常状态如果线程请求栈深度大于虚拟机所允许的深度,则会抛出StackOverFlowError异常如果虚拟机可以动态扩展(当前大部分虚拟机都能够动态扩展),如果无法申请到足够的内存,那么就会抛出OutOfMemoryError异常

本地方法栈

本地方法栈和虚拟机栈非常类似,它是用来执行虚拟机调用到的Native方法。在虚拟机规范中对虚拟机使用的语言、使用方式和数据结构并没有强制的规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(sun hotspot)直接就把本地方法栈和虚拟机栈合为一个。与虚拟机栈一样,它也会抛出StackOverFlowError和OutOfMemoryError异常。

Java堆

java堆是虚拟机管理的最大的一块内存,在虚拟机的启动时创建。Java堆是被所有线程共享的一块区域,几乎所有对象的实例都存放在这块区域
java堆是垃圾收集管理的主要区域,所以也被叫做GC堆。从内存回收的角度来看,由于现在的收集器基本都采用分代收集的算法,所以java堆还可以细分为:新生代和老年代;再细分还有Eden空间、From Survivor空间、To Survivor空间。关于这些区域的分配和回收,我们会在之后的文章进行详细介绍。
java堆可以是物理上不连续的一片存储空间,只要逻辑上是连续的一片内存空间即可。在实现时,既可以固定大小的也可以是可扩展的,**当前主流的虚拟机都是按照可扩展的来实现的(通过-Xmx和-Xms来控制)**。如果在堆中没有完成内存的分配,则会抛出OutOfMemoryError的异常。

方法区

方法区和java堆内存一样,都是线程共享的内存区域。它是用来存放已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。方法区从内存回收的角度来讲,可以被叫做永久代但其实两者是不等价的,只是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。其实这样的方案更容易使方法区出现内存溢出,所以在jdk1.7的hotspot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。在jdk1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了元数据空间(MetaData Space)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory),而常量池被移动到了堆内存中
这个区域的垃圾回收目标主要是对常量池的回收和类型的卸载。尤其是类型的卸载的回收,要求很苛刻,但这部分的回收是很必要的。当方法区无法满足内存的分配需求时,会抛出OutOfMemoryError的异常。

运行时常量池

运行时常量池是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,用于存放编译期生成的字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放
(此处有待于确定)字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。它属于方法区的一部分,同样也会因为内存没有满足分配需求而抛出OOM的异常。

类被加载后各个类型变量的存放位置

下边我们通过一段代码来分析一下:

public class Demo {
    public static void main(String aa[]) {
        A a =new A();
        a.print();
    }
}

//建一个A类
class A {
    //成员变量 num字段名存放在方法区(元空间)中,值存在堆内存中
    private int num = 0;
    //字段名name存放在方法区(元空间)中,字符串值存放在方法区的字符串常量池中(或者堆内存中)
    private String name = "default name";
    //字段名temperature存放在方法区(元空间)中,数值存放在方法区的常量池中(或者堆内存中)
    private static double temperature = 36.5;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void print(){
          //临时变量名和值存放在虚拟机栈中
         int age = 10;
         //字符串名存在虚拟机中,值存在方法区的字符串常量池中(或者堆内存中)
         String msg = "this is A";
        System.out.println(msg + " ,and my age is " + age);
    }
}

上边代码中,当JVM加载了类Demo和A之后,这两个类的类信息被存放在方法区中(jdk1.8以后存放在元数据区域),比如符号引用(包括:1.类的全限定名,2.字段名和描述符,3.方法名和描述符)这些都存储在方法区的常量池中;而这个类在main方法被实例化之后,它的num字段对应的值存放在堆内存中,temperaturename字段因为是静态变量,所以存放在方法区中;name字符串的值存放在方法区的字符串常量池中。当调用print方法时,里边的age是一个基础类型的临时变量,它的名字和值都存放在了虚拟机栈中,而msg是一个字符串,它的值存放在方法区的字符串常量池(或者元数据空间)中;如果该方法中有一个类的实例,那么它的引用存在虚拟机栈中,而实际的实例对象数据存放在堆内存中。如果类中有一个类的成员变量,那么这个类的引用和实例对象都存在堆内存中。

内存溢出异常

在讲内存溢出之前,我们先来了解一下两个概念。

内存泄漏

内存泄漏(memory leak)是指程序在申请内存后,无法释放已经申请的内存空间,多次的内存泄漏就会导致内存被占满。

内存溢出

内存溢出(out of memory)是指程序在申请内存时,没有足够的内存空间供使用,导致OOM。

参考文献

《深入理解Java虚拟机》

----------------------------------------未完待续-----------------------------------------

posted @ 2020-08-13 19:59  爪哇洋  阅读(179)  评论(0编辑  收藏  举报