Runtime Data Area运行时数据区

概念

见名知意,就是JVM运行时的数据区,我们需要的数据一般都在这里.
前面已经知道,一个class文件,经过load-link-initialize加载到JVM.
然后,就是经过运行时引擎(run engine)进入运行时数据区(runtime data area).

最权威的参考文档:Java Virtual Machine Specification

结构

总体上分为线程私有和线程共享的两类:

  1. 每个线程私有的区域: PC,VMStack,NativeMethodStack
  2. 所有线程共享的区域:Heap,MethodArea,DirectMemory

    1. (MethodArea的实现:当JDK<1.8 PermSpace实现, JDK>=1.8时由MetaSpace实现)<br /> 每个区域详情:<br />[点击查看【processon】](https://www.processon.com/embed/5ee0529f07912929cb3704d7)<br />Program Counter(PC)程序计数器: 存放指令位置,虚拟机不断的从PC中取出下一条指令的位置,找到指令去执行

heap堆:对象存放的地方,所有线程共享. 重点,后面GC详细学

JVM stacks: JVM管理的栈,每个线程有自己私有的JVM stack,其中的每个方法对应一个栈帧(frame). 重点
native method stacks本地方法栈:线程私有,通过JNI等调用C和C++方法时用的栈

  1. 栈包括JVM stacksnative method stacks
  2. 平时一般说到栈,指的都是JVM stacks

Direct Memory直接内存: 从JVM内可以访问OS管理的内存(内核空间),可以提高IO效率(零拷贝),JDK1.4新增的.
比如一个网络请求传过来一个数据到OS内核空间中,1.4以前使用这个数据时需要把内核空间中的数据拷贝到JVM内存中;
1.4之后,通过NIO可以使用直接内存,直接访问内核空间的该数据,不需拷贝.
method area方法区:class结构存放的地方,所有线程共享
方法区中还有块 run-time constant pool 常量池,存放class文件的常量池(constant_pool)

  1. 方法区是一个逻辑上的概念
  2. JDK1.8之前,方法区由永久区(Permament Space)实现,字符串常量也在永久区,FGC不会清理;JVM启动时指定永久区空间大小,不能改变
  3. JDK1.8开始,方法区由Meta Space实现,字符串常量位于堆,会触发FGC 被清理.可以指定大小,如果不指定的话,最大就是物理内存空间.

image.png

栈帧Frame

A Frame is used to store data and partial results,as well as to perform dynamic linking,return values for methods,and dispatch exceptions.
主要包括:

  1. 局部变量表(local variable table)

    注意,形参也在局部变量表;如果是成员方法,this也在局部变量表
    变量表从下标0开始,排序优先级为:this > 形参 > 方法中的变量
    形参和方法中的变量按出现的顺序排列

  2. 操作数栈(Operand Stacks)

    里面存的是一个个操作数,比如_load指令会把一个值压栈,_store指令会把栈顶的值弹出.
    (对于long的处理(store and load),多数虚拟机的实现都是原子的
    jls 17.7,没必要加volatile)

  3. dynamic linking,

    指向运行时常量池
    比如a() -> b(),方法a调用了方法b,class文件解析时把方法放在运行时常量池中了,这个就是用来找到b
    jvms2.6.3
    https://blog.csdn.net/qq_41813060/article/details/88379473

  4. return address

    1. a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

栈的指令集

这个指令集是JVM很底层的东西了,这个看明白了,任何其他语言也就很容易理解了,大家都差不多.
马老师说:别人看山是山,你看山是看到的都是各种分子,所以任何的山都是一样的.哈哈哈
第一个境界是 看山是山;
第二个境界是 看山不是山;
第三个境界是 看山还是山.
JVM学完这节,勉强到第二境界了,奔着第三境界前进!
具体怎么奔?参看jvms!

常见的几个指令:
: 静态语句块
: 构造方法
_store : 出栈,并赋值给一个局部变量
_load : 压栈
invoke_XXX : 调用方法,这个指令很复杂,下面单独拎出来说
dup : 把栈顶的东西复制一份,然后把其压栈,一般是调用实例方法前会dup

看一道面试题,认识指令集,更好的理解栈帧

实际中肯定没人写这种代码了,下面这种写法就是为了理解栈帧

  1. public class TestIPulsPlus {
  2. public static void main(String[] args) {
  3. int i = 8;
  4. i = i++; // 结果是8
  5. //i = ++i; // 结果是9
  6. System.out.println(i);
  7. }
  8. }

具体原因我们来通过JClassLib查看字节码,一探究竟:
无论i = i++;还是i = ++i;,局部变量表是一样的:
查看的是main方法,静态方法,所以0位置是形参String[] args
1位置是方法中的第一个变量 i.

当i = i++; 时,用JClassLib查看字节码:

  1. 0 bipush 8 // 把8压栈,用int扩展这个立即数
  2. 2 istore_1 // 出栈,把栈顶的数弹出,并赋给局部变量表下标为1的变量,就是变量i; 到此 int i = 8; 这句完事
  3. 3 iload_1 // 把i压栈,从(下标1)本地变量表中拿值放到栈中(Operand Stack),此时i=8,所以栈中是8
  4. 4 iinc 1 by 1 // 局部变量表1的位置自增1,(第一个1表示局部变量表的下标),此时局部变量表的i值为9
  5. 7 istore_1 // 出栈,把栈顶的值赋给局部变量表下标为1的变量,此时栈中仍是8,所以最后i=8. 到此,i=i++; 这句完事
  6. // 下main的就是System.out.println(i);了
  7. 8 getstatic #2 <java/lang/System.out>
  8. 11 iload_1
  9. 12 invokevirtual #3 <java/io/PrintStream.println>
  10. 15 return

当i = ++i; 时,用JClassLib查看字节码:

  1. 0 bipush 8 // 8压栈
  2. 2 istore_1 // 8出栈, int i=8;完事
  3. 3 iinc 1 by 1 // i自增1,此时i=9
  4. 6 iload_1 // 把i压栈,此时i已经时9了,栈中也是9
  5. 7 istore_1 // 9出栈,赋值给i,所以此时i=9, i=++i;完事
  6. // 下main的就是System.out.println(i);了
  7. 8 getstatic #2 <java/lang/System.out>
  8. 11 iload_1
  9. 12 invokevirtual #3 <java/io/PrintStream.println>
  10. 15 return
  1. 设计一台机器的指令集,有两种做法:
  2. 基于栈的指令集(JVMStack选择的方式)
  3. 基于寄存器的指令集(汇编语言)(HotSpot的局部变量表类似于寄存器)
  4. 最终在硬件层面都是基于寄存器的指令集
  5. 单条指令也不一定是原子性的

总结:
i++ 是先把i的值压栈,然后把局部变量i自增,代码层面可以理解为先使用i的当前值,用完了让i自增;
++i 是先把局部变量i自增,然后把i的值压栈,代码层面可以理解为先给i+1,然后使用i自增后的值.

创建对象&调用其实例方法的指令集

Java代码:

  1. public static void main(String[] args) {
  2. Hello_02 h = new Hello_02();
  3. h.m1();
  4. }
  5. public void m1() {
  6. int i = 200;
  7. }

main方法的字节码指令:

  1. 0 new #2 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02> // 在堆中创建对象,并把对象的地址压栈,此时该对象各属性为默认值
  2. 3 dup // 把栈顶的元素复制一份,压栈,此时栈顶两份该对象的地址
  3. 4 invokespecial #3 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.<init>> // 出栈,拿到对象后调用其构造方法
  4. 7 astore_1 // 出栈,并赋值给h
  5. 8 aload_1 // 把h压栈
  6. 9 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1> // 出栈,调用其实例方法
  7. 12 return

m1方法的字节码指令:

  1. 0 sipush 200 // 把200压栈,开头的s代表short,200已经超过byte的范围了,最后会扩展为int
  2. 3 istore_1 // 出栈,赋值给i
  3. 4 return

带返回值的方法的指令集

Java代码:

  1. public static void main(String[] args) {
  2. Hello_02 h = new Hello_02();
  3. h.m1();
  4. int m1 = h.m1();
  5. }
  6. public int m1() {
  7. int i = 200;
  8. return i;
  9. }

main方法的指令集:

  1. 0 new #2 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02>
  2. 3 dup
  3. 4 invokespecial #3 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.<init>>
  4. 7 astore_1
  5. 8 aload_1
  6. 9 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1>
  7. // 上面都一样,不谈了
  8. 12 pop // 出栈,因为代码中第一次调用m1()方法是没有接收它的返回值,所以只是出栈
  9. 13 aload_1
  10. 14 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_02.m1>
  11. 17 istore_2 // 出栈,并赋值给m1变量
  12. 18 return

m1方法的指令集:

  1. 0 sipush 200
  2. 3 istore_1
  3. // 上面都一样,不谈了
  4. 4 iload_1 // 这里因为代码有返回值,所以就把返回值压栈了,为了下一步返回做准备
  5. 5 ireturn // 自己的栈出栈,拿到返回之后往main方法的栈帧中压栈,由main方法去弹栈拿到返回值

递归求阶乘方法的指令集

Java代码:

  1. public static void main(String[] args) {
  2. Hello_04 h = new Hello_04();
  3. int i = h.m(3);
  4. }
  5. public int m(int n) {
  6. if (n == 1) {
  7. return 1;
  8. }
  9. return n * m(n - 1);
  10. }

main方法的指令集就不谈了,就看看m方法的指令集:

  1. 0 iload_1 // 压栈,形参n
  2. 1 iconst_1 // 压栈,常量int 1
  3. 2 if_icmpne 7 (+5) // 出栈两个(此时栈空),比较他们的值是否相等,如果是相等就转到编号5那行继续执行,否则转到编号为7那行
  4. 5 iconst_1 // 压栈,常量 int 1
  5. 6 ireturn // 返回结果
  6. 7 iload_1 // 压栈,形参n
  7. 8 aload_0 // 压栈,this
  8. 9 iload_1 //压栈,形参n
  9. 10 iconst_1 // 压栈,常量 int i
  10. 11 isub // 出栈两个,相减,结果压栈
  11. 12 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_04.m> // 出栈两个,第一个是方法参数,第二个是this
  12. 15 imul // 出栈两个,相乘,结果压栈
  13. 16 ireturn // 返回结果

invoke_XXX

  1. InvokeStatic 调静态方法
  2. InvokeVirtual 调一般的实例方法,支持多态
  3. InvokeInterface 通过接口调用的方法
  4. InvokeSpecial

    可以直接定位,不需要多态的方法
    具体有:private 方法 , 构造方法
    final方法不是invokeSpecial

  5. InvokeDynamic

    JVM最复杂的指令,>=JDK1.7
    lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
    (lambda表达式,其实就是一个语法糖,匿名内部类的简化写法,但是具体细节应该有很多不同,回头再学习.)