could not initialize class什么意思_JVM执行子系统,一点一滴来解析.class文件

一、前言

笔者关于JVM的一共有四篇文章,前一篇讲述“JVM自动内存管理”,讲述JVM的底层结构,内存分配与内存回收。本篇讲述“JVM执行子系统”,本篇的全部目标是解析.class文件,读完本篇后,您会发现从.java文件到.class文件的映射,直至一个变量的定义,每一行代码,都是有矩可循的。

全文的结构是:第二部分,从Java两个无关性引入class文件,并对一个打印"hello world"字符串的程序的class文件进行分析,这里读者可能看不懂分析过程,没有关系,因为第二部分只是一个引子;第三部分,介绍JVM类加载机制(包括类加载概要、类加载明细);第四部分,介绍JVM执行引擎(包括介绍帧栈结构,方法调用,方法执行);第五部分,对上面示意的demo程序做“.java文件--.class文件”一一映射分析。

二、类文件结构(Class文件)

2.1 平台无关性与语言无关性

Java这里有两个无关性:平台无关性、语言无关性,是两个不同的东西,不要搞混了,虽然底层实现都是虚拟机和字节码存储格式.

(1)平台无关性(JVM可以运行在任何操作系统上)

Java最值得令人称道之处就是其“一次编写,到处运行(Write Once,Run Anywhere)”,这句宣传指的是Java平台无关性,值得注意的是,平台无关性并不是JVM所特有,而是所有虚拟机的诉求,很多其他虚拟机都在不断实现平台无关性。

平台无关性是指虚拟机(这里是JVM)可以运行在不同平台上,这些虚拟机都可以载入和执行同一种平台无关的字节码(byteCode),从而实现程序的“一次编写,到处运行(Write Once,Run Anywhere)”,由此可知,各种不同平台虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。

(2)语言无关性(JVM上可以运行任何语言)

语言无关性是JVM的又一特性,实际上,我们熟知的JVM不与任何语言关联(也不与Java语言相关),只与Class文件(.class格式文件)这种特定的文件格式关联,即其他任何语言编写的程序,只要经过对应的编译器生成class文件,都可以在JVM上运行,这就是JVM的语言无关性(实现这种语言无关性的基础也是虚拟机和字节码存储格式),如图所示:

f0d7a25d901cec936269c07fc3f44eac.png
JVM识别的是Class文件,而不是由Java编译生成的Class文件(其他语言写代码只要编译生成class文件也可以在JVM上运行,不一定要Java语言,JVM与Class文件绑定,不与Java语言绑定)

这里注意,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息,所以,基于安全考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构性约束(当然,这是后话,这里不讲述)。

小结:辨析平台无关性和语言无关性

定义(区别) 底层实现
平台无关性 平台无关性针对的对象是包括JVM在内的所有虚拟机,平台无关性是指虚拟机可以运行在不同平台上,且都可以载入和执行同一种平台无关的字节码,从而实现程序的“一次编写,到处运行(Write Once,Run Anywhere)”注意:我们平时说的“一次编译,到处运行”是指JVM平台无关性,不是其语言无关性,但是其语言无关性也是一个很重要的性质。 虚拟机和字节码存储格式
语言无关性 语言无关性针对的对象是JVM(局限于本文),是指JVM可以运行Class文件,而不在乎这个Class文件是由什么语言编译而成的。 虚拟机和字节码存储格式

无论是平台无关性,语言无关性,底层支持都是字节码存储,都是生成的class文件,所以,本文的全部精力和所有目的就是真正读懂class文件。我们先来看一个.class文件(打印hello world的class文件),不在于看懂,而是要熟悉class文件的结构(当第五部分才要求完全看懂)。

2.2 从.java文件到.class文件,手把手教你阅读.class文件(从十六进制到javap)

as we all know,计算机认识二进制0和1,那么如何让程序员书写的代码被计算机识别,这就需要一种中介转换机制,将代码按既定规则转换为计算机可以识别的0和1,这种中介转换机制就是虚拟机,我们这里就是Java虚拟机,而能够与Java虚拟机沟通,能够被Java虚拟机识别的就是Class文件。

对于java程序,.class文件是由.java文件编译生成,而.java文件就是程序员书写的代码,所以,这个帮助我们实现两个无关性的.class文件实际上就是根据程序的书写的代码编译生成的(不只是Java语言,其他语言也是这样)。

(1)十六进制阅读class文件

现在我们来解析.class结构文件,所以要辛苦读者阅读.class文件的二进制的格式了(我们用十六进制表示以方便阅读),有点像我们学计算机网络的时候阅读ip和tcp首部20字节,虽然.class文件不只20字节,但是阅读起来都是一样的。

