java虚拟机运行时数据区分析

时间:2022-11-01 23:33:49

jvmmemorymodel

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

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

总览

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

比如:

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

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

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

java虚拟机运行时数据区分析

上图是一个jvm的总览

jvm解释编译器生成的字节码。虽然jvm是java虚拟机的缩写,但是只要是能够编译为字节码的语言,都可以基于jvm运行,比如 scala、groovy<喎�"/kf/ware/vc/" target="_blank" class="keylink">vcd4ncjxwps6qwcux3mpixrw3sbxetmxfzekvt6os19a92slru+gxu2nsyxnzbg9hzgvyvnpu2lkiu7q05rw91mvq0mqxyv2+3cf41tc1xnk7upbh+npyo6zwqrxavnpu2mv8tcrjbgfzc2xvywrlcrg7z/q72bvy1d9kvk3no9a51mvq0kgjpc9wpg0kpha+vnpu2lxe19a92slrzai5/da00nds/cfmkgv4zwn1dglvbiblbmdpbmupvfjq0l3iys26zda00na8l3a+dqo8cd7wtndq0v3h5tdo0qq05rsis8zq8snpz8loxkosscji57pm0plwtndqtb3exnk70ncjrlvy1d/k/b7dvmbl47xe1tc85l3hufs8l3a+dqo8cd7wtndq0v3h5tkyulru8lsmwo3t67xxsuoy2df3z7xns7xevbu7ptwvcd4ncjxwpioquty24epwtba8yrxp1shlvltksbhg0uu5pstckepjvd1qdxn0igluihrpbwupoanksvs+zcrhsng+rboj1rtq0lxetprc6yjiylxjtprc6ymx4nlrs8mxvrxytprc6yhoyxrpdmugq29kzsmho7tmt8vksvsx4nlryfqzybt6wuu1xmf40/kzxs6qpc9wpg0kpha+tprc67u6tobh+chdb2rlienhy2gpoao8tmqxseds67y8yvuosklukby2tpo1xmzhun/by0pwtbxe0nte3coqpc9wpg0kpggyiglkpq=="基于栈stack的架构">基于栈(stack)的架构

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

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

java虚拟机运行时数据区分析

举个两个数相加的例子。这这个操作称为 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

coma*s: 两个值的比较操作 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。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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代码的翻译(包括隐藏的父类构造)

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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内部使用。

java虚拟机运行时数据区分析

堆(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通过运行时常量池来寻找这些数据在内存中的真实地址。运行时常量池也包含字符串字面值或基本类型的常量

?
1
2
3
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">虚拟机栈(每个线程) java virtual machine stacks (per thread)
虚拟机栈存储多个帧,因此在描述栈前,我们先来看下帧

帧(frames)

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

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

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

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

栈(stack)

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

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

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

我们再看一个加法的例子

?
1
2
3
4
5
6
7
8
9
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
}

java虚拟机运行时数据区分析

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

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

总结

以上就是本文关于java虚拟机运行时数据区分析的全部内容,希望对大家有所帮助。如有不足之处,欢迎留言指出。

原文链接:https://www.2cto.com/kf/201608/543147.html