摘要:本文已收录修炼内功跃迁之路学习语言的时候,需要在不同的目标操作系统上或者使用交叉编译环境,使用正确的指令集编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理宏的
本文已收录【修炼内功】跃迁之路
学习C语言的时候,需要在不同的目标操作系统上(或者使用交叉编译环境),(使用正确的CPU指令集)编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理(宏)
Java的出现也正是为了解决"平台无关性","Write Once, Run Anywhere"的口号也充分表达了软件开发人员对冲破平台接线的渴求
"与平台无关"的最终实现还是要在操作系统的应用层上,这便是JVM的存在,不同的平台有不同的JVM,而所有的JVM都可以载入与平台无关的字节码,从而实现程序的"一次编写,到处运行"
JVM并非只为Java设计,而字节码也并非只有Java才可以编译得到,早在Java发展之初,设计者便将Java规范拆分为Java语言规范及Java虚拟机规范,同时也承诺,对JVM做适当的扩展,以便更好地支持其他语言运行于JVM之上,而这一切的基础便是Class文件(字节码文件),Class文件中存放了JVM可以理解运行的字节码命令
In the future, we will consider bounded extensions to th Java virtual machine to provide better support for other languages
JVM并不关心Class的来源是何种语言,在JVM发展到1.7~1.8的时候,设计者通过JSR-292基本兑现了以上承诺
本篇不会去详细地介绍如何去解析Class文件,目的是为了了解Class文件的结构,Class文件中都包含哪些内容
Class文件可以由JVM加载并执行,其中记录了类信息、变量信息、方法信息、字节码指令等等,虽然JVM加载Class之后(在JIT之前)进行的是解释执行,但Class文件并不是文本文件,而是被严格定义的二进制流文件
接下来,均会以这段代码为示例进行分析
import java.io.Serializable; public class ClassStruct implements Serializable { private static final String HELLO = "hello"; private String name; public ClassStruct(String name) { this.name = name; } public void print() { System.out.println(HELLO + ": " + name); } public static void main(String[] args) { ClassStruct classStruct = new ClassStruct("ManerFan"); classStruct.print(); } }
使用$ javac ClassStruct.java进行编译,编译后的文件可以使用$ javap -p -v ClassStruct查看Class文件的内容(见文章末尾)
魔数很多文件存储都会使用魔数来进行身份识别,比如图片文件,即使将图片文件改为不正确的后缀,绝大多数图片预览器也会正确解析
同样Class文件也不例外,使用二进制模式打开Class文件,会发现所有Class文件的前四个字节均为OxCAFEBABE,这个魔术在Java还被称为"Oak"语言的时候就已经确定下来了
版本号紧接着魔术的四个字节(接下来不再对照二进制进行查看,而是直接查看javap帮我们解析出来的结果,见文章末尾)存储的是Class文件的版本号,前两个字节为次版本号(minor version),后两个字节为主版本号(major version)
Java版本号从45开始,高版本的JDK可以向下兼容低版本的Class文件,但无法向上兼容高版本,即使文件格式并未发生变化
常量池紧接着主次版本号之后的是常量池入口
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)
字面量:如文本字符串、被声明为final的常量值等
符号引用:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述(Descriptor)、方法的名称和描述符
Java代码在进行编译时,并不像C或C++那样有"连接"这一步骤,而是在虚拟机加载Class文件时进行动态连接,Class文件中不会保存各方法和字段的内存布局,在虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中,才能被虚拟机使用
访问标志访问标志用于识别一些类或接口层次的访问信息
访问标志用于识别这个Class是类还是接口;是否定义为public;是否为abstract类型;是否声明为final;等等,具体标志含义如下
标志 | 名称 |
---|---|
ACC_PUBLIC | 是否为public类型 |
ACC_FINAL | 是否被声明为final |
ACC_SUPER | 是否允许使用invokespecial字节码指令 |
ACC_INTERFACE | 是否为接口 |
ACC_ABSTRACT | 是否为abstract |
ACC_SYNTHETIC | 标识这个类并非由用户代码生成 |
ACC_ANNOTATION | 标识这是一个注解 |
ACC_ENUM | 标识这是一个枚举 |
Class文件中由类索引(this_class)、父类索引(super_class)及接口索引集合(interfaces)三项数据确定这个类的继承关系
父类索引只有一个(对应extends语句),而接口索引则是一个集合(对应implements语句)
字段表集合字段表(field_info)用于描述类或者接口中声明的变量
字段(field)包括了类级变量(如static)及实例级变量,但不包括在方法内部声明的变量
字段包含的信息有:作用域(public、private、protected)、类级还是实例级(static)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、数据类型、字段名等
描述符这里简单解释一下描述符(descriptor)
描述符用来描述字段数据类型、方法参数列表和返回值,根据描述符规则,基本数据类型及代表无返回值的void类型都用一个大写字符表示,对象类型则用字符L加对象全限定名来表示
标识字符 | 含义 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型,如 Ljava/lang/Object; |
对于数组,每一个维度使用一个前置的[来描述,如java.lang.String[][]将被记录为[[java/lang/String;,int[]将被记录为[I
描述方法时,按照先参数列表,后返回值的顺序描述,参数列表放在()内,如void inc()描述符为()V,方法java.lang.String toString()描述符为()Ljava/lang/String;,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex的描述符为([CII[CIII)I
方法表集合Class文件中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes),但是方法内部的代码并不在方法表中,而是经过编译器编译成字节码指令后,存放在属性表集合中一个名为"Code"的属性中
属性表集合在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息,Java虚拟机规范中预定义了9种虚拟机实现应当能识别的属性
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键自定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 原文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
关于属性表,会在之后的文章中穿插介绍
附:Class文件Classfile ~/articles/【修炼内功】跃迁之路/JVM/[JVM] 类文件结构/src/ClassStruct.class Last modified 2019-6-2; size 829 bytes MD5 checksum 9f7454acd0455837a33ff8e03edffdb3 Compiled from "ClassStruct.java" public class ClassStruct implements java.io.Serializable minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #14.#31 // java/lang/Object."":()V #2 = Fieldref #6.#32 // ClassStruct.name:Ljava/lang/String; #3 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream; #4 = Class #35 // java/lang/StringBuilder #5 = Methodref #4.#31 // java/lang/StringBuilder." ":()V #6 = Class #36 // ClassStruct #7 = String #37 // hello: #8 = Methodref #4.#38 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #9 = Methodref #4.#39 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #40.#41 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = String #42 // ManerFan #12 = Methodref #6.#43 // ClassStruct." ":(Ljava/lang/String;)V #13 = Methodref #6.#44 // ClassStruct.print:()V #14 = Class #45 // java/lang/Object #15 = Class #46 // java/io/Serializable #16 = Utf8 HELLO #17 = Utf8 Ljava/lang/String; #18 = Utf8 ConstantValue #19 = String #47 // hello #20 = Utf8 name #21 = Utf8 #22 = Utf8 (Ljava/lang/String;)V #23 = Utf8 Code #24 = Utf8 LineNumberTable #25 = Utf8 print #26 = Utf8 ()V #27 = Utf8 main #28 = Utf8 ([Ljava/lang/String;)V #29 = Utf8 SourceFile #30 = Utf8 ClassStruct.java #31 = NameAndType #21:#26 // " ":()V #32 = NameAndType #20:#17 // name:Ljava/lang/String; #33 = Class #48 // java/lang/System #34 = NameAndType #49:#50 // out:Ljava/io/PrintStream; #35 = Utf8 java/lang/StringBuilder #36 = Utf8 ClassStruct #37 = Utf8 hello: #38 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #39 = NameAndType #53:#54 // toString:()Ljava/lang/String; #40 = Class #55 // java/io/PrintStream #41 = NameAndType #56:#22 // println:(Ljava/lang/String;)V #42 = Utf8 ManerFan #43 = NameAndType #21:#22 // " ":(Ljava/lang/String;)V #44 = NameAndType #25:#26 // print:()V #45 = Utf8 java/lang/Object #46 = Utf8 java/io/Serializable #47 = Utf8 hello #48 = Utf8 java/lang/System #49 = Utf8 out #50 = Utf8 Ljava/io/PrintStream; #51 = Utf8 append #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #53 = Utf8 toString #54 = Utf8 ()Ljava/lang/String; #55 = Utf8 java/io/PrintStream #56 = Utf8 println { private static final java.lang.String HELLO; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: String hello private java.lang.String name; descriptor: Ljava/lang/String; flags: ACC_PRIVATE public ClassStruct(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: aload_0 5: aload_1 6: putfield #2 // Field name:Ljava/lang/String; 9: return LineNumberTable: line 7: 0 line 8: 4 line 9: 9 public void print(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #4 // class java/lang/StringBuilder 6: dup 7: invokespecial #5 // Method java/lang/StringBuilder." ":()V 10: ldc #7 // String hello: 12: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: getfield #2 // Field name:Ljava/lang/String; 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 12: 0 line 13: 28 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new #6 // class ClassStruct 3: dup 4: ldc #11 // String ManerFan 6: invokespecial #12 // Method " ":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: invokevirtual #13 // Method print:()V 14: return LineNumberTable: line 16: 0 line 17: 10 line 18: 14 } SourceFile: "ClassStruct.java"
参考:
深入理解Java虚拟机
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/74750.html
摘要:本文已收录修炼内功跃迁之路在诞生之初便提出,各提供商发布很多不同平台的虚拟机,这些虚拟机都可以载入并执行同平台无关的字节码。设计者在第一版虚拟机规范中便承诺,时至今日,商业机构和开源机构已在之外发展出一大批可以在上运行的语言,如等。 本文已收录【修炼内功】跃迁之路 Java在诞生之初便提出 Write Once, Run Anywhere,各提供商发布很多不同平台的虚拟机,这些虚拟机...
摘要:本文已收录修炼内功跃迁之路在浅谈虚拟机内存模型一文中有简单介绍过,虚拟机栈是线程私有的,每个方法在执行的同时都会创建一个栈帧,方法执行时栈帧入栈,方法结束时栈帧出栈,虚拟机中栈帧的入栈顺序就是方法的调用顺序写了很多文字,但都不尽如意,十分惭 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbtSi5?w=1654&h=96...
摘要:本文已收录修炼内功跃迁之路初次接触的时候感觉表达式很神奇表达式带来的编程新思路,但又总感觉它就是匿名类或者内部类的语法糖而已,只是语法上更为简洁罢了,如同以下的代码匿名类内部类编译后会产生三个文件虽然从使用效果来看,与匿名类或者内部类有相 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...
摘要:本文已收录修炼内功跃迁之路我们写的方法在被编译为文件后是如何被虚拟机执行的对于重写或者重载的方法,是在编译阶段就确定具体方法的么如果不是,虚拟机在运行时又是如何确定具体方法的方法调用不等于方法执行,一切方法调用在文件中都只是常量池中的符号引 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbuesq?w=2114&h=12...
摘要:也正是因此,一旦出现内存泄漏或溢出问题,如果不了解的内存管理原理,那么将会对问题的排查带来极大的困难。 本文已收录【修炼内功】跃迁之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不论做技术还是做业务,对于Java开发人员来讲,理解JVM各种原理的重要性不必再多言 对于C/C++而言,可以轻易地操作任意地址的...
阅读 1421·2021-11-15 11:38
阅读 3565·2021-11-09 09:47
阅读 1968·2021-09-27 13:36
阅读 3210·2021-09-22 15:17
阅读 2547·2021-09-13 10:27
阅读 2861·2019-08-30 15:44
阅读 1157·2019-08-27 10:53
阅读 2701·2019-08-26 14:00