JVM 运行时数据区域

本贴最后更新于 2588 天前,其中的信息可能已经水流花落

一、JVM 运行时数据区

1. 程序计数器(Program Counter Register)

Java 虚拟机规范规定当线程在执行一个 Java 方法时,程序计数器记录的是当前正在执行的字节码指令。当线程在执行的是本地方法时,该计数器的值是未定义的(undefined)。
该内存区域是唯一一个在 Java 虚拟机规范中么有规定任何 OOM(内存溢出:OutOfMemoryError)情况的区域。

对于 Java 虚拟机规范规定的“当前正在执行的字节码指令”,可以用指针实现,具体有两种方式,一种是内存地址(bcp,bytecode pointer),一种是偏移量(bci,bytecode index)。

2. 方法区(Method Area)

Java 虚拟机规范

存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

JDK6 及其以前的 JDK 版本

方法区的实现为永久代(Permanent Generation)。

存储:
类的信息,每个类都有一个运行时常量池(Run-time Constant Pool);
通过字符串字面量创建的字符串对象、Java 堆里的字符串对象调用了 intern()方法后拷贝到方法区的字符串对象,这些字符串对象就组成了字符串常量池(Interned String Pool)。

JDK7

方法区的实现还是永久代,但已经开始出现一些变化。

把 StringTable 引用的字符串对象移到 Java 堆中;
把静态变量从永久代(instanceKlass 末尾)移动到了 java.lang.Class 对象的末尾;
把 SymbolTable 指向的对象从 PermGen 移动到了 native memory。

JDK8

移除永久代,使用元空间。元空间并不在虚拟机内存区域中,而是使用本地内存。

Native Memory

Native Memory 是相对于 GC Heap 的一个概念,不能处于 GC Heap 的区域就算 Native Memory,JDK7 以前的 Java 堆和永久带都属于 GC Heap,JDK7 及其以后的 Java 堆和元空间也属于 GC Heap)

JIT 编译器编译后的代码存储在 Native MemoryCode Cache 区域中(虽然 Java 虚拟机规范规定 JIT 编译后的代码也应该属于方法区的一部分)。

字符串表(String Table),位于 Native Memory 只有一张,它里面存储的是字符串对象的引用,实际的字符串对象在 JDK6 及其以前版本中是存储在永久代中,JDK7 及其以后的版本是存储在 Java 堆中。

除了 Float、Double 外,其他包装类都有自己的常量池,Java 虚拟机启动时就默认缓存了范围在[-128, 127]的对象,不在这个范围内的对象将永远不会被缓存到相应的常量池中,这些常量池是固定的,不像字符串常量池是可以添加字符串对象的。

符号表(Symbol Table)

3. Java 堆(Java Heap)

Java 虚拟机规范规定 Java 堆存储所有对象。

数组也是对象,但由 JVM 为我们动态创建。数组继承 Object 类,多了个 length 属性

在 JDK7 以前,Hotspot 虚拟机的永久代中存储了一些字符串对象。

对象内存布局

对象头(Header)
①Mark Word(32/64bit)
hashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳。非固定结构,以便在有限的空间尽可能存储多的信息。
② 指向 class 对象的指针(32/64bit)
③ 长度(length)(如果是数组对象的话)(32/64bit)

实例数据(Instance Data)
包括自身还有从父类继承过来的,包括父类的 private 字段,只不过子类继承过来以后不能访问这个字段。数据的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。

对齐填充(Padding)
不是必须,保证 8 字节对齐。

OOP/Klass 二分模型

OOP(Ordinary Object Pointer):存放实例信息;
Klass:存放元数据。

instanceKlass
instanceOopDesc 表示 Java 对象,arrayOopDesc 表示 Java 数组,klassOopDesc 表示 Java 类。

4. Java 虚拟机栈(Java Virtual Machine Stacks)

Java 虚拟机栈由栈帧(Stack Frame)组成。每个栈帧又包括操作数栈、局部变量表、方法返回地址和动态链接。操作数栈和局部变量表的大小在编译的时候就确定了,并且写入了方法表的 Code 属性中。

局部变量表(Local Variables)
我们在调用实例方法的时候,局部变量表的第 0 位是一个指向当前方法所属对象的引用,这个引用也就是我们说的 this 关键字,它是作为方法的隐藏参数传进来的,所以我们可以在实例方法里通过它访问当前对象;如果我们调用的是静态方法,第 0 位就不是 this 了,从第 0 位开始就直接开始放数据了。

局部变量表的单位是变量槽(Slot)。一个 Slot 可以存放一个 32 位以内的数据类型:boolean、byte、char、short、int、float、reference 和 returnAddresss。对于 64 位的数据类型(long 和 double),虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。

局部变量表中的 Slot 是可重用的。

操作数栈(Operand Stack)

Java 语言中,实例构造器只能在 new 表达式(或别的构造器)中被调用。

new 表达式作为一个整体保证了对象的创建初始化是打包在一起进行的,不能分开进行;但实例构造器只负责对象初始化的部分,“创建对象”的部分是由 new 表达式本身保证的。

new 一个对象过程:

  1. new 这个字节码指令做了两件事,一个是创建了一个对象,但还未初始化,也就是一个空对象,第二件事就是把指向这个对象的引用压入操作数栈;
  2. 执行 dup 指令,dup 指令将栈顶元素,也就是指向刚刚创建好的那个空对象的引用取出来,然后复制两份,一份保存到局部变量表里,因为构造器没有返回参数,待会构造器执行完了不会有引用返回回来。另一份作为隐藏参数传给构造器,所以在构造器里可以使用 this 关键字。
  3. 执行 invokespecial 指令,调用构造器,将这个对象进行初始化。
  4. 构造器执行完毕后方法返回,原先的方法执行 return 指令,把原来记录在局部变量表的引用返回。

动态链接(Current Class Constant Pool Reference)

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

方法返回地址(Return Value)

当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址,用于恢复上一个方法的操作数栈和局部变量表。

5. 本地方法栈(Native Method Stacks)

Java 虚拟机规范规定该区域是供本地方法执行用的。

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...