前言

JVM内存模型.png

与上图类似的JVM内存模型图见过多次,仅从概念上去理解各个区域的作用,难有深刻印象。

当学习一个类如何存储,即JVM如何解析.Class文件,能知道方法区存在的意义。本文的目的则是学习JVM如何执行一个方法,如此对栈与程序计数器有更深刻的认识。

note 文中部分内容需要.Class文件知识,但总体上不妨碍理解

.Class参考

字节码基础

Java代码通过编译后,会将对应的函数方法转为字节码指令,如果了解.Class如何组成,可在对应方法表里的Code属性表查找到对应的一系列字节码指令。函数的执行本质上是数据运算与执行调度,因此可以用一系列的指令来进行描述。

字节码指令由一个字节长度表示,代表特定的操作含义,后面可以跟随零到多个必要参数。使用一个字节表示,意味着字节码的总数不可能操作 256条。

下面表列出了常用的数据类型对应的字节码指令,粗略看一眼就可以,需要的时候再具体查阅每条字节码的含义。

opcode

byte

short

int

long

float

doubt

char

Reference

Tipush

bipush

sipush

Tconst

iconst

lconst

fconst

dconst

aconst

Tload

iload

lload

fload

dload

alod

Tstore

istore

lstore

fstore

dstore

astore

Tinc

iinc

Taload

baload

saload

iaload

laload

faload

daload

caload

aaload

Tastore

bastore

sastore

iastore

lastore

fastore

dastore

castore

astore

Tadd

iadd

ladd

fadd

dadd

Tsub

isub

lsub

fsub

dsub

Tmul

imul

lmul

fmul

dmul

Tdiv

idvi

ldiv

fdiv

ddiv

Trem

irem

lrem

frem

drem

Tneg

ineg

lneg

fneg

dneg

Tncg

ineg

lneg

fneg

dneg

Tshl

ishl

lshl

Tshr

ishr

lshr

Tushr

iushr

lushr

Tadnd

iadn

land

Tor

ior

lor

Txor

ixor

lxor

i2T

i2b

i2s

i2l

i2f

i2d

l2T

l2i

l2f

l2d

f2T

f2i

f2l

F2d

d2T

d2i

d2l

D2f

Tcmp

lamp

Tcmpl

fcmpl

dcmpl

Tcmpg

fcmpg

dcmpg

if_TcmpOP

if_icmpOP

if_acmpOP

Treturn

ireturn

lreturn

freturn

dreturn

Return

字节码用途大致分为9类,仅做简要介绍:

加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,如将一个局部变量加载到操作数栈Tload; 将一个数值从操作数栈存储到局部变量表Tstore

运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶,如加减对应为Tadd、Tsub

类型转换指令:将两种不同的数值进行相互转换

对象创建与访问指令: 创建类实例指令new,访问类字段getfield、putfield

操作数栈管理指令:与操作数据结构的堆栈类似,如将操作数栈栈顶一个或两个元素出栈pop、pop2

控制转移指令:可以让JVM有条件或无条件地从指定位置指令继续执行程序,如条件分支if系列;如无条件分支goto

方法调用和返回指令:如invokevirtural 用于调用对象的实例方法、Treturn 返回值

异常处理指令:throw语句抛出的异常,由athrow指令来实现

同步指令:处理同步操作,如synchronized关键字由 monitorenter 和 monitorexit 指令来实现

栈帧基础

每执行调用一个方法,将用一个栈帧来支持此方法的执行。栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等。每一个方法调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在进行字节码指令操作时,需要确定数据归属到什么变量,需要局部变量表;需要对数据进行操作并存取,需要操作数栈;需要知道执行到哪,需要程序计数器;可能需要在运行时转化调用的具体方法,需要动态连接。

一个线程中的方法调用链可能会很长,很多方法同时处于执行状态,对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

栈帧结构如下图

栈帧结构图.jpg

局部变量表

局部变量表用来存放方法参数和方法内部定义的局部变量,最大所需容量有max_locals表示,单位为Slot。

