java虚拟机内存模型

source: http://coding-geek.com/jvm-memory-model/

JVM memory model

这篇文章主要介绍在JVM规范中描述的运行时数据区(Runtime Data Areas)。这些区域设计用来存储被JVM自身或者在JVM上运行的程序所是用的数据。

我们先总览JVM,然后介绍下字节码,最后介绍 不同的数据区域。

总览

JVM作为操作系统的抽象,保证同样的代码在不同的硬件或操作系统上的行为一致。

比如:

  • 对于基本类型 int, 无论在 16位/32位/64位 操作系统上,都是一个32位有符号整数。范围从-2^31 到 2^31-1

  • 无论操作系统或者硬件是大字节序还是小字节序,保证JVM存储和使用的内存中的数据都是大字节序(先读高位字节)

不同的JVM实现可能会有些区别,但大体上是相同的。

这里写图片描述
上图是一个JVM的总览

  • JVM解释编译器生成的字节码。虽然JVM是Java虚拟机的缩写,但是只要是能够编译为字节码的语言,都可以基于JVM运行,比如 scala、groovy

  • 为了避免频繁的磁盘I/O,字节码会被classloader加载并缓存到运行时数据区中的一个区域,知道加载它的classloader被销毁或者JVM停止运行。

  • 加载的字节码通过执行引擎(execution engine)进行解释和执行

  • 执行引擎需要存储程序上下文,比如程序执行到哪一行,或者数据计算的中间结果

  • 执行引擎也负责处理与底层操作系统的交互

**很多JVM都实现了即时编译功能(JIT=just in time)。JIT就是把经常执行的代码(热点代码)编译成本地代码(Native Code)。存放JIT编译生成代码的区域称为

代码缓存区(Code Cach)。即时编译技术(JIT)级大的提高了JVM的性能**

基于栈(stack)的架构

JVM使用基于栈的架构。虽然栈对于开发者是透明的,但是栈对于生成的字节码和JVM都有很重要的作用或者说影响。

我们开发的程序,会转换位低级别的操作,存于字节码中。在JVM中通过操作数(operand)映射到操作指令。按照JVM规范,操作指令需要的参数是从操作数栈获得的(the operand stack)。

这里写图片描述

举个两个数相加的例子。这这个操作称为 iadd 。下面是在字节码中 3+4 的过程

  • 首先把3和4压入操作数栈

  • 调用 iadd 指令

  • iadd 指令会从操作数栈顶弹出2个数

  • 3+4的结果压入操作数栈,供后面使用

这种方式被称为基于栈的架构。还有其他的方式可以处理低级别操作,比如基于寄存器的架构(register based architecture)。

字节码

java字节码是java源码转换为一系列低级别操作的结果。每个操作由一个字节长度的操作码(opcode or operation code)和零或多个字节长度的参数(但是大多数操作使用的参数都是通过操作数栈获取的)组成。一个字节可以表示256个数,从0x00到0xff,目前到java8,共使用了204个。

下面列出不同种类的字节码操作码以及其范围和简单的描述

  • Constants: 将常量池的值或者已知的值压入操作数栈。 0x00 - 0x14

  • Loads: 将局部变量值压入操作数栈。 0x15 - 0x35

  • Stores: 从操作数栈加载值赋给局部变量 0x36 - 0x56

  • Stack: 处理操作数栈 0x57 - 0x5f

  • Math: 从操作数栈获取值进行基本的数学计算 0x60 - 0x84

  • Conversions: 进行类型之间的转换 0x85 - 0x 93

  • Comaprisons: 两个值的比较操作 0x94 - 0xa6

  • Controls: 执行goto、return、循环等等控制操作 0xa7 - 0xb1

  • References: 执行分配对象或数组,获取或检查 对象、方法、静态方法的引用。也可以调用静态方法。 0xb2 - oxc3

  • Extended: Extended: operations from the others categories that were added after. From value 0xc4 to 0xc9

  • (这句说不好什么意思。。。)

  • Reserved: JVM实现内部是用的槽子0xca,oxfe,oxff

