深入理解Java虚拟机之java内存区域

229 阅读8分钟

运行时数据区域

Java虚拟机管理的数据区域包括:堆、虚拟机栈、本地方法栈、方法区、程序计数器。其中堆、方法区是线程共享的内存区域,其他几个区域为线程隔离的内存区域。

Java堆

是Java虚拟机所管理的内存中最大的一块。被所有线程所共享,在虚拟机启动的时候创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。为什么说几乎呢,难道不是所有的对象实例都在这里分配内存吗?有两点会导致new出来的对象不在堆上:逃逸分析与TLAB(Thread Local Allocation Buffer),本篇暂不展开过多介绍,后续会整理出一篇来详细说明。

Java堆是垃圾收集器管理的主要内存区域。从内存回收的角度来看,由于现在收集器基本上采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,再细致一点的有:Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer),划分的目的是为了更好的回收内存与更快的分配内存。

方法区

方法区用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(HotSpot虚拟机上开发部署人员更愿意称为“永久代”,Permanent Generation),我们来具体看下方法区中存储的内容(参考:方法区):

类型信息

包括以下几点:

  • 类的完整名称(比如,java.long.String)

  • 类的直接父类的完整名称

  • 类的直接实现接口的有序列表(因为一个类直接实现的接口可能不止一个,因此放到一个有序表中)

  • 类的修饰符

类型的常量池 (即运行时常量池)

每一个Class文件中,都维护着一个常量池(这个保存在类文件里面,不要与方法区的运行时常量池搞混),里面存放着编译时期生成的各种字面值和符号引用;这个常量池的内容,在类加载的时候,被复制到方法区的运行时常量池 ;

字面值:就是像string, 基本数据类型,以及它们的包装类的值,以及final修饰的变量,简单说就是在编译期间,就可以确定下来的值;

符号引用:不同于我们常说的引用,它们是对类型,域和方法的引用,类似于面向过程语言使用的前期绑定,对方法调用产生的引用;

字段信息

  • 声明的顺序

  • 修饰符

  • 类型

  • 名字

方法信息

  • 声明的顺序

  • 修饰符

  • 返回值类型

  • 名字

  • 参数列表(有序保存)

  • 异常表(方法抛出的异常)

  • 方法字节码(native、abstract方法除外,)

  • 操作数栈和局部变量表大小

类变量(即static变量)

  • 非final类变量

  • 在java虚拟机使用一个类之前,它必须在方法区中为每个非final类变量分配空间。非final类变量存储在定义它的类中;

  • final类变量(不存储在这里)

  • 由于final的不可改变性,因此,final类变量的值在编译期间,就被确定了,因此被保存在类的常量池里面,然后在加载类的时候,复制进方法区的运行时常量池里面 ;final类变量存储在运行时常量池里面,每一个使用它的类保存着一个对其的引用;

对类加载器的引用

jvm必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。

jvm在动态链接的时候需要这个信息。当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的。这对jvm区分名字空间的方式是至关重要的。

对Class类的引用

jvm为每个加载的类都创建一个java.lang.Class的实例(存储在堆上)。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据(类的元数据)联系起来, 因此,类的元数据里面保存了一个Class对象的引用;

方法表

为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还可以添加一些其他的数据结构,如方法表。jvm对每个加载的非虚拟类的类型信息中都添加了一个方法表,方法表是一组对类实例方法的直接引用(包括从父类继承的方法。jvm可以通过方法表快速激活实例方法。(译者:这里的方法表与C++中的虚拟函数表一样,但java方法全都 是virtual的,自然也不用虚拟二字了。正像java宣称没有 指针了,其实java里全是指针。更安全只是加了更完备的检查机制,但这都是以牺牲效率为代价的,个人认为java的设计者 始终是把安全放在效率之上的,所有java才更适合于网络开发)

运行时常量池

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面常量和符号引用,这部分内容被类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个特征具有动态性,可以在运行期间将新的常量放入池中(典型的如String类的intern()方法)

虚拟机栈

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

我们常说的栈,就是虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表中存放了存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们之间的区别 是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令,分支、循环 、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制。

如果读完觉得有收获的话,欢迎点赞、关注、加公众号【Java在线】,查阅更多精彩历史!!!