JVM没有指明一个 Slot占用的内存空间大小。Slot可以用32位或更小的物理内存来存放,也可以在64位虚拟机中使用64位的物理内存去实现一个Slot。对于64位的数据类型,虚拟机会以高位对齐的方式分配两个连续的Slot空间。Slot除了能存放基础数据类型外,还能存储reference和returnAddress,reference为一个对象实例的引用,能通过此引用直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,以及直接或间接地查找到对象所属数据类型在方法区中的存储的类型索引。

Slot是可以重用的,如果在之后的执行区域里,局部变量如x不再使用,则x占用的Slot将会被清理再做他用。如果方法不是静态方法,一般第0个Slot为 “this”。

操作数栈

字节码指令进行操作时,将从操作数栈中写入和提取内容。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。任意时刻不会超过max_stacks。

动态连接

每个栈帧都包含一个指向运行时常量池中(位于方法区),该栈帧所属方法的引用,以支持方法调用过程中的动态连接。在源文件被编译成.Class文件后,.Class文件中常量池存有大量的符号引用。字节码指令进行方法调用时,会以指向方法的符号引用作为参数。这些符号引用一部分在类加载或第一次使用时转化为直接引用,称为静态解析。而另一部分将在每一次运行期间转化为直接引用,称为动态连接。

方法返回地址

一个方法执行后有两种方式退出。

正常退出:遇到任意代表方法返回的字节码指令,将返回值返回给上层调用者

异常退出:执行过程遇到了异常,并且没用在方法内进行处理,没有返回值。

不管哪一种方法退出,都需要返回到方法被调用的位置,让程序继续执行。返回地址位置,可以通过程序计数器来确定,将程序计数器的值指向下一条执行,令程序继续执行。

字节码运行

说了这么多,通过几个例子看字节码如何运行。

简单的运算

public static int addAndDouble(int a, int b){

return (a + b) * 2;

}

函数将a和b相加然后乘以2,拿到结果返回。通过命令

javac fileName.java

编译出.Class文件,能拿到具体的字节码。

通过命令

javap -verbose class文件

能对.Class进行分析。

上面代码转成的字节码以及字节码指令为:

addAndDouble字节码指令.jpg

addAndDouble()需要的操作数栈深为stack=2,局部变量表深度为locals=2,参数args_size=2个(因为是static,不包含this),字节码指令流为 1A 1B 60 06 68 AC 。字节码命令前0、1、2等代表的是在指令在指令流中开始的位置。

0: iload_0 // 将第一个局部变量压入栈,也就是代码里的a

1: iload_1 // 将第二个局部变量压入栈,也就是代码里的b

2: iadd // 将栈顶的两个元素取出相加并入栈, 即a+b,暂用c表示

3: iconst_2 // 将常数2压入栈

4: imul // 将栈顶的两个元素取出相乘并入栈,即 c * 2 ,暂用d表示

5: ireturn // 将栈顶元素即d返回

过程用下图表示

相加再乘2代码字节码运行实例.png

同步方法和条件语句

代码为

public void syncFunction(int a){

if (a == 2){

synchronized (this){

a++;

}

}

}

代码目的仅是为看同步操作和条件语句如何执行,转出的字节码指令流和字节码指令为:

条件判断和同步.jpg

syncFunction()是有两个参数的,第一个为this,第二个则为传来的a,对于操作数栈和局部变量表的操作与之前没有区别,只需注意在有this时存于局部变量表第一位。astore命令时从操作数栈取出数据存入局部变量。

”2:“ 为代码if翻译出的字节码指令,当满足条件时从"5:"处继续执行指令,如果不满足条件则跳转到 "22:" 处。字节码指令是可以带参数的,“2:”的下一条指令从"5:"开始,if翻译出的指令占了三个字节,为 A0 00 14,其中A0代表if_icmpne指令,0x0014 为参数,十进制值为20,即要跳转的指令位置,当不满足if条件时跳转到字节码指令流第“2 + 20”处的指令,也就是“22:”处的指令。因为方法占用的最大字节为65535,因此用两位字节表示跳转位置足够。

“8:” ~ “13:” 是同步代码里的正常运行指令,简单了解即可。

异常调用

代码为

public void exceptionFunction(){

try {

File file = new File("");

file.getName();

} catch (Exception e){

e.printStackTrace();

}

}

转出的字节码指令流和字节码指令为:

异常方法字节码示例.jpg

如果没有异常发生,执行到“15:”的字节码指令后,会跳转到“23:”,意味着“18:” ~ "20:" 表示catch部分的执行。代码实例中,执行“11:”会发生异常,进入catch部分。

字节码执行小结

以上三个例子抛砖引玉说明字节码指令的执行,其它情况可以用类似方法分析。只要弄明白栈帧里局部变量表、操作数栈、返回地址、程序计数器的作用,以及字节码指令含义和携带参数含义,就可以知道字节码是怎样执行的。比如上一张图片中, “6:”出invokespecial命令,携带一个参数,参数指向的是常量池中的一个方法描述符,通过这些信息可以知道此命令是调用File的初始化函数创建对象。

字节码指令的执行可以简述为:

运算中需要的额外数据存储,需要局部变量表,

对操作数进行运算,需要操作数栈

程序计数器记录指令执行到哪

方法调用

上面部分说明了方法是怎样执行的,在执行之前,JVM需知道具体要调用哪个方法,可以通过解析和分派完成。

解析

.Class文件

中存储的都是符号引用,不是直接引用。因而在类加载的解析阶段,会将一部分符号引用转化为直接引用,前提是在程序真正运行之前就有一个可以确定的调用版本,这个过程称为 “解析” 。

执行方法调用的字节码指令有:

invokestatic: 调用静态方法

invokespecial: 调用实例构造器方法、私有方法和父类方法

invokevirtual: 调用所有的虚方法

invokeinterface: 调用接口方法,会在运行时再确定一个实现次接口的对象

invokedynamic: 在运行时才能确定调用的具体方法,由调用点限定符确定

能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,有 静态方法、私有方法、实例构造器、父类方法 以及被标识为 final的虚方法,没有任何手段可以覆盖或隐藏以上方法。

也因此解析是静态过程,在编译期间就可以确定,在类装载的解析阶段能把涉及到的符号引用全部转变为可以确定的直接引用。

分派

分派可以分为静态分派和动态分派,可以通过“重载”和“重写”一探究竟。无论如何,目的是看虚拟机如何选择正确的目标方法。

静态分派

代码例子

public class StaticDispatch {

static class Parent{ }

static class Child extends Parent{ }

public void call(Parent parent){

System.out.println("call parent");

}

public void call(Child child){

System.out.println("call child");

}

public static void main(String[] args) {

Parent parent = new Child();

StaticDispatch dispatch = new StaticDispatch();

dispatch.call(parent);

}

}

实际会输出 “call parent”。 对于 Parent parent = new Child() 来说,前面的Parent称为静态变量,后面的Child称为实际变量。静态变量和实际变量在程序中都可以发生一些变化,区别是静态变量的变化仅仅发生在使用时,变量本身的静态类型不会改变,在编译期可知。JVM在确定重载版本时,是通过静态变量作为依据的。

静态分派.jpg

对StaticDispatch的call()方法选取重载版本翻译成字节码指令时,选择的是Parent的版本。

动态分派

动态分派则不同,需要确定运行时确定的数据类型,否则就乱了套。

代码如下:

public class DynamicDispatch {

static class Parent{

public void hello(){

System.out.println("hello parent");

}

}

static class Child extends Parent{

@Override

public void hello(){

System.out.println("hello child");

}

}

public static void main(String[] args) {

Parent parent = new Child();

parent.hello();

}

}

执行结果输出为 “hello child”,下图为字节码解析图:

动态分派.jpg

字节码指令翻译出来的静态类型为Parent,但在方法执行是,调用的是实际类型为Child的方法。在"8: " 处,将代码新建的parent变量压入栈,然后“9: ”处调用invokevirtual指令,调用的hello()方法归属于parent(实际类型为Child),parent也称为方法的接收者(Receiver)。

invokevirtual指令的运行过程分为以下步骤:

找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C

如果在类型C中找到与常量中的描述符和简单名称都相符的方法(也就是上图红圈右边部分),则进行权限校验,通过则返回这个方法的直接引用,结束查找;否则,返回java.lang.IllegalAccessError异常

否则按照继承关系从上往下按照步骤2查找