我们使用最基本的命令行方式:我们写出一段打印hello world的程序,

public class Test {
    public static  void main(String[] args){
        System.out.println("hello world");
    }
}

这里笔者使用UltraEdit打开Test.class文件,可以用十六进制打开,如图,这是整个Test.class的二进制文件:

c35c1eb1a1619e1a7239137025e2901d.png

因为是十六进制数字,每一个十六进制数字是4位,则两个数字是8位,就是一个字节。

如上图所示,前面四个字节(序号为 0 1 2 3)是magic number,为魔法数字,固定位CAFEBABE,

第5-8个字节(序号为 4 5 6 7)存放是的Test.class文件的版本号,其中,第5、6个字节是次版本号Minor Version,第7、8个字节是主版本号Major Version,这里次版本号为0,主版本号为0x0034,换为十进制是52,根据版本对应关系,这里使用的是jdk8,正是如此。

接下来的两个字节(序号为8 9)表示常量区容量,是0x0022,十进制是34,表示常量区容量是34-1=33,这是因为常量区序号从1开始,所以表示常量区有33个常量,分别是1-33,当这个数字为0x0000时,表示“不引用任何一个常量池项目”。

(2)class文件解析工具——javap助力,再也不用阅读16进制

接下来我们来分析这33个常量,用十六进制太麻烦了,使用jdk自动的javap.exe (阅读起来就像是汇编程序或者那种编译原理的味道),如图:

dfa5b70a8296260bacc039930a7d5881.png

现在我们从上到下,一步步解释上图:

首先,minor version次版本号为0,major version主版本号为52;
Constant pool 常量池:
第一个常量:Methodref表示类中方法的符号引用,关于对 java/lang/Object."<init>":()V的理解:
java.lang.Object表示命名空间,init为方法名,表示该命名空间中的方法,:后面表示方法签名(参数列表+返回值),()表示参数为空,V表示void,返回值为空。所以,整句的意思是:调用 java.lang.Object 的 init 方法。
第二个常量:Fieldref表示字段的符号引用,关于对 java/lang/System.out:Ljava/io/PrintStream的理解:
java.lang.System.out表示命名空间,这个字段就是java.io.PrintStream,即打印流字段。
第三个常量:String表示字符串变量,hello world即是该字符串
第四个常量:Methodref 类中方法的符号引用,关于对 java/io/PrintStream.println:(Ljava/lang/String;)V的理解:
java.io.PrintStream表示命名空间,println表示方法名,:后面表示参数和返回值,Ljava.lang.String表示实际参数,V表示void,返回值为空,所以整句的意思是:调用java.io.PrintStream打印流里面的println()方法,参入的实参是一个String字符串,返回值为空。
第五个常量:Class表示类或接口的符号引用, mypackage/Test 表示类的全限定性路径,包名+类名,这里是mypackage.Test类。
第六个变量:Class表示类或接口的符号引用, java/lang/Object 表示类的全限度性路径,包名+类名,这里是java.lang.Object类。
注意:以第一个常量为例,后面有#6.#20,这是什么意思呢,其实意思就是转到序号为6、20的常量,先看#6,#6的值是#27,所以定位到27,值为java.lang.Object,另一方面再看#20,#20值为#7 #8,#7值为<init> #8值为()V,其实,通过这种查看方式查看到的结果和直接看 // 注释后面的内容是一样的,所以刚才笔者解释Constant pool 常量池的时候,是直接拿后面的注释解释的。

8258c87945f3b2b0344ca42398044629.png

这里分为两段,一段是public mypackage.Test(); 第二段是 public static void main(java.lang.String[]);其实就是两个方法,咦,这个程序中明明只有一个main方法,为什么现在有两个方法呢,且看Test.class:

f569b34bd0e9a9d16fed3c5950b9a18c.png

原来,Test类有一个默认无参构造函数,加上main函数就两个函数了,这就是为什么有两个函数的原因,读者记住了,后面还会出现这个默认无参构造,这里解释了,后面就不再解释了。

先看第一个方法,Test默认无参构造函数,public mypackage.Test();

descriptor:表示该方法的描述(实参+返回值),这里 ()V 表示实参为空,返回值为空;

flags:表示访问标记,和java程序中关键字对应,ACC_PUBLIC对应public关键字,表示mypackage.Test()是公共方法;
Code:表示是代码段,

stack=1, locals=1, args_size=1 表示操作数栈数目为1,本地变量表容量为1(表示局部变量表中只有一个变量,这个变量的的具体信息是下面的LocalVariableTable:清单),参数数目为1(注意:这里看不懂操作数栈和本地变量表不要紧,本文后面都有介绍)