这204个操作都很简单,举几个例子

  • ifeq(0x99) 判断两个值是否相等

  • iadd(0x60) 把两个数相加

  • i2l (0x85) 把一个int 转换位 long

  • arraylength (0xbe) 返回数组长度

  • pop (0x57) 从操作数栈顶弹出一个值

我们需要编译器来创建字节码文件,标准的java编译器就是jdk中的 javac。


public class Test {

  public static void main(String[] args) {
    int a =1;
    int b = 15;
    int result = add(a,b);
  }

  public static int add(int a, int b){
    int result = a + b;
    return result;
  }
}

通过“javac Test.java” 可以得到 “Test.class”的字节码文件。字节码文件是2进制的,我们可以通过javap,把二进制的字节码文件转换成文本形式

java -verbose Test.class


Classfile /C:/TMP/Test.class
  Last modified 1 avr. 2015; size 367 bytes
  MD5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426
  Compiled from "Test.java"
public class com.codinggeek.jvm.Test
  SourceFile: "Test.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Methodref          #3.#16         //  com/codinggeek/jvm/Test.add:(II)I
   #3 = Class              #17            //  com/codinggeek/jvm/Test
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               add
  #12 = Utf8               (II)I
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #5:#6          //  "<init>":()V
  #16 = NameAndType        #11:#12        //  add:(II)I
  #17 = Utf8               com/codinggeek/jvm/Test
  #18 = Utf8               java/lang/Object
{
  public com.codinggeek.jvm.Test();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: bipush        15
         4: istore_2
         5: iload_1
         6: iload_2
         7: invokestatic  #2                  // Method add:(II)I
        10: istore_3
        11: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 5
        line 9: 11

  public static int add(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 12: 0
        line 13: 4
}

可以看出字节码不只是java代码的简单翻译,它包括:

  • 类的常量池(cosntant pool)描述。常量池是用于存储类元数据的JVM数据区域,比如类内部的方法名,参数列表,等等。当JVM加载一个类的时候,这些元数据就会加载到常量池

  • 通过行号表和或局部变量表来提供函数和天猫的变量在字节码中的具体位置信息

  • java代码的翻译(包括隐藏的父类构造)

  • 提供更具体的对于操作数栈的操作和更完整的传递和获取参数的方式

下面是一个简单的字节码文件存储信息的描述


ClassFile {
  u4 magic;
  u2 minor_version;
  u2 major_version;
  u2 constant_pool_count;
  cp_info constant_pool[constant_pool_count-1];
  u2 access_flags;
  u2 this_class;
  u2 super_class;
  u2 interfaces_count;
  u2 interfaces[interfaces_count];
  u2 fields_count;
  field_info fields[fields_count];
  u2 methods_count;
  method_info methods[methods_count];
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

运行时数据区

运行时数据区是存储数据的内存区域设计。这些数据供开发者或JVM内部使用。

这里写图片描述

堆(Heap)

堆 在JVM启动的时候创建,由所有的JVM线程所共享。所有的类实例、数组都分配到堆(由new所创建的)。

堆必须由一个垃圾收集器来管理,垃圾收集器负责释放被开发者所创建,并且不会再被使用到的对象。

至于垃圾收集的策略由JVM实现决定(比如HotSpot提供了多种算法).

堆内存有一个最大值限制,如果超过这个值 JVM会抛出一个 OutOfMemroy异常

方法区(Method area)

方法区也是被JVM的所有线程所共享。同样的随JVM启动被创建。方法区存储的数据由classloader从字节码中加载,这些数据会在应用运行过程中一致存在,除非加载它们的classloader被销毁或者JVM停止。

方法区存储如下数据:

  • 类信息(属性名、方法名、父类名、借口名、版本、等等)

  • 方法和构造的字节码

  • 加载每个类时创建的运行时常量池

JVM规范并不强迫在堆中实现方法区。在java7以前,HotSpot 使用一个称为永久带(PermGen)的区域实现方法区。永久带与堆相邻(和堆一样进行内存管理),默认位64MB

从java8开始,HptSpot使用分离的本地内存实现方法区,起名元数据区(Metaspace)。元数据区最大可用空间即整个系统的可用内存。

如果方法去申请不到可用内存,JVM也会抛出OutOfMemoryError.

运行时常量池(Runtime constant pool)

运行时常量池是方法区的一部分。因为运行吃常量池对于元数据的重要性,java规范中在方法区之外单独对其进行了描述。运行时常量池会随着加载的类和接口而增长。

常量池有点想传统语言中的语法表。换句话说,当调用一个类、方法或属性时,JVM通过运行时常量池来寻找这些数据在内存中的真实地址。运行时常量池也包含字符串字面值或基本类型的常量


    Stirng myString="This is a string litteral"

    static final int MY_CONSTANT = 2 ;

pc(程序计数器)寄存器(每个线程) The pc Register (Per Thread)

每个线程有自己的pc(程序计数器)寄存器,与线程创建是一同创建。每个线程在一个时间点上只能执行一个方法,称为该线程的当前方法(current method)。pc寄存器包含JVM当前在执行指令(在方法区)的地址。

如果当前执行的方法是本地方法(native),pc寄存器的值是undefined

虚拟机栈(每个线程) java virtual machine stacks (per thread)

虚拟机栈存储多个帧,因此在描述栈前,我们先来看下帧

帧(frames)

帧是一个数据结构,帧包含表示线程正在执行的当前方法状态的多个数据:

  • 操作数栈(Operand Stack): 之前已经提到过,字节码指令使用操作数栈来传递参数

  • 局部变量数组(Local variable array): 这个数组包含当前执行方法的一个作用域内的所有局部变量。这个数组可以包含基本类型、引用或者返回地址。局部变量数组的大小在编译时就已经确定。jvm在方法调用时使用局部变量传递参数,被调方法的局部变量数组通过调用方法的操作数栈创建。

  • 运行时常量池引用: 引用当前类当前被执行方法的常量池。JVM使用常量池引用传递信号给真正的内存引用。

栈(stack)

每个JVM线程都有一个私有的JVM栈,与线程同时创建。java虚拟机栈存储帧。每次调用一个方法时,都会创建一个帧,并且压入虚拟机栈。当这个方法执行完成时,这个帧也会销毁(无论方法是正常执行完成,还是抛出异常)

在一个线程执行的过程中只有一个帧是可用的。这个帧称为当前帧(current frame)。

对局部变量和操作数栈的操作通常和当前帧的引用一起。

我们再看一个加法的例子


public int add(int a, int b){
  return a + b;
}

public void functionA(){
// some code without function call
  int result = add(2,3); //call to function B
// some code without function call
}

这里写图片描述

在方法A内部,A帧是当前帧,位于虚拟机栈顶。在调用add方法开始时,创建一个新的帧B,并且压入虚拟机栈。帧B成为新的当前帧。

帧B的局部变量数组通过帧A的操作数栈中的数据填充。当add方法结束在,帧B被销毁,帧A重新成为当前帧。add方法的结果压入A帧的操作数栈,这样方法A可以通过帧A的操作数栈获取add 的结果

虚拟机栈可以动态扩展或收缩,但是虚拟机栈有个一最大值,限制递归调用深度,如果超出了最大值JVM会抛出StackOverflowError

本地方法栈(每个线程) native method stack(per thread)

本地方法栈是给分java语言写的,或者通过java本地接口(JNI(java native interface))调用的本地代码是用的。因为是”本地的(native)”,所以本地方法栈的行为依赖于底层操作系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值