一、jvm调优学习记录之.class文件篇
最近由于工作上的问题,对
java
虚拟机产生了浓厚的兴趣,这里做下学习记录,欢迎大佬们的指正。
一、jvm含义解读
jvm
,Java Virtual Machine即java
虚拟机。jvm
与java
语言其实没有任何关系,这是很多人的误区。他只是一种规范,是虚构出来包含字节码指令集和内存管理(堆、栈、方法区等)的计算机。
.java
文件通过javac
编译为.class
文件后的类加载过程在jvm
中进行。所以,任何语言,只要是通过了编译,成为.class
文件,就可进入jvm
中执行。
如图所示,本文将对前期编译出的.class文件进行分析。
二、.class文件分析
class文件从本质上来说,是二进制字节流。但是计算机是无法直接读取执行二进制串的,所以jvm应运而生解决了这个问题,它可以把Class文件的二进制翻译成计算机能够读懂的形式。
1、class基本信息
我这里是在idea中下载了BinEd插件,用以观察class文件的二进制源码。
新建一个空类
public class ByteCode01 {
}
打开可以看到如下十六进制码,各位都分别对应了class文件中的版本号、常量池长度、常量池、接口等。
其中,CA FE BA BE
为16进制八位一字节的4字节magic_number
,它表明了这个文件是一个符合class文件定义规范的文件。
之后,000行04-05位的两字节为MinorVersion
,是小版本号,如:xx.0这里的0即为小版本号。06-07位的两字节为MajorVersion,大版本号,如:8.x这里的8即为大版本号。所以图中的小版本号为0,大版本号十六进制0034换算成十进制也就是52,也就是说我们的版本号为52.0,也就是对应JDK1.8。打开class文件,如图:
再往后看,08-09位的两字节为Constant_Pool_Length
,常量池长度。当前的常量池长度为00 10
,换算成十进制就是16,但实际只保存15个对象,因其会保留0位存储索引指向。
常量池长度设置了,接下来当然就是常量池里的对象了。在idea
中下载了jclasslib
插件,观察class文件bytecode结构信息。ByteCode01
打开,可以看如下图所示:
该工具将一般信息做了相对javap的可视化展示,版本号、常量池计数等都进行了相对可视化的展示。其中,访问标志(access_flags)为0x0021,它代表什么呢?这里有一个access_flags的位运算对应关系图。
那么,0x0021也就是0x0001与0x0020,这样子的表示方式就使得它可以仅用两个字节就将很多内容进行表述。
2、常量池
class文件最重要的常量池环节来了,把常量池弄明白基本也就没什么问题了。常量池仅仅是常量类型就有很多,下图其中部分类型展示:
中间缺失tag2,是为标记位,常量池的每一个类型都会有一个1字节的标记。那么,怎么看呢?我们拿Methodre
f举例,tag:10为该类型的标记,index为指向声明方法的类或者接口描述符CONSTANT_Class_info
的索引项,占两字节,另一index
指向描述符索引在tag[12]中,图中未给出,占两字符。
回到本文的空类中,点开常量池[01],如下图:
类名(即第一个index
)指向tag[3],名称和描述符(即第二个index)指向tag[13],分别如下如所示
可以看到,[03]中类指向了[15],而[15]为Utf-8字符串,即该声明方法的类名为Object
类。而[13]指向了[4]、[5],即该方法的类型为init
(构造)方法,且()表示无参数,v表示返回值是void
,也就意味着这是一个Object
类下的默认无参构造方法,也就是说我们在创建一个类时,若类内没有构造方法,编译自动填加默认无参构造方法。回到class文件的二进制里去看,如下图
000行的0A-0E
为常量池中的第一个元素,0A位为其标志位,转为十进制则为10,10对应的则是CONSTANT_Methodref_info
,0B-0C
则是它的第一个索引0003
,0C-0D
则是第二个索引000D
,也就是十进制3和13。 回到常量池,继续往下看
[4]为Utf-8字符串,表示[3]方法名称为init。[5]为方法的描述符,()V为其书写规范,下图为规范举例及规范对应
再之后的[6]、[7]、[8]分别为类方法与类属性的属性名称,其余也均是字符串,不一一赘述了。
接下来则是方法实现部分,可以看到是属性名称,然后下面是字节码
aload_0
,感兴趣的小伙伴,可以去下载JAVA虚拟规范,查询java汇编指令aload_0
那么aload_0
是做什么的,点击aload_0
查看jvm
规范,可以看到如下,大概意思为将索引为0(基本为this)的局部变量压到操作数栈中。
变量放到栈中后,执行第二条指令invokespecial
,调用其实例初始化方法即构造方法。
第三步,return
。再回到二进制中去看该方法实现部分,aload_0
对应十六进制0A
,0A
之后则是B7
,看到上图JVM
规范,也就是invokespecial
。
执行完invokespecial
,则是return(方法结束),查看JVM
规范
B1
在120行,00 列,那前两个字节里的00
、01
是什么呢?回到invokespecial
规范里可以看到,是它带了两个参数,分别指向常量池中的位置,01
也就是一开始分析的Object
构造方法。所以,2a-b1
即为一个构造方法的具体实现。实现流程:虚拟机读到2a
开始对this
进行压栈,b7知道调用构造方法,00
、01为其参数指向java.lang.Object
,即为Object
类构造方法,return
方法结束。