周口网站建设哪家好,登封seo推广,做名片的网站,acm网站免费做JVM 基础 - 类字节码详解 多语言编译为字节码在JVM运行Java字节码文件Class文件的结构属性从一个例子开始反编译字节码文件字节码文件信息常量池方法表集合类名 再看两个示例分析try-catch-finallykotlin 函数扩展的实现 源代码通过编译器编译为字节码#xff0c;再通过类加载… JVM 基础 - 类字节码详解 多语言编译为字节码在JVM运行Java字节码文件Class文件的结构属性从一个例子开始反编译字节码文件字节码文件信息常量池方法表集合类名 再看两个示例分析try-catch-finallykotlin 函数扩展的实现 源代码通过编译器编译为字节码再通过类加载子系统进行加载到JVM中运行。 多语言编译为字节码在JVM运行
计算机是不能直接运行java代码的必须要先运行java虚拟机再由java虚拟机运行编译后的java代码。这个编译后的java代码就是本文要介绍的java字节码。
为什么jvm不能直接运行java代码呢这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的java是高级语言只有人类才能理解其逻辑计算机是无法识别的所以java代码必须要先编译成字节码文件jvm才能正确识别代码转换后的指令并将其运行。 Java代码间接翻译成字节码储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行从而实现一次编写到处运行的目的。 JVM也不再只支持Java由此衍生出了许多基于JVM的编程语言如Groovy, Scala, Koltin等等。 Java字节码文件
class文件本质上是一个以8位字节为基础单位的二进制流各个数据项目严格按照顺序紧凑的排列在class文件中。jvm根据其特定的规则解析该二进制数据从而得到相关信息。
Class文件采用一种伪结构来存储数据它有两种类型无符号数和表。这里暂不详细的讲。
本文将通过简单的java例子编译后的文件来理解。
Class文件的结构属性
在理解之前先从整体看下java字节码文件包含了哪些类型的数据
从一个例子开始
下面以一个简单的例子来逐步讲解字节码。
//Main.java
public class Main {private int m;public int inc() {return m 1;}
}通过以下命令, 可以在当前所在路径下生成一个Main.class文件。
javac Main.java以文本的形式打开生成的class文件内容如下:
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0011 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 3ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 00e文件开头的4个字节(“cafe babe”)称之为 魔数唯有以cafe babe开头的class文件方可被虚拟机所接受这4个字节就是字节码文件的身份识别。 0000是编译器jdk版本的次版本号00034转化为十进制是52,是主版本号java的版本号从45开始除1.0和1.1都是使用45.x外,以后每升一个大版本版本号加一。也就是说编译生成该class文件的jdk版本为1.8.0。
通过java -version命令稍加验证, 可得结果。
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)继续往下是常量池… 知道是这么分析的就可以了然后我们通过工具反编译字节码文件继续去看。
反编译字节码文件 使用到java内置的一个反编译工具javap可以反编译字节码文件, 用法: javap options classes 其中options选项包括: -help --help -? 输出此用法消息-version 版本信息-v -verbose 输出附加信息-l 输出行号和本地变量表-public 仅显示公共类和成员-protected 显示受保护的/公共类和成员-package 显示程序包/受保护的/公共类和成员 (默认)-p -private 显示所有类和成员-c 对代码进行反汇编-s 输出内部类型签名-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)-constants 显示最终常量-classpath path 指定查找用户类文件的位置-cp path 指定查找用户类文件的位置-bootclasspath path 覆盖引导类文件的位置
输入命令javap -verbose -p Main.class查看输出内容:
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.classLast modified 2018-4-7; size 362 bytesMD5 checksum 4aed8540b098992663b7ba08c65312deCompiled from Main.java
public class com.rhythm7.Mainminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 Methodref #4.#18 // java/lang/Object.init:()V#2 Fieldref #3.#19 // com/rhythm7/Main.m:I#3 Class #20 // com/rhythm7/Main#4 Class #21 // java/lang/Object#5 Utf8 m#6 Utf8 I#7 Utf8 init#8 Utf8 ()V#9 Utf8 Code#10 Utf8 LineNumberTable#11 Utf8 LocalVariableTable#12 Utf8 this#13 Utf8 Lcom/rhythm7/Main;#14 Utf8 inc#15 Utf8 ()I#16 Utf8 SourceFile#17 Utf8 Main.java#18 NameAndType #7:#8 // init:()V#19 NameAndType #5:#6 // m:I#20 Utf8 com/rhythm7/Main#21 Utf8 java/lang/Object
{private int m;descriptor: Iflags: ACC_PRIVATEpublic com.rhythm7.Main();descriptor: ()Vflags: ACC_PUBLICCode:stack1, locals1, args_size10: aload_01: invokespecial #1 // Method java/lang/Object.init:()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/rhythm7/Main;public int inc();descriptor: ()Iflags: ACC_PUBLICCode:stack2, locals1, args_size10: aload_01: getfield #2 // Field m:I4: iconst_15: iadd6: ireturnLineNumberTable:line 8: 0LocalVariableTable:Start Length Slot Name Signature0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: Main.java字节码文件信息
开头的7行信息包括:Class文件当前所在位置最后修改时间文件大小MD5值编译自哪个文件类的全限定名jdk次版本号主版本号。
然后紧接着的是该类的访问标志ACC_PUBLIC, ACC_SUPER访问标志的含义如下:
标志名称标志值含义ACC_PUBLIC0x0001是否为Public类型ACC_FINAL0x0010是否被声明为final只有类可以设置ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义ACC_INTERFACE0x0200标志这是一个接口ACC_ABSTRACT0x0400是否为abstract类型对于接口或者抽象类来说次标志值为真其他类型为假ACC_SYNTHETIC0x1000标志这个类并非由用户代码产生ACC_ANNOTATION0x2000标志这是一个注解ACC_ENUM0x4000标志这是一个枚举
常量池
Constant pool意为常量池。
常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念如文本字符串final常量等而符号引用则属于编译原理方面的概念包括以下三种:
类和接口的全限定名(Fully Qualified Name)字段的名称和描述符号(Descriptor)方法的名称和描述符
不同于C/C, JVM是在加载Class文件的时候才进行的动态链接也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当虚拟机运行时需要从常量池获得对应的符号引用再在类创建或运行时解析并翻译到具体的内存地址中。 直接通过反编译文件来查看字节码内容
#1 Methodref #4.#18 // java/lang/Object.init:()V
#4 Class #21 // java/lang/Object
#7 Utf8 init
#8 Utf8 ()V
#18 NameAndType #7:#8 // init:()V
#21 Utf8 java/lang/Object第一个常量是一个方法定义指向了第4和第18个常量。以此类推查看第4和第18个常量。最后可以拼接成第一个常量右侧的注释内容:
java/lang/Object.init:()V这段可以理解为该类的实例构造器的声明由于Main类没有重写构造方法所以调用的是父类的构造方法。此处也说明了Main类的直接父类是Object。 该方法默认返回值是V, 也就是void无返回值。
第二个常量同理可得:
#2 Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 Class #20 // com/rhythm7/Main
#5 Utf8 m
#6 Utf8 I
#19 NameAndType #5:#6 // m:I
#20 Utf8 com/rhythm7/Main复制代码此处声明了一个字段m类型为I, I即是int类型。关于字节码的类型对应如下
标识字符含义B基本类型byteC基本类型charD基本类型doubleF基本类型floatI基本类型intJ基本类型longS基本类型shortZ基本类型booleanV特殊类型voidL对象类型以分号结尾如Ljava/lang/Object;
对于数组类型每一位使用一个前置的[字符来描述如定义一个java.lang.String[][]类型的维数组将被记录为[[Ljava/lang/String;
方法表集合
在常量池之后的是对类内部的方法描述在字节码中以表的集合形式表现暂且不管字节码文件的16进制文件内容如何我们直接看反编译后的内容。
private int m;descriptor: Iflags: ACC_PRIVATE此处声明了一个私有变量m类型为int返回值为int
public com.rhythm7.Main();descriptor: ()Vflags: ACC_PUBLICCode:stack1, locals1, args_size10: aload_01: invokespecial #1 // Method java/lang/Object.init:()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/rhythm7/Main;这里是构造方法Main()返回值为void, 公开方法。
code内的主要属性为:
stack: 最大操作数栈JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1locals: 局部变量所需的存储空间单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位为4个字节大小。方法参数(包括实例方法中的隐藏参数this)显示异常处理器的参数(try catch中的catch块所定义的异常)方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是locals的大小并不一定等于所有局部变量所占的Slot之和因为局部变量中的Slot是可以重用的。args_size: 方法参数的个数这里是1因为每个实例方法都会有一个隐藏参数thisattribute_info: 方法体内容0,1,4为字节码行号该段代码的意思是将第一个引用类型本地变量推送至栈顶然后执行该类型的实例方法也就是常量池存放的第一个变量也就是注释里的java/lang/Object.:()V, 然后执行返回语句结束方法。LineNumberTable: 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息如果选择不生成LineNumberTable当程序运行异常时将无法获取到发生异常的源码行号也无法按照源码的行数来调试程序。LocalVariableTable: 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息如果没有生成这项信息那么当别人引用这个方法时将无法获取到参数名称取而代之的是arg0, arg1这样的占位符。 start 表示该局部变量在哪一行开始可见length表示可见行数Slot代表所在帧栈位置Name是变量名称然后是类型签名。
同理可以分析Main类中的另一个方法inc():
方法体内的内容是将this入栈获取字段#2并置于栈顶, 将int类型的1入栈将栈内顶部的两个数值相加返回一个int类型的值。
类名
最后很显然是源码文件
SourceFile: Main.java再看两个示例
分析try-catch-finally
通过以上一个最简单的例子可以大致了解源码被编译成字节码后是什么样子的。 下面利用所学的知识点来分析一些Java问题:
public class TestCode {public int foo() {int x;try {x 1;return x;} catch (Exception e) {x 2;return x;} finally {x 3;}}
}试问当不发生异常和发生异常的情况下foo()的返回值分别是多少。
javac TestCode.java
javap -verbose TestCode.class查看字节码的foo方法内容:
public int foo();descriptor: ()Iflags: ACC_PUBLICCode:stack1, locals5, args_size10: iconst_1 //int型1入栈 -栈顶11: istore_1 //将栈顶的int型数值存入第二个局部变量 -局部212: iload_1 //将第二个int型局部变量推送至栈顶 -栈顶13: istore_2 //!!将栈顶int型数值存入第三个局部变量 -局部314: iconst_3 //int型3入栈 -栈顶35: istore_1 //将栈顶的int型数值存入第二个局部变量 -局部236: iload_2 //!!将第三个int型局部变量推送至栈顶 -栈顶17: ireturn //从当前方法返回栈顶int数值 -18: astore_2 // -局部3Exception9: iconst_2 // -栈顶210: istore_1 // -局部2211: iload_1 //-栈顶212: istore_3 //!! -局部4213: iconst_3 // -栈顶314: istore_1 // -局部1315: iload_3 //!! -栈顶216: ireturn // - 217: astore 4 //将栈顶引用型数值存入第五个局部变量any19: iconst_3 //将int型数值3入栈 - 栈顶320: istore_1 //将栈顶第一个int数值存入第二个局部变量 - 局部2321: aload 4 //将局部第五个局部变量(引用型)推送至栈顶23: athrow //将栈顶的异常抛出Exception table:from to target type0 4 8 Class java/lang/Exception //0到4行对应的异常对应#8中储存的异常0 4 17 any //Exeption之外的其他异常8 13 17 any17 19 17 any在字节码的4,5以及13,14中执行的是同一个操作就是将int型的3入操作数栈顶并存入第二个局部变量。这正是我们源码在finally语句块中内容。也就是说JVM在处理异常时会在每个可能的分支都将finally语句重复执行一遍。
通过一步步分析字节码可以得出最后的运行结果是
不发生异常时: return 1发生异常时: return 2发生非Exception及其子类的异常抛出异常不返回值
kotlin 函数扩展的实现
kotlin提供了扩展函数的语言特性借助这个特性我们可以给任意对象添加自定义方法。
以下示例为Object添加sayHello方法
//SayHello.kt
package com.rhythm7fun Any.sayHello() {println(Hello)
}编译后使用javap查看生成SayHelloKt.class文件的字节码。
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/SayHelloKt.class
Last modified 2018-4-8; size 958 bytesMD5 checksum 780a04b75a91be7605cac4655b499f19Compiled from SayHello.kt
public final class com.rhythm7.SayHelloKtminor version: 0major version: 52flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool://省略常量池部分字节码
{public static final void sayHello(java.lang.Object);descriptor: (Ljava/lang/Object;)Vflags: ACC_PUBLIC, ACC_STATIC, ACC_FINALCode:stack2, locals2, args_size10: aload_01: ldc #9 // String $receiver3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V6: ldc #17 // String Hello8: astore_19: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;12: aload_113: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V16: returnLocalVariableTable:Start Length Slot Name Signature0 17 0 $receiver Ljava/lang/Object;LineNumberTable:line 4: 6line 5: 16RuntimeInvisibleParameterAnnotations:0:0: #7()
}
SourceFile: SayHello.kt观察头部发现,koltin为文件SayHello生成了一个类类名com.rhythm7.SayHelloKt.
由于我们一开始编写SayHello.kt时并不希望SayHello是一个可实例化的对象类所以SayHelloKt是无法被实例化的SayHelloKt并没有任何一个构造器。
再观察唯一的一个方法发现Any.sayHello()的具体实现是静态不可变方法的形式:
public static final void sayHello(java.lang.Object);所以当我们在其他地方使用Any.sayHello()时事实上等同于调用java的SayHelloKt.sayHello(Object)方法。
顺便一提的是当扩展的方法为Any时意味着Any是non-null的这时编译器会在方法体的开头检查参数的非空即调用 kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(Object value, String paramName) 方法来检查传入的Any类型对象是否为空。如果我们扩展的函数为Any?.sayHello()那么在编译后的文件中则不会有这段字节码的出现。