JVM系列(五)[Runtime Data Area运行时数据区,栈帧的组成,JVM Stack Instructions 指令集]

Runtime Data Area运行时数据区

概念

见名知意,就是JVM运行时的数据区,我们需要的数据一般都在这里.

前面已经知道,一个class文件,经过load-link-initialize加载到JVM.
然后,就是经过运行时引擎(run engine)进入运行时数据区(runtime data area).

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

结构

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

  • 每个线程私有的区域: PC,VMStack,NativeMethodStack
  • 所有线程共享的区域:Heap,MethodArea,DirectMemory
    (MethodArea的实现:当JDK<1.8时 由PermSpace实现, 当JDK>=1.8时由MetaSpace实现)

每个区域详情:

  • Program Counter(PC)程序计数器: 存放指令位置,虚拟机不断的从PC中取出下一条指令的位置,找到指令去执行
  • heap堆:对象存放的地方,所有线程共享. 重点,后面GC详细学
  • JVM stacks: JVM管理的栈,每个线程有自己私有的JVM stack,其中的每个方法对应一个栈帧(frame). 重点
  • native method stacks本地方法栈:线程私有,通过JNI等调用C和C++方法时用的栈

栈包括JVM stacks和native method stacks
平时一般说到栈,指的都是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)

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

在这里插入图片描述

栈帧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

  1. dynamic linking,
    指向运行时常量池
    比如a() -> b(),方法a调用了方法b,class文件解析时把方法放在运行时常量池中了,这个就是用来找到b
    jvms2.6.3
    https://blog.csdn.net/qq_41813060/article/details/88379473
  2. return address
    a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方

栈的指令集

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

常见的几个指令:

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

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

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

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

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

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

设计一台机器的指令集,有两种做法:

  • 基于栈的指令集(JVMStack选择的方式)
  • 基于寄存器的指令集(汇编语言)(HotSpot的局部变量表类似于寄存器)
    最终在硬件层面都是基于寄存器的指令集
    单条指令也不一定是原子性的

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

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

Java代码:

    public static void main(String[] args) {
        Hello_02 h = new Hello_02();
        h.m1();
    }

    public void m1() {
        int i = 200;
    }

main方法的字节码指令:

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

m1方法的字节码指令:

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

带返回值的方法的指令集

Java代码:

    public static void main(String[] args) {
        Hello_02 h = new Hello_02();
        h.m1();
        int m1 = h.m1();
    }

    public int m1() {
        int i = 200;
        return i;
    }

main方法的指令集:

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

m1方法的指令集:

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

递归求阶乘方法的指令集

Java代码:

    public static void main(String[] args) {
        Hello_04 h = new Hello_04();
        int i = h.m(3);
    }

    public int m(int n) {
        if (n == 1) {
            return 1;
        }
        return n * m(n - 1);
    }

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

 0 iload_1 // 压栈,形参n
 1 iconst_1 // 压栈,常量int 1
 2 if_icmpne 7 (+5) // 出栈两个(此时栈空),比较他们的值是否相等,如果是相等就转到编号5那行继续执行,否则转到编号为7那行
 5 iconst_1 // 压栈,常量 int 1
 6 ireturn // 返回结果
 7 iload_1 // 压栈,形参n
 8 aload_0 // 压栈,this
 9 iload_1 //压栈,形参n
10 iconst_1 // 压栈,常量 int i
11 isub // 出栈两个,相减,结果压栈
12 invokevirtual #4 <com/mashibing/jvm/c4_RuntimeDataAreaAndInstructionSet/Hello_04.m> // 出栈两个,第一个是方法参数,第二个是this
15 imul // 出栈两个,相乘,结果压栈
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表达式,其实就是一个语法糖,匿名内部类的简化写法,但是具体细节应该有很多不同,回头再学习.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值