浅析Java的内存模型

Posted 特务依昂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析Java的内存模型相关的知识,希望对你有一定的参考价值。

一、前言

  之前过年在家,空闲时间比较多,想要了解一下JVM相关的内容,于是买了《深入理解Java虚拟机》这本书,过了一遍其中的基础知识。时隔多日,都忘得差不多了。为了重新捡起来,我决定复习一遍,并编写相关的系类博文加深印象,这是第一篇,来讲一讲JVM最基础的内容——JVM的内存分区。


二、正文

 2.1 Java内存分布

  Java的内存主要分为五个部分:

  1. 程序计数器;
  2. Java虚拟机栈;
  3. 本地方法栈;
  4. 堆内存;
  5. 方法区;

  具体结构如下图所示:


2.2 程序计数器

  首先看第一部分——程序计数器。上过计算机组成原理的可能都听过这个名词,也大概知道它的作用是指向下一条需要执行的计算机指令,而在JVM中,程序计数器的作用也是类似。

  程序计数器是JVM中较小的一块内存空间,它的作用是记录下一条需要执行的字节码指令的地址Java代码编译后的字节码在未经过实时编译器编译前,其执行方式是通过“字节码解释器”进行解释执行)。在JVM的概念模型里,就是通过改变这个计数器的值来辅助程序的执行。选择,循环,方法调用,异常处理等功能都需要通过程序计数器的辅助来完成。

  JVM的多线程和CPU的多线程处理类似,都是使用时间片轮转的方式来执行各个线程,即每个线程轮流得到一个时间片,执行完这个时间片后,等待下一次调度,而每次只有一个线程在执行。正因为如此,线程在调度后,为了能够恢复到这个线程上一次执行的位置,就需要程序计数器的帮助,让程序计数器来告知下一条需要执行的指令的地址,对于每一个线程都是如此。所以,JVM中的程序计数器是线程私有的,即每一个线程都有一个独立的程序计数器,辅助当前线程的运行,各个线程之间互不影响。

  如果当前JVM执行的是一个Java方法,则程序计数器的值就是正在执行的字节码指令的地址;若当前执行的是一个本地方法(如Objecthashcode方法),则程序计数器的值为空(Undefined),因为本地方法并不会被加载到JVM的内存空间中。


 2.3 Java虚拟机栈

  虚拟机栈和程序计数器一样,也是线程私有的。Java虚拟栈是Java方法执行时需要依赖的内存空间,这个栈中的元素被称为栈帧。栈帧可以理解为一个对象,它记录了一个方法的局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法对应一个栈帧,而每一个方法从执行到返回,就对应着它所属的栈帧从入栈到出栈的过程,即当一个Java方法被调用,JVM就会创建这个方法的栈帧,并将它加入的虚拟机栈中,执行结束后才会出栈。对于JVM来说,正在执行的是虚拟机栈栈顶的栈帧所对应的方法,这个栈帧被称为当前栈帧,栈帧对应的方法被称为当前方法

  下面我来简单介绍一下栈帧最重要的两个部分:

  • 局部变量表:局部变量表是用来存放方法的参数以及在方法中定义的局部变量,它的大小在编译期间就被确定,并写入到class文件中。若一个变量是基本数据类型,则在局部变量表中存放的就是这个变量的具体值;若是引用类型,存放的则是这个变量所引用对象的地址。局部变量表的最小单位是变量槽(Slot),对于doublelong类型的变量,在局部变量中需要占用两个单位,其他类型只需要占用一个单位。
  • 操作数栈:从名字可以看出,这是一个栈Stack,它存放的是:方法运行时需要用到的操作数。和局部变量表一样,它的最大深度也是在编译期间就被计算好,并被写入class文件中的。在一个方法刚准备运行前,它是空的,而在运行的过程中,不断的有操作数被入栈和出栈(比如假设方法中需要计算num = 1 + 2,则在这条语句执行时,12被入栈,然后被取出,计算出3存入栈中,再将3取出赋值给num),程序的计算都需要依靠这个栈进行。操作数栈可以存放任意类型的数据(基本数据类型和引用类型,引用类型存放的是地址),而doublelong类型将占用两个单位,其余类型占用一个单位。

  在虚拟机规范中规定,若线程的虚拟机栈的深度超过了JVM允许的最大深度,将抛出StackOverflowError(比如调用没有出口的递归方法);若JVM实现的虚拟机栈可以动态扩展,则如果在扩展时没有申请到足够的内存,将抛出OutOfMemoryError异常。


 2.4 本地方法栈

  本地方法栈的作用和虚拟机栈相同,不同的是,虚拟机栈是辅助Java方法的运行,而本地方法栈是辅助本地方法的运行。在虚拟机规范中,并没有规定实现本地方法栈使用的语言,数据结构以及使用方式,所以JVM可以自由地实现它。与虚拟机栈一样,这一块内存区域也可能抛出StackOverflowErrorOutOfMemoryError异常,同时也是线程私有


 2.5 堆内存

  这一块内存是JVM中最大,可能也是最重要的一块内存,而堆存在的唯一目的就是存放实例对象,几乎所有的对象都是在堆中分配内存。和之前所说的几块内存不同,堆内存是所有的线程共享的,它在虚拟机启动时被创建

  堆内存是垃圾回收的主要区域,从内存回收的角度看,现在的垃圾回收机制一般都是分代收集,所以堆内存可以被分为新生代和老年代。新生代用来存放生命周期短的对象,这一块区域将频繁进行垃圾回收;老年代用来存放生命周期长的对象(或者在新生代内存不足时代替新生代存放对象),所以垃圾回收的频率较低,甚至不进行垃圾回收。

  根据Java虚拟机规范规定,堆可以是不连续的内存空间,只需要逻辑上连续即可。在实现虚拟机时,可以将堆实现为固定大小,也可以实现为可扩展的,当前的虚拟机一般都是采用可扩展的方式(可以通过配置参数来改变堆的大小)。如果堆中的内存被占满,实例对象无法存入,且堆无法再进行扩展时,将抛出OutOfMemaryError异常。


 2.6 方法区

  和堆一样,方法区也是线程共享的,它的作用是存放类信息,常量,静态常量等内容。Java虚拟机规范对这一块区域的限制非常宽松,方法区可以选择实现为固定的或者可扩展的,也可以选择是否实现垃圾回收。方法区中的数据,很少会发生改变,所以虚拟机规范并不强制要求在此区域实现垃圾回收。这块区域进行垃圾回收的主要目的就是卸载已经不需要使用的类,但是类卸载的条件是非常苛刻的。需要注意的一点是,在Java8之前,方法区被称为永久代,使用的是JVM的内存空间,但是从Java8开始,方法区使用的是本地内存,被称为元空间。此时能够加载多少类,就取决于本地内存的大小了。

  在方法区中有一块内存区域,叫做运行时常量池,它的作用就是存储编译期字面量以及符号引用。在一个类中如果定义了常量,或使用了字面量(如直接使用字符串为变量赋值,String str = ”aaa“),则在编译之后,会被放入到class文件的常量池中(class文件有一个常量池部分),同时,类编译后的符号引用也会被放入常量池。当程序运行时,class中的内容被加载到方法区中,而其中常量池的内容会被放入运行时常量池里面,被所有的类共享。运行时常量池是一个动态的内存区域,可以在程序运行时,动态的为其中添加数据,比如String类的intern()方法。因为运行时常量池是方法区的一部分,所以它的大小受到方法区的限制,当无法在其中申请空间时,将抛出OutOfMemaryError异常。需要注意的是,从Java7开始,常量池从方法区移动到了堆空间。


 2.7 我们平常理解的Java内存模型

  我们在学习Java的过程中,经常会看到这样的内容:Java的内存分为两部分,即栈内存和堆内存,栈内存中存放基本数据类型以及对象引用,而堆内存中存放对象。

  以上是对Java内存模型的片面描述,只关注了程序员最重视的部分。在上面的描述中,栈内存指的就是本地方法栈中,局部变量表的部分;而堆内存就是Java中的堆了。


三、总结

  以上内容对Java的内存模型进行了一个简单的描述,讨论了每一个部分的基本作用,了解了上面的内容后,再遇到Java内存方面的问题,也能更好地找出错误的所在。


四、参考

  • 《深入理解Java虚拟机》

以上是关于浅析Java的内存模型的主要内容,如果未能解决你的问题,请参考以下文章

浅析虚拟机内存管理模型

Java内存模型与volatile关键字浅析

Java中堆内存与栈内存分配浅析

浅析JAVA中堆内存与栈内存的区别

架构师技能1:Java工程规范浅析领域模型VODTODOPO优秀命名

浅析C++内存布局