0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

这三句中 0: 1: 4:表示的都是地址偏移量,冒号后面表示的是助记符,#后面表示的常量池序号(这里是1),//后面表示的是注释,现在来一个个解释助记符。

aload_0的解释:分为三部分,a表示类型,为this,当前类对象,load表示操作数栈入栈操作,0表示slot序号,整句意思是将局部变量表第0个slot中的this对象复制到操作数栈顶。

关于阅读class文件中的load操作 store操作(注意:单独的load store 不会在.class文件中,出现在class文件中的一般是 “类型简称+load+_+slot序号”)
load操作:将局部变量表中指定的某个元素复制到操作数栈栈顶,即操作数栈入栈操作,操作数栈元素个数+1,局部变量表元素个数不变。
store操作:将操作数栈栈顶元素出栈并存放入指定的局部变量slot中,局部变量表要记录,即操作数栈出栈操作,操作数栈元素个数-1,局部变量表中元素个数+1。
aload_0:a表示类型,为引用类型referenceType,load表示操作数栈入栈操作,0表示slot序号,整句意思是将局部变量表第0个slot中的引用类型值复制到操作数栈顶。
iload_0:i表示类型,为基本类型int,load表示操作数栈入栈操作,0表示slot序号,整句意思是将局部变量表第0个slot中int整型值复制到操作数栈顶。
lload_1:l表示类型,为基本类型long,load表示操作数栈入栈操作,1表示slot序号,整句意思是将局部变量表第1个slot中的long长整型值复制到操作数栈顶。
fload_2:f表示类型,为基本类型float,load表示操作数栈入栈操作,2表示slot序号,整句意思是将局部变量表第2个slot中的float单精度浮点型值复制到操作数栈顶。
dload_3:d表示类型,为基本类型double,load表示操作数栈入栈操作,3表示slot序号,整句意思是将局部变量表第3个slot中的double双精度浮点型值复制到操作数栈顶。
astore_0:a表示类型,为引用类型referenceType或返回地址returnAddress,store表示操作数栈出栈操作,0表示序号,整句意思是将操作栈栈顶的引用类型值出栈并存放到第0个局部变量Slot中。
istore_0:i表示类型,为int整型类型,store表示操作数栈出栈操作,0表示序号,整句意思是将操作栈栈顶的int整型值出栈并存放到第0个局部变量Slot中。
lstore_1:l表示类型,为long长整型类型,store表示操作数栈出栈操作,1表示序号,整句意思是将操作栈栈顶的long长整型值出栈并存放到第1个局部变量Slot中。
fstore_2:f表示类型,为float单精度浮点型类型,store表示操作数栈出栈操作,2表示序号,整句意思是将操作栈栈顶的float单精度浮点型值出栈并存放到第2个局部变量Slot中。
dstore_3:d表示类型,为double双精度浮点型类型,store表示操作数栈出栈操作,3表示序号,整句意思是将操作栈栈顶的double双精度浮点型值出栈并存放到第3个局部变量Slot中。

invokespecial:这是一条方法调用字节码,调用构造函数、私有方法、父类方法都是用invokespecial,这里调用构造函数,所以使用invokespecial

return:表示构造器返回,因为构造函数时没有返回值的,所有只要一个return。(如果返回一个整型,为ireturn,返回一个长整型,为ireturn,不同的jdk可能稍微有一点不一样,但描述的意思是一样的,读者理解就好)

LineNumberTable: 表示行号表,存放方法的行号信息,就是 line 3: 0 表示的意思是.java代码中第三行对应的是Code代码块中偏移地址为0这句。

LocalVariableTable:表示局部变量表,存放方法的局部变量信息(方法参数+方法内定义的局部变量)

Start Length Slot Name Signature
0 5 0 this LTest;

这里的局部变量表只有一行数据,表示只有一个变量,这和上面的locals=1(局部变量表容量为1)是对应的。

Start 这里为0,一般都是0开始的,略;

Length 这里5表示长度,即局部变量表的长度;

Slot 表示变量槽,局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间,这里的0表示的是序号,第0个Slot;

Name 表示局部变量名,这是是this;

Signature 表示局部变量类型,L前缀表示对象,这里表示的是Test类对象。

这里有一个小疑问,LocalVariableTable是局部变量表,里面是存放变量的,是存放一个方法(这里是Test类默认无参构造函数)的参数与方法内定义的局部变量的,但是这个Test默认无参构造函数中,既没有参数,里面又没有定义局部变量,为什么生成的class文件中,这个方法里面还有一个类型为mypacka
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值