前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >1、引言

1、引言

作者头像
文彬
发布2022-06-06 09:29:05
3670
发布2022-06-06 09:29:05
举报
文章被收录于专栏:醒者呆

本文重点介绍JVM运行时数据区的整体概况,其中堆和方法区等比较复杂的会在GC的部分学习。另外本文还学习了JVM的指令集,涉及到的常用的一些指令,通过查看JVM规范手册,还确定每一个是如何使用,并与运行时数据区进行对应。 笔记系列。 关键字:运行时数据区,自增的字节码指令执行,局部变量表,栈帧,this,iadd,invoke指令

1、引言

一个java类的完整生命周期如下:

class文件 -> (loading,linking,initailizing)-> JVM -> run engine -> 运行时数据区 -> GC

2、运行时数据区

  1. PC,program counter,程序计数器。存放下一条指令位置,空间很小。虚拟机运行就是一个循环,不断去取PC中的位置,找到对应位置的指令然后执行,PC++,直到结束。
    1. 每个JVM线程都有它自己的PC寄存器。
    2. 任何时候,每条JVM线程都在执行一个独立方法的代码,被称作那条线程的当前方法。
    3. 如果方法不是native的,pc寄存器包含的JVM指令的地址将被执行。
  2. Heap,堆内存。GC垃圾收集的时候会重点学习。
    1. JVM拥有一个Heap堆,它是被所有的JVM线程所共享的。
    2. 堆是在JVM运行时,所有对象实例和被分配的数组所占内存的空间。
  3. stack
    1. JVM stacks
      1. 每个JVM线程都有一个私有的JVM栈,在线程被创建的时候,栈也随即被创建。
      2. 一个JVM栈里面保存着很多的栈帧。
      3. stack frames,每个栈帧对应每一个方法。
    2. Native method stacks,本地方法栈,通过JNI去调用,保存的是C/C++依据JVM规范编写的方法。
  4. Direct memory,直接内存,NIO。原来的IO方式是数据先进入操作系统内存,然后JVM在执行时要从操作系统内存将数据拷贝过来一份再进行处理。NIO的直接内存方式就是省去了拷贝的过程,提高效率。Zero Copy,零拷贝,JVM可以直接去访问操作系统内核储存空间,不需要再拷贝到JVM空间处理。
  5. Method area,包含Run-time constant pool。
    1. JVM拥有一个方法区,是被所有线程所共享的。
    2. 方法区保存着每一个类的结构。
    3. 方法区是一个规范,包括两个不同版本的方法区的实现:
      1. Perm Space,JVM<1.8,FGC不会清理。大小在启动的时候指定,不能改变。一旦动态类特多的时候,会溢出。字符串常量位于PermSpace,在<1.8的情况下。而>1.8的时候,String常量位于堆。
      2. Meta Space,JVM>=1.8,会触发FGC清理。如果不设定默认最大就是物理内存占满,当满了就会FGC清理。
    4. 运行时常量池是每个类或接口在运行时读取的Class文件中的constant_pool的内容。

总结一下, 线程A: PC、JVMStacks、NMStacks 线程B: PC、JVMStacks、NMStacks 线程C: PC、JVMStacks、NMStacks 共享的内容:Heap,Method Area(Perm/Meta Space)

3、栈帧

栈帧对应一个线程的一个方法的内容,用于方法的执行,包括方法执行过程中的变量的临时状态。同时栈帧也执行动态链接,方法的返回值以及分发异常。栈帧被包含在JVM栈中。每一个栈帧包括:

1、局部变量表,Local Variable Table。

2、操作数栈,Operand Stack。

3、动态链接,Dynamic Linking。指向运行时常量池中的符号连接,如果解析就直接使用,未解析则执行解析再使用。例如A方法调用B方法,B要去常量池去找,找的这个过程就是动态链接。

4、返回地址,return address。a()->b(),方法a调用了方法b,b方法执行完了以后,返回值存放的位置,以及b方法执行完毕,应该接着执行a的哪里,也存放在这个返回地址。

3.1 自增代码的字节码检查

这里面的局部变量表,如上图的选中部分,Class文件中的方法的code中的LocalVariableTable会被JVM读取进入每一个线程中的栈的一个栈帧中的局部变量表,可以理解为这是一对一的。

注意,上图中局部变量表是初始状态,最右侧选中的部分,显示的是int a,其中a是名字,int是描述符I来代替。

当前这个LocalVariableTable读取到JVM是初始状态,接下来在JVM中要执行code的JVM指令,通过这些指令的执行,会改变这个LocalVariableTable。下面先来看对应main方法源码code的JVM指令。

0 sipush 311 3 istore_1 4 iload_1 5 iinc 1 by 1 8 istore_1 9 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> 12 iload_1 13 invokevirtual #3 <java/io/PrintStream.println : (I)V> 16 return

  • sipush 311,Push short。添加整数常量(311)到当前线程栈的main方法栈帧的操作数栈的栈顶[进栈]。
  • istore_1,Store int into local variable。把操作数栈顶的数(311)出栈,设定为局部变量表中下标为1的变量(a)的值(311)。