如果都没有找到合适的方法,抛出java.lang.AbstracMethodError异常

因此,上面代码的hello()方法的实际接收者类型为Child。invokevirtual指令在运行期确定接收者的实际类型,是将常量池中的方法描述符指向了实际的直接引用上,这个过程是重写的本质。运行期根据实际类型确定方法执行版本的过程就是动态分派。

多分派与单分派

分派除了能以静态分派与动态分派区分外,还能以多分派和单分派区分,两者的区别在于选取方法是,参照的“宗量”,按照一个宗量选取的称为单分派,按照多个宗量选取的称为多分派。方法的接受者、方法的参数称为方法的宗量,也可以不贴切地理解为依据。

public class MultiDispatch {

static class Cigarette{} // 烟

static class Toy{} // 玩具

static class Parent{

public void choice(Cigarette cigarette){

System.out.println("parent choice cigarette");

}

public void choice(Toy toy){

System.out.println("parent choice toy");

}

}

static class Child extends Parent{

public void choice(Cigarette cigarette){

System.out.println("Child choice cigarette");

}

public void choice(Toy toy){

System.out.println("Child choice Toy");

}

}

public static void main(String[] args) {

Parent parent = new Parent();

parent.choice(new Cigarette());

Parent child = new Child();

child.choice(new Toy());

}

}

运行结果为

parent choice cigarette

Child choice Toy

多分派.jpg

在编译时期,也就是静态分派时期。选择目标方法的依据有亮点:静态类型、方法参数。因此翻译成的字节码指令invokevirtual的指令参数均指向了 Parent.choice(),一个指向的是常量符号引用是 Parent.choice(Cigarette),另一个是Parent.choice(Toy)。根据两个宗量进行原则,因此Java里的静态分派数据多分派。

在运行时期,也就是动态分派过程。执行choice()方法时,需要确定接收者的实际类型,因为要执行的方法已被确认,无需关心,因此参数的静态类型、实际类型都不会对方法的选择构成影响。只有接收者的实际类型会构成影响。因此动态分派属于单分派类型,只以一个宗量作为选择。

总结

JVM 方法的执行可以总结为以下几点:

方法调用时,根据字节码指令的不同,以解析和分派确定目标方法

invokestatic、invokespecial指令和以final声明的方法可以以解析确认目标方法,invokevirtual、invokeinterface、invokedynamic则以分派方式确认目标方法

分派需要考虑的宗量为:接收者的静态类型和实际类型,参数的静态类型。静态分派时考虑接受者的静态类型和参数的静态类型;动态分派考虑接受者的实际类型

方法代码最终转换为紧凑的字节码指令流,不同的字节码值代表不同的指令,后面可能会带有0到多个参数,查表可以知道对象的字节码含义以及参数含义,进而进行指令操作

字节码指令执行时,需要程序计数器记录执行到的指令,以能执行下一条指令;执行过程需要存储数据,借助局部变量表存储;指令执行时需要对操作数进行运算,通过操作数栈进行存取

return系列指令代表一个方法正常执行结束,栈帧出栈,下一栈帧,也就是方法的调用这个方法的方法继续从它的程序计数器指向的下一条指令继续执行

参考

《深入理解 Java 虚拟机》—— 第 8 章

Java字节码分析

