JVM 类加载与类加载器
类加载
类的生命周期是由7个阶段组成,如图
首先类的加载阶段
1.通过类的全限命名获取储存该类的class文件(获取方式)
2.解析成运行时数据,即instanceKlass 实例,存放在方法区
3.在堆区生成该类的class对象,即instanceMirrorKlass实例
何时加载
-主动使用时
1、new、getstatic、putstatic、invokestatic
2、反射
3、初始化一个类的子类会去加载其父类
4、启动类(main函数所在类)
5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
-预加载:包装类,String,Thread
获取方式 比如
1、从压缩包中读取,如jar、war
2、从网络中获取,如Web Applet
3、动态生成,如动态代理、CGLIB
4、由其他文件生成,如JSP
5、从数据库读取
6、从加密文件中读取
验证阶段
1、文件格式验证
2、元数据验证
3、字节码验证
4、符号引用验证
准备阶段
为静态变量分配内存并赋初值:比如 int = 0;long = 0L;
实例变量时在创建对象的时候完成赋值的,并没有赋初值的说法
如果变量被final修饰了,那么在编译的时候会给属性添加 constantValue 属性,准备阶段(编译完)直接完成赋值
public static int a = 20;
public static final int b = 30;
public static void main(String[] args) {
//不运行直接编译
}
解析阶段
通俗点就是说 间接引用->直接引用
间接引用:指向运行时常量池的引用
直接引用:内存地址
还是用这个Test对象,我们分别来看看编译后和运行中的常量池信息
public static int a = 20;
public static final int b = 30;
public static void main(String[] args) {
for (;;);//运行中
}
1.编译后的 切换到classes目录下使用javap -verbose 或者 jclasslib
比如我的是 D:\spring-boot\boot\target\classes>javap -verbose com.waf.boot.wk.Test 得到的结果是常量池的引用
javap -verbose
D:\spring-boot\boot\target\classes>javap -verbose com.waf.boot.wk.Test
Classfile /D:/spring-boot/boot/target/classes/com/waf/boot/wk/Test.class
Last modified 2021-3-24; size 566 bytes
MD5 checksum d7655afea6f7cf4d8b5ab6005cd334ab
Compiled from "Test.java"
public class com.waf.boot.wk.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#27 // com/waf/boot/wk/Test.a:I
#3 = Class #28 // com/waf/boot/wk/Test
#4 = Class #29 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 b
#8 = Utf8 ConstantValue
#9 = Integer 30
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/waf/boot/wk/Test;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 StackMapTable
#22 = Utf8 MethodParameters
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #5:#6 // a:I
#28 = Utf8 com/waf/boot/wk/Test
#29 = Utf8 java/lang/Object
{
public static int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int b;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 30
public com.waf.boot.wk.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/waf/boot/wk/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: goto 0
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 1
frame_type = 0 /* same */
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 20
2: putstatic #2 // Field a:I
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "Test.java"
主要看 Constant pool:
#3 = Class #28 // com/waf/boot/wk/Test
jclasslib 得到的也是常量池的引用
2.运行后的通过进程号在HSDB里面看 得到的是真正的内存地址
初始化阶段
执行静态代码块,完成静态变量的赋值
静态字段、静态代码段,字节码层面会生成clinit方法
方法中语句的先后顺序与代码的编写顺序相关
我们可以看看static的几个例子
public class Test_1 {
public static void main(String[] args) {
System.out.println(Test_1_B.str);
}
}
class Test_1_A{
static {
System.out.println("a static");
}
}
class Test_1_B extends Test_1_A{
public static String str = "a str";
static {
System.out.println("b static");
}
}
//output
a static
b static
a str
public class Test_1 {
public static void main(String[] args) {
System.out.println(Test_1_B.str);
}
}
class Test_1_A{
public static String str = "a str";
static {
System.out.println("a static");
}
}
class Test_1_B extends Test_1_A{
static {
System.out.println("b static");
}
}
//output
a static
a str
这个静态字段在klass中是怎么存储的呢--通过HSDB工具看第二个例子中Test_1_A & Test_1_B
存储在 instanceMirrorKlass(Oop) 中 而且 Test_1_B 是没有 str 的属性(只存在Java语法的继承关系)那底层是怎么找到的呢(没搞懂....)
public class Test_1 {
public static void main(String[] args) {
System.out.println(new Test_1_B().str);
}
}
class Test_1_A{
public String str = "a str";
static {
System.out.println("a static");
}
}
class Test_1_B extends Test_1_A{
static {
System.out.println("b static");
}
}
//output
a static
b static
a str
JVM读取静态属性的方式(例子二)(那底层是怎么找到的呢(没搞懂....))
str是类Test_1_A的静态属性,可以看到不会存储到子类Test_1_B的镜像类中
可以猜得到,通过子类Test_1_B访问父类Test_1_A的静态字段有两种实现方式:
1、先去Test_1_B的镜像类中去取,如果有直接返回;如果没有,会沿着继承链将请求往上抛。很明显,这种算法的性能随继承链的death而上升,算法复杂度为O(n)
2、借助另外的数据结构实现,使用K-V的格式存储,查询性能为O(1)
Hotspot就是使用的第二种方式,借助另外的数据结构ConstantPoolCache,常量池类ConstantPool中有个属性_cache指向了这个结构。每一条数据对应一个类ConstantPoolCacheEntry。
ConstantPoolCacheEntry在哪呢?在ConstantPoolCache对象后面,看代码C++
\openjdk\hotspot\src\share\vm\oops\cpCache.hpp
ConstantPoolCacheEntry* base() const { return (ConstantPoolCacheEntry*)((address)this + in_bytes(base_offset())); }
这个公式的意思是ConstantPoolCache对象的地址加上ConstantPoolCache对象的内存大小
ConstantPoolCache
常量池缓存是为常量池预留的运行时数据结构。保存所有字段访问和调用字节码的解释器运行时信息。缓存是在类被积极使用之前创建和初始化的。每个缓存项在解析时被填充
如何读取
\openjdk\hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp
CASE(_getstatic): { u2 index; ConstantPoolCacheEntry* cache; index = Bytes::get_native_u2(pc+1); // QQQ Need to make this as inlined as possible. Probably need to // split all the bytecode cases out so c++ compiler has a chance // for constant prop to fold everything possible away. cache = cp->entry_at(index); if (!cache->is_resolved((Bytecodes::Code)opcode)) { CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode), handle_exception); cache = cp->entry_at(index); } ……
从代码中可以看出,是直接去获取ConstantPoolCacheEntry
类加载器