以上两句就完成了int a = 1;语句。

  • iload_1,Load int from local variable。将局部变量表中下标为1的变量的int值压入操作数栈栈顶。
  • iinc 1 by 1,Increment local variable by constant。将局部变量表中下标为1的变量(a)的值(311)加1。第一个1是index,指的是局部变量表的下标;第二个1是常量,指的是加几。所以执行完这条指令以后,局部变量表中下标为1的变量a的值编变成了312。

以上就完成了i++的代码,局部变量表的状态发生了变化。

  • istore_1,把操作数栈栈顶的值出栈,再存入局部变量表下标为1的变量a的值(311),替换掉了当前的312。

以上完成的是i = i++;的代码,把i在操作数栈中值就赋回给局部变量表了。所以结果打印出来i仍旧等于311。后面的几行指令都是执行system.out.println()的指令的,不再深入介绍。

反过来,改为i=++i;进行查看code字节码。

0 sipush 311 3 istore_1 4 iinc 1 by 1 7 iload_1 8 istore_1 9 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> 12 iload_1 13 invokevirtual #3 <java/io/PrintStream.println : (I)V> 16 return

这里面与a++的区别是 iinc 1 by 1和 iload_1指令的位置调换了,其他的都一样。

  • 先执行iinc 1 by 1,修改局部变量表下标1的变量的值加1,即a = 312。
  • 再执行iload_1,将局部变量表下标1的变量的值取出来,即312,压入操作数栈栈顶。

后面的逻辑是一样的,取出操作数栈栈顶的值保存到局部变量表下标1的值的位置中。

HotSpot的LocalVariableTable类似于CPU的寄存器,CPU寄存器的指令集是汇编语言。JVM的指令集是基于JVM规范,例如上面的字节码中code的存在于栈帧中的指令集,但硬件层面都会最终去执行CPU的寄存器,即执行汇编语言。

3.2 this

上面介绍了main方法的内容,那么栈帧与方法是一对一的,如果我们写一个自己的方法,它的字节码应该是怎样的呢?

我们自己写了一个方法getMoney,但是要注意的是这个方法不是static的。可以看到它的字节码中方法的code的局部变量表有3行,其中第一行是this,指向了当前类。如果加上static关键字,局部变量表中就没有this了。

原因是什么呢?因为非static方法是需要对象来执行的,而对象的类被this所指定了。

3.3 iadd

看一下code的JVM指令:

0 iload_1 1 iload_2 2 iadd 3 istore_3 4 iload_3 5 ireturn

  • iload_1,将a的值读入栈顶。
  • iload_2,将b的值读入栈顶。
  • iadd,Add int。从栈顶读出value1(b的值)和value2(a的值),然后相加得到结果result,将result值压入栈顶。
  • istore_3,栈顶出栈,将结果值存入局部变量表下标3的变量,即c的值里。
  • iload_3,将局部变量表c的值读入栈顶。
  • ireturn,Return int from method。方法返回int结果。

3.4 类内部构造其他对象

注意:前面提到了Stack Overflow。我们观察一下上图,在字节码方法的区域,有[0,1,2]三个方法,分别是init、main和add,这是Class文件中字节码代表的方法。要注意区分的是Class中的方法和JVM栈帧的区别,前者是静态的,后者是动态的,JVM栈帧在执行的时候会根据调用的层次关系逐个入栈,如果某方法并未被调用,则不会进入JVM栈帧。Class文件中每个方法静态的给出了code,这是对源码的翻译,也是JVM栈帧在执行时所需要解释执行的逐条读入的指令。

main方法中new了一个对象,下面重点看一下它的字节码指令。

0 new #2 <com/evswards/jvm/TestByteCodeJVM> 3 dup 4 invokespecial #3 <com/evswards/jvm/TestByteCodeJVM. : ()V> 7 astore_1 8 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;> 11 aload_1 12 iconst_1 13 iconst_2 14 invokespecial #5 <com/evswards/jvm/TestByteCodeJVM.add : (II)I> 17 invokevirtual #6 <java/io/PrintStream.println : (I)V> 20 return

  • new #2,创建一个对象。#2去找常量池,是一个类信息。这里涉及到对象内存的内容,包括堆的分配,GC详细学习。创建完,对象的引用objectref会压入栈顶。
  • dup,Duplicate the top operand stack value。栈顶出栈,拷贝,连续入栈2个值。
  • invokespecial,调用实例的方法。包括自动调用构造函数,以及当前类的方法以及它的父类的方法(若有)。该指令会执行操作数栈出栈,将对象引用取走使用。这就是dup要拷贝一份的原因。

后面的指令就不继续研究了,主要想体现的内容是对象引用的复制和调用时取走的机制。

3.5 返回值的接收

main方法中创建当前类对象,然后调用m方法,m方法有返回值,但是main方法并没有派变量来接收。观察右侧code的字节码指令。前面的都学习过了,直接看pop。

  • pop,这个指令很简单,就是操作数栈出栈。在执行完m方法以后,m方法的返回值会压入到main方法的栈顶,而这个返回值由于main方法没有安排变量来接收,所以直接pop掉了。