java方法执行jvm做了什么_JVM 方法到底如何执行相关推荐

  1. 深入理解JVM:Java语言与JVM关系

    在那个电闪雷鸣,群鸟环绕的夜晚,一个不为人知的语言Oak诞生了,此时正值1991年4月份,气温舒适,百花齐放.然而Oak的诞生并没有得到人们的关注,直到1995年5月23号,Oak决定正是更名为Jav ...

  2. Java编译器、JVM、解释器

    Java虚拟机(JVM)是可运行Java代码的假想计算机.只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行.本文首先简要介绍从Java文件的编译 ...

  3. JVM(类加载、运行时数据区、堆内存、方法区、本地接口、执行引擎和垃圾回收)java虚拟机(JVM)的超详细知识点

    JVM虚拟机 一.JVM的概述 1.为什么要学习JVM 2.虚拟机 3.JVM的作用 作用 特点 4.JVM的位置 5.JVM的分类 6.各个组成部分的用途 7.Java 代码的执行流程 8.JVM ...

  4. java调优方法,jvm监控工具

    graph LR A-->B 性能概述 程序性能表现形式 执行速度:程序响应速度,总耗时是否足够短 内存分配:内存分配是否合理,是否过多消耗内存或者存在泄漏 启动时间:程序运行到可以正常处理业务 ...

  5. 【Java书笔记】:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》第2部分-自动内存管理,第3部分-虚拟机执行子系统,第5部分-高效并发

    作者:周志明 整理者GitHub:https://github.com/starjuly/UnderstandingTheJVM 第2部分-自动内存管理 第2章 Java内存区域与内存溢出异常 2.2 ...

  6. JVM优化Java代码时都做了什么?

    专栏的前几篇文章了解了JVM的内存模型,GC调优的思路,让我们对于Java底层有了一定的了解,那么采用这种思路去提高JVM的性能,减少JVM额外消耗的同时,JVM究竟做了哪些工作,使我们的Java代码 ...

  7. 【JVM源码解析】模板解释器解释执行Java字节码指令(上)

    本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...

  8. java执行jar中的main_浅谈java 执行jar包中的main方法

    浅谈java 执行jar包中的main方法 通过 OneJar 或 Maven 打包后 jar 文件,用命令: java -jar ****.jar 执行后总是运行指定的主方法,如果 jar 中有多个 ...

  9. Java 8的默认方法:可以做什么和不能做什么?

    什么是默认方法 在Java 8发行版中,您可以修改接口以添加新方法,以便该接口与实现该接口的类保持兼容. 如果您要开发一个库,该库将由基辅到纽约的几位程序员使用,那么这非常重要. 在Java 8出现之 ...

  10. java打包成jar|执行jar包中的main方法

    java打包成jar jar -cvf [jar包的名字] [需要打包的文件] 执行jar包中的main方法 java -jar ****.jar 执行后总是运行指定的主方法,如果 jar 中有多个 ...

最新文章

  1. centos 6安装 vim
  2. 自动驾驶有量子飞跃式改进,马斯克称年内实现L5级别自动驾驶?
  3. 运行程序时java后面跟的是文件名对吗_运行程序时java命令后面跟的是文件名。...
  4. jzoj3302-[集训队互测2013]供电网络【上下界网络流,费用流,动态加边】
  5. 关于Eclipse基本设置(字体大小、项目导入、简单)
  6. 使用MaxCompute LOAD命令批量导入OSS数据最佳实践—STS方式LOAD开启KMS加密OSS数据
  7. 正常web页面登录时效是多少_Web 系统的安全性测试之权限管理测试
  8. 国外的电子商务开发情况
  9. cs6给画笔分组_画笔工具,PS cs6笔刷入门介绍
  10. 天正安装autocad启动失败_安装天正后cad无法启动 - 卡饭网
  11. 【c++NOIP2015 普及组】 推销员
  12. 巧用RoaringBitMap处理海量数据内存diff问题
  13. 1660 super安装tensorflow1.15
  14. NodeJs 面试题 2023
  15. 一种确定六边形螺栓中心(形心)的Opencv方法——Python实现
  16. 渗透测试-内网横向移动专题
  17. 今天心情好,给各位免费呈上200兆SVN代码服务器一枚,不谢!
  18. VASP计算弹性常数
  19. Java学习之旅(三四):包装类之 Double
  20. 数据库学习笔记7关系数据库标准语言SQL

热门文章

  1. TCSVT论文结构整理
  2. 计算机主机由cpu和内储存器构成,计算机主机由CPU、存储器和硬盘组成。
  3. 创建LV报错/dev/vgdata/data: not found: device not cleared Aborting. Failed to wipe start of new LV.
  4. php 手写签批 手机办公_好签原笔迹手写签批SDK
  5. 如何更好的提问-在提问之前试试Stack Overflow、小黄鸭调试法
  6. 微软Kinect:谁还要控制器?
  7. 配置本地yum源文件
  8. numpy 求向量夹角 区间 [-pi, +pi]
  9. Windows 10 家庭版在忘记旧密码的情况下,如何重置密码
  10. 丹佛机场行李处理系统