class字节码结构(一)(字节码结构和字节常量池的结构)
《Java虚拟机原理图解》 1.1、class文件基本组织结构
关于变量的几个叫法:
局部变量/全局变量:很好区分根据所在位置。 类变量:静态的全局变量。 类常量:全局的final修饰的变量 静态常量:static final 的字段 常量:这个有几种说法: 1,final 修饰的变量 2,基本类型和字面值(比如变量,方法,类的名字)也被称为常量(一般在字节码)
简单来说:class文件结构是魔数、副版本号、主版本之后,常量池等:
magic:魔数,文件类型是字节码的一个数字标志,以便jvm识别。 minor_version:次版本号 major_version:主版本号(每个大的版本之间,jdk和jvm可能是不兼容的,以便jvm提前发现) constant_pool_count:常量计数器,常量的个数 constant_pool[constant_pool_count-1]:常量,该字节码中字面量和引用类型:基本类型和字符串的值;类,变量,方法,接口,属性的名字和类型 access_flags:表示类和接口访问权限和属性:比如public,final,super,enum,annotation,abstract this_class:标识当前字节码是哪个类,对应常量池的一个索引(指向具体的类名称和类型) super_class:是否有父类,用索引对应常量池中记录的类 interfaces_count:当前类有多少个接口 interfaces[interfaces_count-1]:继承了那些接口 fields_count:有多少个字段 fields[fields_count]:具体的字段 methods_count:有多少方法 methods[methods_count]:具体哪些方法 attrributes_count:有多少个属性(属性不是字段) attributes[attributes_count]:具体属性,(只认识有个内部类的属性,别的都没见过)
具体说下字节码常量池:
《Java虚拟机原理图解》 1.2.2、Class文件中的常量池详解(上)
《Java虚拟机原理图解》 1.2.3、Class文件中的常量池详解(下)
字面量:就是8种基本类型和字符串的值
符号引用:类名的全称也就包括类的包名,字段,方法的名字和类型的名字
注意,以上理解的片面的!不正确的。
字面量:存放的是确切内容的数据,包括了:基本类型(类型和值)和字符串的值,还包括了:类,方法,字段的名字和他们的描述符(类型)的文本表示。
符号引用:存放的是指向具体内容的索引和索引的索引,
包括了:指向字符串值,类名,方法名and类型,字段名and类型,的索引,还包括了指向这些索引的索引项。比如:字段的引用项,方法的引用项。
常量池区域的数据结构:计数器和常量项
常量池计数器(constant_pool_count),它记录着常量池的组成元素常量池项(cp_info)的个数。
常量项的结构:一个类型标志和具体内容
类型标志表:
常量项可以划分的种类:
具体怎么存储:
1,比如常量(常量是带有final的变量):
package com.louis.jvm; public class IntAndFloatTest { private final int a = 10; private final int b = 10; private float c = 11f; private float d = 11f; private float e = 11f; }
对应的字节码:可以直接看字节码,也可以通过javap -v IntAndFloatTest 指令
我们发现,对于数值即使定义了多个,常量池也只会存在一个。
特别注意的是:
int类型:
有final标识才会把放到常量池中。并且还会当成字段处理。
有static final 标识只会把值放到常量池中,不会当作字段处理
没有final修饰,只会当作字段处理。(即使字段赋值了,值也是不会放到常量池)
float类型:
却不需要final标识值也会放到常量池中。并且当作字段处理。
字段(字段的处理后面有具体介绍)和常量的区别:
字段:
只要没有 static和final同时修饰的变量都当字段处理。
常量:
被final修饰的变量。如果只是final修饰,那么还会当作字段处理。
被final和static同时修饰的变量才会只当作常量处理。
什么是字段处理和常量处理?
常量处理就是:变量和变量值,分别作为常量项存放。
字段处理就是:不管有没有值,都是不存放在常量池中。一个字段包括了多个常量项:名字项,类型项,名字和类型的索引项,字段引用项(索引的索引项)
例子:
public class TestClass{ int a=11; int aa=11; final int b=22; static final int c=33; float f=44f; String e="字符串变量"; String ee="字符串变量"; final String eee="常量字符串变量"; public static void main(String[] arg){
字节码如下:
Constant pool: #1 = Methodref #13.#40 // java/lang/Object."<init>":()V #2 = Fieldref #12.#41 // TestClass.a:I #3 = Fieldref #12.#42 // TestClass.aa:I #4 = Fieldref #12.#43 // TestClass.b:I #5 = Float 44.0f #6 = Fieldref #12.#44 // TestClass.f:F #7 = String #45 // 字符串变量 #8 = Fieldref #12.#46 // TestClass.e:Ljava/lang/String; #9 = Fieldref #12.#47 // TestClass.ee:Ljava/lang/String; #10 = String #48 // 常量字符串变量 #11 = Fieldref #12.#49 // TestClass.eee:Ljava/lang/String; #12 = Class #50 // TestClass #13 = Class #51 // java/lang/Object #14 = Utf8 a #15 = Utf8 I #16 = Utf8 aa #17 = Utf8 b #18 = Utf8 ConstantValue #19 = Integer 22 #20 = Utf8 c #21 = Integer 33 #22 = Utf8 f #23 = Utf8 F #24 = Utf8 e #25 = Utf8 Ljava/lang/String; #26 = Utf8 ee #27 = Utf8 eee #28 = Utf8 eeee #29 = String #52 // 静态常量字符串变量 #30 = Utf8 <init> #31 = Utf8 ()V #32 = Utf8 Code #33 = Utf8 LineNumberTable #34 = Utf8 main #35 = Utf8 ([Ljava/lang/String;)V #36 = Utf8 getA #37 = Utf8 ()I #38 = Utf8 SourceFile #39 = Utf8 TestClass.java #40 = NameAndType #30:#31 // "<init>":()V #41 = NameAndType #14:#15 // a:I #42 = NameAndType #16:#15 // aa:I #43 = NameAndType #17:#15 // b:I #44 = NameAndType #22:#23 // f:F #45 = Utf8 字符串变量 #46 = NameAndType #24:#25 // e:Ljava/lang/String; #47 = NameAndType #26:#25 // ee:Ljava/lang/String; #48 = Utf8 常量字符串变量 #49 = NameAndType #27:#25 // eee:Ljava/lang/String; #50 = Utf8 TestClass #51 = Utf8 java/lang/Object #52 = Utf8 静态常量字符串变量 {
2,字符串类型:
虽然多个字符串也只会存储一份,但是存储结构不同,会分成2个常量项来存:1,字符串类型标志和内容的索引;2,文本类型标志和文本内容。
字符也和常量一样分为3种情况(见上面字节码效果):没有final,有final,和final和static
package com.louis.jvm; public class StringTest { private String s1 = "JVM原理"; private String s2 = "JVM原理"; private String s3 = "JVM原理"; private String s4 = "JVM原理"; }
字节码的表示:
3,本类和使用到的类型是如何存储:和字符串类似,也许需要两个常量项,只是类型标志不同。
package com.jvm; import java.util.Date; public class ClassTest { private Date date =new Date(); }
字节码的表现形式:
补充一点就是:Object类型会被自动添加,因为所有的类是其子类。
3.1注意:类型只是被声明不会记录到常量池
public class Other{ private Date date; public Other() { Date da; }
Date类型是不会被记录的,需要改成这样才会:
public class Other{ public Other() { new Date(); }
字节码表现:
特别补充一点:对于文本内容的字符编码是可以设置(之前几个是ask码这个utf8)
4,对字段的存储:需要至少4个常量项(因为有对字段所在的类的指向,所在类的存储需要2个常量项)
CONSTANT_UTF8_info:字段的名字常量项;
CONSTANT_UTF8_info:字段的类型常量项(术语,不叫类型叫字段描述符!!!),
CONSTANT_Name_Type_info(名字和类型的索引):指向名字和类型的索引的常量项;
CONSTANT_Fieldref_info(字段的引用也是索引):指向所在类和【名字和类的索引常量项】的索引的常量项
例子:
package com.louis.jvm; public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } ....
整个结构:
字节码的结构表示:
4.1字段还有一个特别之处是,字段的类型(也就是字段描述符)的表示形式有自己的规则:
比如:
基本类型是首字母大写除了long和boolean. long是:J;boolean是:Z 引用类型要加"L"(字符串类型是:Ljava/lang/String); 数组类型要加“["(字符串数组是:[Ljava/lang/String);
4.2类中定义了field 字段,在类中的其他地方没用到这些字段,它是不会被编译器放到常量池中的。
比如:私有并且没有方法使用的情况
个人理解常量池对常量和字段的划分是:
字段是没有final修饰的。
常量是有final修饰的,
补充:少了字段的权限(权限不在常量池中)和字段的值(字段值也是不在常量池中)
5,方法的存储:和字段类似,并且方法还要被其他方法使用才会加入到常量池,区别是方法的描述符有多个(包括了参数类型和返回类型)
与字段的区别是:
1,方法引用的索引代替了字段引用的索引常量项。 2,方法描述符,多个是在一起的格式如:(Lxxx/String,Lxxx/String)Lxxx/String
5.1方法描述符的组成(方法的返回和参数类型):
5.2,字节码中的显示效果:
注意:
方法的描述符(类型)和字段的描述是一样的,但是格式有区别:参数在括号中返回类型在括号后 比如,参数是字符串返回类型是字符串:(Ljava/lang/string)Ljava/lang/string
如果没有参数和返回类型(用“v”表示void):()v
6,如何存类中使用到的接口中的方法,和类方法类似,只有一个常量项有区别:
接口方法引用(CONSTANT_InterfaceMethodref_info)替换了方法引用项
package com.louis.jvm; public interface Worker{ public void work(); } public class Boss { public void makeMoney(Worker worker) { worker.work(); } }
字节码显示:
7,java7中的常量项:CONSTANT_MethodType_info,CONSTANT_MethodHandle_info,CONSTANT_InvokeDynamic_info
这三项主要是为了让Java语言支持动态语言特性
总结:常量池的意义就是把那些经常使用到放到常量池。主要就是文本和值还有引用项(对应文本和值的索引)