下面看另一种情况:

源码中我们做了调整,安排了int i来接收m的返回值。这时候code字节码的指令中发生了变化,原来pop的位置编程了istore_2,是因为main方法中i是在局部变量表中的下标为2的位置,前面还有0是args,1是h。所以将m的返回值在栈顶的内容直接出栈并写入到局部变量表中i的值里面。

回顾一下,this在局部变量表中什么时候出现,一定是在非static方法的局部变量表中的第一个元素。

3.6 递归

编写了一个阶乘的实现。字节码指令解释一下没见过的:

  • if_icmpne 7,比较栈顶俩值(此时栈顶俩值是前面两条指令压入,分别是iload_1压入n的值是3,然后iconst_1压入整数常量1,所以当前栈顶是1,3)是否相等,这两个值都必须是int类型,他们都会被依次出栈,然后进行对比。指令的结构是if代表控制,i代表类型,cmp是compare,ne是不等于,其他的还有:eq等于,lt小于,le小于等于,gt大于,ge大于等于。当比较结果为成功时,跳到第7行指令,即iload_1。
  • iload_1,执行前栈是空的,压入3。
  • iload_0,压入this
  • iload_1,再压入3
  • iconst_1,再压入1
  • isub,弹出栈顶1和3,用3-1,得到结果2压入栈顶。
  • invokevirtual,调用m方法,弹出传入2和this,当前栈顶为3。这里再嵌套进入新的栈帧去执行以上相同的指令。
  • 当执行到m(1)的时候,新的栈帧里面当执行到if_icmpne成功,不跳,直接返回1。ireturn返回给上一层栈帧,即m(2),同时m(1)栈帧从当前JVM栈弹出销毁。
  • imul,相乘的指令,m(2)拿到m(1)的返回值以后,会入栈,然后imul指令会把栈顶两个值,即m(1)结果和n即2相乘得到2。
  • ireturn,m(2)的结果作为返回值给m(3),同时m(2)栈帧从当前JVM栈弹出销毁。m(3)拿到m(2)返回值仍旧去执行imul指令,将返回值给main方法,同时m(3)栈帧从当前JVM栈弹出销毁。 那么,然后会进入到main方法栈帧,从中断位置重新执行,会拿到m(3)返回值在栈顶出栈,并记录为变量i的局部变量表中的值。

3.7 Stack Overflow

栈帧本身位于JVM栈中,针对每一个方法有一个栈帧,上面例子中如果main方法调用了add方法,JVM栈中会先读取main方法栈帧,然后在执行到调用add方法时,会继续入栈,读取add方法的栈帧。此时JVM栈中会有两个栈帧同时存在。当add方法执行完毕,会按照add方法栈帧的Return Address返回到main方法栈帧之前执行中断的位置继续执行,而同时add方法栈帧会出栈销毁(执行完毕没有用了)。

那么当方法嵌套调用太多,会导致JVM栈中的栈帧太多,超过了栈大小的限制,就会报错Stack Overflow。这部分在GC内容中会详细学习。

4、invoke指令

上面有介绍到一些invoke指令,例如invokeVirtual、InvokeStatic等。JVM的invoke指令总共有:

  • InvokeStatic,调用静态方法指令。main函数中调用一个静态方法m,这时候在main函数code中就会用到这个指令。
  • InvokeVirtual,大部分的方法调用都是这个指令,main函数中调用普通方法,需要先创建当前类的对象。InvokeVirtual指令自带多态,调用哪个对象的方法就会去到哪个对象里面,因此会创建新的方法栈帧,然后转去新的栈帧执行。
  • InvokeSpecial,目前只有构造方法和private方法使用到。因为private方法没有重写。InvokeSpecial,调用可以直接定位的,不需要多态的方法。(注意,final方法是执行InvokeVirtual,而不是InvokeSpecial)
  • InvokeInterface,通过interface来调用的方法,就需要这个指令。例如 List<String> list = new ArrayList<String>)();这时候通过list调用add方法时,list是List对象,List是一个接口,这时候调用add方法就是InvokeInterface指令。
  • InvokeDynamic,JVM最难的指令,lambda表达式或者反射或者其他动态语言,动态产生的class会用到该指令。

4.1 InvokeDynamic

lambda表达式的动态产生的类,会用到InvokeDynamic。输出的情况,看到都是Lambda的动态产生的类的情况。看一下字节码的内容:

可以验证到,字节码的指令都是用到了InvokeDynamic。

JVM规范中还有很多没有涉及到的指令,可以在用到的时候去手动查询,重点关注操作数栈的变化以及执行的描述。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、引言
  • 2、运行时数据区
  • 3、栈帧
    • 3.1 自增代码的字节码检查
      • 3.2 this
        • 3.3 iadd
          • 3.4 类内部构造其他对象
            • 3.5 返回值的接收
              • 3.6 递归
                • 3.7 Stack Overflow
                • 4、invoke指令
                  • 4.1 InvokeDynamic
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档