摘要: java定义了一套与操作系统,硬件无关的字节码格局,这个字节码就是用java class文件来示意的,java class文件外部定义了虚拟机能够辨认的字节码格局,这个格局是平台无关性的。
本文分享自华为云社区《java之深刻class文件》,原文作者:技术火炬手。
java语言是跨平台的,所谓一次编写,到处运行。之所以是跨平台的,就是java定义了一套与操作系统,硬件无关的字节码格局,这个字节码就是用java class文件来示意的,java class文件外部定义了虚拟机能够辨认的字节码格局,这个格局是平台无关性的,在linux零碎或者在windows零碎上都是统一的。这个就好比html文件,咱们定义好标准,这个零碎只有去依照标准显示进去外面的内容就好了。
一.JVM的语言无关性
JVM是干什么用的?
运行java的啊,难不成是运行python的?
这句话是对的,但不残缺,JVM并不是只能运行java程序。
事实上,JVM上运行的自身也不是java文件,而是class文件。
而可能编译转化为class文件的,并不只有java一种。
这就是JVM的语言无关性。
至于能不能运行python,取决于是否有一个能将python转成class文件的工具。
当然这样做没有太多的意义,毕竟python也有其运行环境,且在某种意义上,比java更弱小,外围类库更欠缺。
各种语言也有各自的平台,所以没有必要强制编译。
但把握class文件还是很有意义的。
作为一个程序员,你是否有过或者已经有过创立一门语言的奢望?最好还是用汉语开发。
但事实,或者大学里的某个导师,却给你兜头一盆冷水。
先花个三五年钻研汇编,再思考实现这些。
三五年,黄花菜都凉了。
当初,有了JVM,仿佛看到了一点心愿的曙光。
二.class文件的实质
要实现之前的构想,或者说,想开发一个编译工具。首先要做的,就是要解构class文件自身。
无论如何得来,class文件的实质都是一组以 8 位字节为根底单位的2进制流。
记住,是2进制。
为了证实这一点,咱们还是要用到一些工具。比方,Sublime。
它并不是一个间接查看2进制的工具,而是16 进制的编辑器(2进制和16进制能够无缝切换)。
这外面仿佛还有python的事件哦。应用时,间接点击sublime_text.exe文件即可。
而后抉择class文件,关上,如下图的样子。
看的人眼花对不对?这都什么玩意!
前文说了,2进制,不,这就是16进制啊。
如果你不想去看16进制,也能够应用javap,间接去查看字节码指令(具体内容见前文《一段java代码是如何执行的》)。
如果你也不想关上命令行,还有一个叫jclasslib的工具,可提供图形化界面,它还有实用于idea的插件。
但它不是重点,暂且疏忽。
三.class文件构造揭秘
class文件格式中只有两种数据类型,无符号数和表。
其中,无符号数蕴含所有的根底数据类型和字符串,索引援用等,依据字节长度又能够分为u1,u2,u4,u8,别离代表无符号数的长度为1,2,4,8。
而表,即对象类型。
接下来,以sublime文件解析的内容为底本,按程序说说的class文件的形成。
(1)class文件的头四个字节被称为魔数,它的作用是确定这个文件是否为一个能被虚拟机承受的 Class 文件。
如,上文中魔数的值为:
它代表该文件是一个class类型的文件,不信,你能够多关上几个class文件看看。
(2)接下来的四个字节代表jdk的版本
如上的内容代表jdk的版本为1.8。
PS:jdk1.1的版本数字为45,当前每跨一个大版本,数字+1,所以jdk1.8的版本数字为51(十进制),转化为16进制即为34。
(3)上面一个概念是常量池
以上内容是常量池的计数器,通过该数字,咱们计算出常量的个数为15个(计算出的数字减1,因为该计数器的起始数不是0,而是1)
咱们用javap命令关上常量池,证实常量确实是15个。
(4)常量池前面就是拜访标记,拜访标记次要分为如下类别
咱们回头去看看这段class的源码(竟然如此简略)
Java 代码
public class ByteCode { public ByteCode(){ }}
该类非接口,非抽象类,非枚举,非零碎代码,非final,有pulbic,且编译器在jdk1.2之后,所以,满足条件的标记为:
ACC_PUBLIC和 ACC_SUPER,对应标记数为0001和0020,合并起来就是0021。如下图地位:
(5)类索引,父类索引和接口索引
- 上文拜访标记前面就是类索引,索引值为0002,对应常量池第二位。
- 类索引前面就是夫类索引,索引值为0003,对应常量池第三位。
- 父类索引前面就是接口索引,索引值为0000,代表该类没有实现任何接口。
(6)字段表,办法表,属性表
三大索引之后就是字段表
字段表为0000,代表无字段。
如上图,办法表分为四局部
- 办法表计数器的后果为1,代表有一个字段
- 办法表拜访标记为0001,代表public
- 办法表名称索引为0004,对应常量池第4个
- 办法表形容索引为0005,对应常量池第5个
属性表以此类推。
四.字节码指令
独自开一个章节讲讲字节码指令,它存在于办法表中,如下分类:
(1)加载和存储指令
此局部内容,见前文《一段java代码是如何执行的》)
(2)运算或算术指令
源码:
Java 代码
public class Test { public void add(int a,int b){ System.out.println(a+b); System.out.println(a-b); System.out.println(a*b); System.out.println(a/b); }}
字节码指令如下:
(3)类型转换指令
源码:
Java 代码
public class Test { public void add(int a,int b){ int c = 1; long d = c; }}
字节码指令:
(4)创立实例指令
这个不必多讲,就是new
(5)创立数组指令
源码:
Java 代码
public class Test { public void add(int a,int b){ int[] c = new int[4]; String[] d = new String[5]; }}
字节码指令:
(6)拜访字段指令
源码:
Java 代码
public class Test { private static String name = "1"; private String age = "2"; public static void main(String[] args) { Test test = new Test(); String a = test.age; String b = Test.name; }}
字节码指令:
(7)数组存取指令
源码:
Java 代码
public static void main(String[] args) { String[] a = new String[5]; a[1] = "2"; String b = a[1];}
字节码指令:
(8)查看实例类型指令
就是instanceof,演示略
(9)办法返回指令
就是return,演示略
五.异样操作
间接看一段代码:
Java 代码
public class Test { public void test() { InputStream in = null; try { in = new FileInputStream("i.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); }finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } }}
代码是一段典型的文件流操作,与其余代码不同的是,它捕捉了两个异样。
那么,字节码指令又是如何解决该异样的呢
咱们能够看到,最底下呈现了一个exception table,即异样表,它记录了所有的异样数据
以异样表第一行举例,from,to别离代表,如果第12行,到第16行间产生异样,则间接跳到第19行(target)。
六.装箱拆箱
这是绕不过来的一个话题。
凡是有一点java根底的人都晓得,java有八大根底数据类型,每一种类型都对应一种包装类。如int之于Integer,long之于Long。
一般来讲,根底数据类型和包装类都能够互相赋值。但这其中的逻辑如何呢?
Java 代码
public class Test { public static void main(String[] args) { Integer i = 1; int a = 2; int b = 3; i = a; b = i; }}
咱们来看看字节码指令
从字节码指令中,咱们能够看到,有三次拆装操作
- 第一次,调用Integer的valueOf办法,讲常量1转为Integer类型;
- 第二次,调用Integer的valueOf办法,讲栈顶值2转为Integer类型;
- 第三次,调用intValue办法,讲Integer转为int,而后赋值给b。
前两部为装箱,后一步为拆箱。
这就是拆装箱的底层实现逻辑了。
点击关注,第一工夫理解华为云陈腐技术~