本篇文章给大家带来的内容是关于java中jvm字节码的详细介绍,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
这是java基础篇(jvm)的文章,本来想先说说java类加载机制的,后来想想,jvm的作用是加载编译器编译好的字节码,并解释成机器码,那么首先应该了解字节码,然后再谈加载字节码的类加载机制似乎会好些,所以这篇改成详解字节码。
由于java纯面向对象的特性,字节码只要能表示一个类的信息,就可以表示整个java程序了,jvm只要能加载一个类的信息,就能加载整个程序了。所以,不管是字节码,还是jvm加载机制,关注点都是在类。我关注的点主要在于:
1. 由于字节码不是一次性全部加载进入内存,那么jvm是如何知道自己要加载的类信息在.class文件的哪个位置的?
2. 字节码是如何表示类信息的?
3. 字节码会进行程序的优化吗?
第一个问题很简单,因为哪怕一个源文件有很多个类(只有一个public类),编译器也会为其中每个类都生成一个.class文件,jvm加载时按照需要加载的类名称加载即可。
要解决后面的问题,首先我们来看字节码的组成(mac下用hex fiend打开)。
对这样一段代码:
package com.test.main1;public class bytecodetest { int num1 = 1; int num2 = 2; public int getadd() { return num1 + num2; }}class extend extends bytecodetest { public int getsubstract() { return num1 - num2; }}
我们来分析其中的extend类。
用hex fiend打开编译后的.class文件是这样的(16进制代码):
由于class文件没有分隔符,所以每个位置代表什么、各个部分的长度等格式是严格规定死的,见下表:
其中u1、u2、u4、u8代表几个字节的无符号数,在反编译出来的16进制文件中,两个数字代表一个字节,也就是u1。
从头到尾一项一项地看:
(1)magic:u4,魔数,代表本文件是.class文件。.jpg等也会有这种魔数,正因为魔数存在,即使将*.jpg改成*.123,也能照常打开。
(2)minor version、major version:各u2,版本号,向下兼容,即高版本jdk可以使用低版本的.class文件,反之不行。
(3)constant_pool_count:u2,常量池中常量的数量,0019代表有24个。
(4)接下来就是具体的常量,共constant_pool_count-1个。
常量池通常存两种类型的数据:
字面量:如字符串、final修饰的常量等;
符号引用:如类/接口的全限定名、方法的名称和描述、字段的名称和描述等。
根据反编译出来的数字,首先查下表得到该常量的类型和长度,接下来的与查得的长度相等的数字则表示该常量具体的值。
如070002,就表示该种类型为constant_class_info,它的tag为u1,且接下来u2长度为index指向全限定名常量项的索引。这个索引还要结合javap -verbose打开的class文件一起看,这里清晰地列出了常量池中的内容和顺序:
在这里可以看到0002索引项的常量为:com/test/main1/extend,是类的全限定名。如果是值是字符串,那么需要根据该值转换成十进制并查ascii码表得到具体的字符。接下来的常量都照此分析:
01001563 6f6d2f74 6573742f 6d61696e 312f4578 74656e64:com/test/main1/extend
070004:com/test/main1/bytecodetest
01001b63 6f6d2f74 6573742f 6d61696e 312f4279 7465436f 64655465 7374:com/test/main1/bytecodetest
0100063c 696e6974 3e:<init>
01000328 2956:()v
01000443 6f6465:code
0a000300 09:com/test/main1/bytecodetest、"<init>":()v
0c000500 06:<init>、()v
01000f4c 696e654e 756d6265 72546162 6c65:linenumbertable
0100124c 6f63616c 56617269 61626c65 5461626c 65:localvariabletable
01000474 686973:this
0100174c 636f6d2f 74657374 2f6d6169 6e312f45 7874656e 643b:lcom/test/main1/extend;
01000c67 65745375 62737472 616374:getsubstract
01000328 2949:()i
09000100 11:com/test/main1/extend、num1:i
0c001200 13:num1、i
0100046e 756d31:num1
01000149:i
09000100 15:com/test/main1/extend、num2:i
0c001600 13:num2、i
0100046e 756d32:num2
01000a53 6f757263 6546696c 65:sourcefile
01001142 79746543 6f646554 6573742e 6a617661:bytecodetest.java
至此,常量池中的常量全部解析完毕。
(5)再接下来是u2的access_flags:access_flags访问标志的主要目的是标记该类是类还是接口,如果是类,访问权限是否为public,是否是abstract,是否被标志为final等,见下表:
flag_name value interpretation
acc_public 0x0001 表示访问权限为public,可以从本包外访问
acc_final 0x0010 表示由final修饰,不允许有子类
acc_super 0x0020 较为特殊,表示动态绑定直接父类,见下面的解释
acc_interface 0x0200 表示接口,非类
acc_abstract 0x0400 表示抽象类,不能实例化
acc_synthetic 0x1000 表示由synthetic修饰,不在源代码中出现,见附录[2]
acc_annotation 0x2000 表示是annotation类型
acc_enum 0x4000 表示是枚举类型
所以,本类中的access_flags是0020,表示这个extend类调用父类的方法时,并非是编译时绑定,而是在运行时搜索类层次,找到最近的父类进行调用。这样可以保证调用的结果是一定是调用最近的父类,而不是编译时绑定的父类,保证结果的正确性。
(6)this_class:u2的类索引,用于确定类的全限定名。本类的this_class是0001,表示在常量池中#1索引,是com/test/main1/extend
(7)super_class:u2的父类索引,用于确定直接父类的全限定名。本类是0003,#3是com/test/main1/bytecodetest
(8)interfaces_count:u2,表示当前类实现的接口数量,注意是直接实现的接口数量。本类中是0000,表示没有实现接口。
(9)interfaces:表示接口的全限定名索引。每个接口u2,共interfaces_count个。本类为空。
(10)fields_count:u2,表示类变量和实例变量总的个数。本类中是0000,无。
(11)fields:fileds的长度为filed_info,filed_info是一个复合结构,组成如下:
filed_info: {u2access_flags; u2name_index;u2descriptor_index;u2attributes_count;attribute_info attributes[attributes_count];}
由于本类无类变量和实例变量,故本字段为空。
(12)methods_count:u2,表示方法个数。本类中是0002,表示有2个。
(13)methods:methods的长度为一个method_info结构:
method_info {u2access_flags;0000?u2name_index; 0005 <init>u2descriptor_index; 0006()vu2attributes_count; 00011个attribute_info attributes[attributes_count]; 0007code}
其中attribute_info结构如下:
attribute_info {u2attribute_name_index;0007codeu1attribute_length;u1info[attribute_length];}
上面是通用的attribute_info的定义,另外,jvm里预定义了几种attribute,code即是其中一种(注意,如果使用的是jvm预定义的attribute,则attribute_info的结构就按照预定义的来),其结构如下:
code_attribute {//code_attribute包含某个方法、实例初始化方法、类或接口初始化方法的java虚拟机指令及相关辅助信息u2attribute_name_index;0007 codeu4attribute_length;0000002f 47u2max_stack; 00011 //用来给出当前方法的操作数栈在方法执行的任何时间点的最大深度u2max_locals;0001 1 //用来给出分配在当前方法引用的局部变量表中的局部变量个数u4code_length; 000000055 //给出当前方法code[]数组的字节数u1code[code_length]; 2ab70008 b142、183、0、8、177 //给出了实现当前方法的java虚拟机代码的实际字节内容 (这些数字代码实际对应一些java虚拟机的指令)u2exception_table_lentgh; 0000 0 //异常的信息{u2start_pc; //这两项的值表明了异常处理器在code[]中的有效范围,即异常处理器x应满足:start_pc≤x≤end_pcu2end_pc; //start_pc必须在code[]中取值,end_pc要么在code[]中取值,要么等于code_length的值u2handler_pc; //表示一个异常处理器的起点u2catch_type; //表示当前异常处理器需要捕捉的异常类型。为0,则都调用该异常处理器,可用来实现finally。} exception_table[exception_table_lentgh];在本类中大括号里的结构为空u2attribute_count;00022表示该方法的其它附加属性,本类有1个attribute_infoattributes[attributes_count]; 000a、000b linenumbertable、localvariabletable}
linenumbertable和localvariabletable又是两个预定义的attribute,其结构如下:
linenumbertable_attribute { //被调试器用来确定源文件中由给定的行号所表示的内容,对应于java虚拟机code[]数组的哪部分u2attribute_name_index;000au4attribute_length; 00000006u2line_number_table_length; 0001{u2start_pc;0000 u2line_number; 000e //该值必须与源文件中对应的行号相匹配} line_number_table[line_number_table_length];}
以及:
localvariabletable_attribute {u2attribute_name_index;000bu4attribute_length; 0000000cu2local_variable_table_length; 0001{u2start_pc; 0000 u2length; 0005 u2name_index; 000c u2descriptor_index;000d//用来表示源程序中局部变量类型的字段描述符u2 index; 0000} local_variable_table[local_variable_table_length];
然后就是第二个方法,具体略过。
(14)attributes_count:u2,这里的attribute表示整个class文件的附加属性,和前面方法的attribute结构相同。本类中为0001。
(15)attributes:class文件附加属性,本类中为0017,指向常量池#17,为sourcefile,sourcefile的结构如下:
sourcefile_attribute {u2attribute_name_index;0017 sourcefileu4attribute_length; 00000002 2u2sourcefile_index; 0018 bytecodetest.java //表示本class文件是由bytecodetest.java编译来的}
嗯,字节码的内容大概就写这么多。可以看到通篇文章基本都是在分析字节码文件的16进制代码,所以可以这么说,字节码的核心在于其16进制代码,利用规范中的规则去解析这些代码,可以得出关于这个类的全部信息,包括:
1. 这个类的版本号;
2. 这个类的常量池大小,以及常量池中的常量;
3. 这个类的访问权限;
4. 这个类的全限定名、直接父类全限定名、类的直接实现的接口信息;
5. 这个类的类变量和实例变量的信息;
6. 这个类的方法信息;
7. 其它的这个类的附加信息,如来自哪个源文件等。
解析完字节码,回头再来看开始提出的问题,也就迎刃而解了。由于字节码文件格式严格按照规定,可以用来表示类的全部信息;字节码只是用来表示类信息的,不会进行程序的优化。
那么在编译期间,编译器会对程序进行优化吗?运行期间jvm会吗?什么时候进行的,按照什么原则呢?这个留作以后再表。
最后,值得注意的是,字节码不仅是平台无关的(任何平台生成的字节码都可以在任何的jre环境运行),还是语言无关的,不仅java可以生成字节码,其它语言如groovy、jython、scala等也能生成字节码,运行在jre环境中。
以上就是java中jvm字节码的详细介绍的详细内容。