您当前的位置:首页 > 计算机 > 编程开发 > Java

详解Java虚拟机(JVM)的内存区域划分

时间:08-25来源:作者:点击数:

一、Java程序执行过程

由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程:

首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

二、Runtime Data Area (运行时数据区)

上课时老师通常会把java虚拟机的内存分为堆内存(heap)和栈内存(stack)以及方法区,这种分配方式实际上是比较粗糙的,这么划分其实是为了让程序员理解对象内存分配最密切的这两个区域,实际的内存划分远比这复杂。

JVM的内存区域主要分为以下五个部分:程序计数器、虚拟机栈、本地方法栈、方法区、堆,如下图所示:

1、程序计数器

程序计数器(Program Counter Register),也有称作为PC寄存器,指向当前线程所执行的字节码指令的地址

字节码解释器工作时,就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支,跳转,循环,异常处理等基础功能都需要依赖这个计数器来完成。由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是线程私有的内存空间。

在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

2、Java虚拟机

Java栈是Java方法执行的内存模型方法的执行的同时会创建一个栈帧,用于存储方法中的局部变量表、操作数栈、动态链接、返回地址等信息,栈是每个线程私有的内存空间。每个栈帧对应一个被调用的方法,每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。因此线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。

局部变量表:存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

操作数栈:一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

动态链接:如果在一个方法中调用了其他方法,栈帧中需要存放调用方法的地址。

方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。

举个例子:

private void methodOne() {
   int a = 2;
   float b = 4.5f;
   String name = "ss";
   Object object = new Object();
   float sum = a + b;
}

javap -c反汇编之后的方法如下:

public void methodOne();
    Code:
       0: iconst_2         //将整型2入栈(int a = 2;)
       1: istore_1         //将栈顶元素赋值给方法中第一个变量
       2: ldc           #2 //将4.5入栈(float b = 4.5f;)              // float 4.5f
       4: fstore_2         //将栈顶元素赋值给方法中第二个变量
       5: ldc           #3 //将ss入栈 (String name = "ss";)                // String ss
       7: astore_3         //将栈顶元素赋值给方法中第三个变量
       8: new           #4 //申请块堆内存,将申请到的堆内存地址压入栈(Object object = new Object();) // class java/lang/Object
      11: dup              //复制栈顶元素,并将赋值的栈顶元素压入栈
      12: invokespecial #1 //初始化对象(注意,此时当前栈顶的数据会出栈)// Method java/lang/Object."<init>":()V
      15: astore        4  //当前栈顶元素赋值给方法中,第四个变量
      17: iload_1          //将第一个变量值入栈(float sum = a + b;)
      18: i2f              //将栈顶int类型值转换为float类型值。
      19: fload_2          //将第二个变量值入栈
      20: fadd             //将栈顶两个数值相加结果压入栈
      21: fstore        5  //将栈顶的元素出栈赋值给方法中第五个变量
      23: return           //void函数返回
}

当方法嵌套调用时,虚拟机栈的工作如下

private void methodOne() {
        methodTwo();
    }
    private void methodTwo() {
    }
}

当方法递归调用时

private void methodOne() {
        methodOne();
}

当我们无限递归时,就会产生我们的StackOverflow异常,栈溢出。

方法嵌套调用的次数由栈的大小决定,栈越大,方法调用次数就越多

对于同一个方法来说,它的参数越多,局部变量越多,它的栈帧就越大,调用次数就越少

3、本地方法栈

本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的,是由C实现的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

4、堆

Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。java堆是Java虚拟机所管理的内存中最大的一块。java堆是被所有线程所共享的一块内存区域,虚拟机启动时创建,几乎所有对象的实例都存储在堆中,所有的对象和数组都要在堆上分配内存。

java堆是垃圾收集器(GC)管理的主要区域,java堆中可以划分出多线程私有的缓冲区,但是无论怎么划分对象的实例仍然存储在堆中。java堆允许处于不连续的物理内存空间中,只要逻辑连续即可。堆中如果没有空间完成实例分配无法扩展时将会抛出OutOfMemoryError异常。

JVM 的堆分为两个区域,新生代、老年代。同时新生代又分为eden区(对象出生地),s0(From Survivor)、s1(To Survivor)三个区域,通常Eden和S区域是大小是8:1的关系。在有些资料中介绍堆中还有一块区域叫永久代,在JDK的HotSpot虚拟机中,可以认为方法区就是永久代,JVM规范把它描述为堆的一个逻辑部分,但是他却有一个别名叫Non-Heap目的就是为了与堆区分开来。

新生代主要存放的是那些很快就会被GC回收掉的对象,或者不是特别大的对象。新生代中的GC操作是MinorGC,Eden区的对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。

MinorGC采用的是复制算法。复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。详情

在GC最开始的时候,新生代对象只会存在于Eden区和s0区,s1是空的。紧接着进行MinorGC,Eden区中所有存活的对象都会被复制到“s1”,而在“s0”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到老年代中,没有达到阈值的对象会被复制到“s1”区域。经过这次GC后,Eden区和s0区已经被清空。这个时候,“s0”和“s1”会交换他们的角色,也就是新的“s1”就是上次GC前的“s0”,新的“s0”就是上次GC前的“s1”。不管怎样,都会保证名为s1的区域是空的。Minor GC会一直重复这样的过程,直到“s1”区被填满,“s1”区被填满之后,会将所有对象移动到年老代中。

老年代则是存放那些在程序中经历了好几次MinorGC仍然还活着的或者特别大的对象老年代发生的是GC是MajorGC或者FullGC

在JDK1.7之后,运行时常量池从方法区中移动到堆中,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,

5、方法区

方法区与堆一样所有线程所共享的内存区域,它用于存储已被虚拟机加载的类信息(类名、父类名、修饰符、直接接口类)、静态变量、及时编译器编译后的代码等数据。

方法区也叫永久区,是一块独立于java堆的内存空间,有时也叫non-heap,java虚拟机对方法区的限制非常宽松,除了和堆一样不需要连续的内存和可扩展,还可以不实现垃圾收集,相对而言,垃圾收集机制在这个区域出现的较少,当方法区无法分配足够内存时,将会抛出OutOfMemoryError异常。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门