JVM学习笔记 04、类加载与字节码技术(上)
@[toc]
前言
本篇博客是跟随黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓的学习JVM的笔记,若文章中出现相关问题,请指出!
所有博客文件目录索引:博客目录索引(持续更新)
一、类加载
1.1、java文件、字节码文件
分别为Java文件、字节码文件、反编译文件
package com.changlu;
public class Main {
public static void main(String[] args) {
System.out.println("hello,world");
}
}
cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 124c 636f 6d2f 6368
616e 676c 752f 4d61 696e 3b01 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0004 6172
6773 0100 135b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 0100 0a53 6f75 7263
6546 696c 6501 0009 4d61 696e 2e6a 6176
610c 0007 0008 0700 1c0c 001d 001e 0100
0b68 656c 6c6f 2c77 6f72 6c64 0700 1f0c
0020 0021 0100 1063 6f6d 2f63 6861 6e67
6c75 2f4d 6169 6e01 0010 6a61 7661 2f6c
616e 672f 4f62 6a65 6374 0100 106a 6176
612f 6c61 6e67 2f53 7973 7465 6d01 0003
6f75 7401 0015 4c6a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 3b01 0013 6a61
7661 2f69 6f2f 5072 696e 7453 7472 6561
6d01 0007 7072 696e 746c 6e01 0015 284c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b29 5600 2100 0500 0600 0000 0000 0200
0100 0700 0800 0100 0900 0000 2f00 0100
0100 0000 052a b700 01b1 0000 0002 000a
0000 0006 0001 0000 0009 000b 0000 000c
0001 0000 0005 000c 000d 0000 0009 000e
000f 0001 0009 0000 0037 0002 0001 0000
0009 b200 0212 03b6 0004 b100 0000 0200
0a00 0000 0a00 0200 0000 0b00 0800 0c00
0b00 0000 0c00 0100 0000 0900 1000 1100
0000 0100 1200 0000 0200 13
# java -v xxx.class
D:\workspace\workspace_idea\mavenexer\target\classes\com\changlu>javap -v Main.class
Classfile /D:/workspace/workspace_idea/mavenexer/target/classes/com/changlu/Main.class
Last modified 2021年11月29日; size 539 bytes
MD5 checksum 03f2133b550ae36e5c91e58698d6a7c7
Compiled from "Main.java"
public class com.changlu.Main
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // com/changlu/Main
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello,world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/changlu/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/changlu/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello,world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/changlu/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.changlu.Main();
descriptor: ()V
flags: (0x0001) 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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/changlu/Main;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello,world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Main.java"
1.2、类加载与字节码技术
1、类文件结构:整个类文件十六进制表示,分别来表示对应部分内容。
- 是什么?类的文件十六进制形式。意义?通过类文件我们就可以看出类的原始内容信息,若是再懂一点对应的进制表示的含义,我们就可以直接来对字节码文件进行修改来改变类文件的内容!
2、字节码指令:研究字节码分别表示的含义?针对于特定字节码文件!
- 通过编译器将.java文件编译成.class文件,最终通过jvm来进行转换对应平台的机器指令,最终执行!
- 字节码文件作为中间码存在的二进制文件,默认我们文件中打开字节码是十六进制其对应标识常量、指令、引用的序列。机器码和字节码的概念与区别
- 使用javap能够更好的帮助我们来查看字节码文件!
javap -v 类.class
,-v表示输出详细信息。
操作数栈默认是4个字节
java方法执行过程:①常量池载入运行时常量池。②方法字节码载入方法区。③main线程开始运行分配帧栈内存。④最终执行引擎开始执行字节码!
示例:
a++:是先iload,再iinc。++a:是先iinc,再iload
int a = 10;
int b = a++ + ++a + a--;
//a=>11,b=>34
帧栈存储a变量,另外一边是方法区来执行字节码命令:
- 对应a++,首先加载10进来,接着帧栈+1(11)。
- 对应++a,进行帧栈+1(12),再将12加载进来,此时相加得到22。
- 对应a–,加载12进来,接着帧栈-1(11),此时相加得到34。最终a=11
各种指令:
-
条件判断指令(ifne 是否!=0)、循环控制指令(do、while字节码per会一致)。在jvm中使用goto字节码指令来进行跳转到指定代码位置的!
-
public class Main { public static void main(String[] args) { int i = 0; int x = 0; while (i < 10){ x = x++; // 禁止出现这类代码! x = x++在字节码层面每次进行赋值都会赋值为0 i++; } System.out.println(x); } }
-
-
<cinit>()V
:整个类的构造方法。编译器从上至下收集所有static静态代码块及赋值的代码,合并成一个特殊的方法<cinit>()v
在类加载的初始化阶段被调用。-
public class Main { static int i = 0; static { i = 20; } static { i = 30; } } static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 //首先进行初始化,接着不断的取并进行赋值 0: iconst_0 1: putstatic #3 // Field i:I 4: bipush 20 6: putstatic #3 // Field i:I 9: bipush 30 11: putstatic #3 // Field i:I 14: return LineNumberTable: line 10: 0 line 13: 4 line 17: 9 line 18: 14
-
-
<init>()V
:实例化的构造方法,与<cinit>()V
类似也是从上到下依次来进行形成新的构造方法,原始构造方法内的代码总是在最后。 -
方法调用:三种字节码指令invokespecial、invokevirtual、invokestatic
-
构造方法、私有方法(private)、常量方法(final) =》 由于这些一定是静态绑定不会出现其他重写情况,即为invokespecial public方法 => 可以会重写、多态情况,即为invokevirtual,晚绑定的含义 static => 静态方法,即为invokestatic
-
// 指的是在堆中开辟一块空间B并且添加引用到操作数栈 0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9 // 复制刚刚开辟的一块空间的引用A 3: dup //使用引用A来进行初始化操作,结束后会将栈顶清除 4: invokespecial #3 // Method "<init>":()V //将B来进行存储到局部变量引用表中去 7: astore_1
-
// 注意点:对于静态方法的调用尽量不要使用对象实例来进行调用,否则可能会产生出来两条不必要的指令 // 下面20、21就是不必要的指令,会取出对象实例,由于是调用的静态方法所以会使用pop弹出 20: aload_1 21: pop 22: invokestatic #7 // Method test4:()V 25: invokestatic #7 // Method test4:()V 28: return
-
1.3、原理分析
多态原理
多态的方法存储在一个虚方法表(vtable)中,也就是说真正调用的方法与其对应虚方法表中的有关!
通过对象找到class类,然后通过class类来找到虚方法表之后能够确定某个方法实际对应入口地址,有的是来自于父类有的是来自己的eat方法,此时就能够知道对应对象到底调用的是哪个方法!
重点说明:虚方法表是在类的加载过程中链接阶段就会生成虚方法表,在链接的时候确定每个方法对应的入口地址。
当执行 invokevirtual 指令时,
1. 先通过栈帧中的对象引用找到对象
2. 分析对象头,找到对象的实际 Class
3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
4. 查表得到方法的具体地址
5. 执行方法的字节码
从细微上来看,其效率是不如static的,因为其涉及到运行期间需要动态的进行查找,jvm也会对虚方法表做一定的优化,也有缓存处理,若是某个方法被调用了多次就会进行缓存
异常
针对于异常是如何检测的呢?通过一个Exception table,指定from…to,对应的异常类型。
类型:单个catch、多个catch、multcatch
//单个catch:匹配是否与Exception异常符合或者说是其子类
try {
i = 10;
} catch (Exception e) {
i = 20;
}
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
//多个catch
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
Exception table:
from to target type //依次根据异常来进行监测
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable: //本地变量表也会存储异常类
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
//jdk1.7之后,支持multi-catch:多个异常类型的入口都是一致的!
try {
Method test = Demo3_11_3.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
Exception table:
from to target type //注意这三个异常的入口都是一致的!
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
finally
:可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。其中没有若是有其他如error异常也会有对应的措施即执行finally中的内容后再次抛出异常!
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
//三种情况:①第3段中出现Exception异常,执行finally方法完结束。③针对于第5端中出现Exception异常,执行完finally自动抛出异常。②第三段中出现Error或其他类型异常,执行完finally后自动抛出异常。
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try ①--------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // ①finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // ②catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // ②finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // ③catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // ③throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
finally
面试题:本质就是入栈的过程,返回值最终返回的是栈顶的值!在finally中进行return会吞掉自动抛出异常!
面试题1:
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
//结果:20
Code:
stack=1, locals=2, args_size=0
0: bipush 10 //对于return方法调用时会进行将常量10压入栈中
2: istore_0
3: bipush 20 //finally中也是return了,同样将常量20压入栈中
5: ireturn //直接将栈顶常量返回
6: astore_1 //出现异常时执行finally中
7: bipush 20
9: ireturn
Exception table:
from to target type
0 3 6 any
注意点:在finally中return会吞掉异常,也就是说只要你这个方法中的finally有返回值的操作就也异常也不会抛出异常!
面试题2:只要记住在方法中返回值的本质是将指定要返回的进行入栈操作即可,最终返回的值也是栈顶的值!
public class Main {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int i = 10;
try {
return i; //这里进行入栈操作,栈顶为10
} finally {
i = 20; //将20赋值给i
}
}
}
//结果:10
sychronized
public class Main {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock){ //将该对象作为同步代码块的锁
System.out.println("ok");
}
}
}
无论是同步代码块中的代码还是外面的代码最终都会走解锁的指定,也就是说都有对应解锁+抛出异常的动作。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // new Object
3: dup //操作数栈复制一层来进行下面构造器实例化
4: invokespecial #1 // invokespecial <init>:()V
7: astore_1 // lock引用 -> lock 将原始new的对象进行引用指向
8: aload_1 // <- lock (synchronized开始)
9: dup //可以看到又复制了一份来进行上锁
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock引用)
12: getstatic #3 // <- System.out
15: ldc #4 // <- "ok"
17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
20: aload_2 // <- slot 2(lock引用)
21: monitorexit // monitorexit(lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2(lock引用)
27: monitorexit // monitorexit(lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: ...
MethodParameters: ..
注意:方法级别的 synchronized 不会在字节码指令中有所体现。
二、字节码指令
2.3、编译器处理(语法糖)
2.3.1-2.3.9(精简)
1、默认构造器:无参构造器会自动调用super();
2、自动装拆箱:例如int与Integer不需要显示调用方法来进行转换。
3、泛型集合取值(类型擦除):编译成字节码以后,它执行的代码其实已经不区分你list.add()参数的类型了,统一当成Object来进行添加,之后取得时候默认会进行强转来进行取到。
- 擦除的是字节码上的信息, LocalVariableTypeTable 仍然保留了方法参数泛型的信息。
4、可变参数(String … => String[]):底层依旧会将String… args转为String[]
5、foreach数组:底层是fori进行单个遍历循环;foreach集合,底层就是使用迭代器来进行循环遍历。
6、switch:jdk1.7开始可以传入字符串以及枚举类来进行匹配!
- 举例若是对字符串进行switch,底层实际上会有两个switch,一个是针对于hashcode,另一个是针对于原始值。这也是为什么不能将null传入switch的原因。
- 针对枚举类:底层通过一个数组来进行匹配,下标取得枚举类的ordinal()!
7、twr(try with resources):对于资源的关闭我们无需进行手动判断操作,底层会为我们进行生成资源关闭的代码,并且比我们考虑的更周全,对于close()方法出现异常的捕获进行添加异常(该方式叫做压制异常!)我们也可以自己对其进行捕获。
//手动编写
public static void main(String[] args) {
MyResource myResource = null;
try {
myResource = new MyResource();
int i = 1 / 0;
}catch (Exception e){
e.printStackTrace();
}finally {
if (myResource != null){
try {
myResource.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//使用twr,我们专注于业务的异常步骤即可
try(MyResource myResource = new MyResource();){
int i = 1 / 0;
}catch (Exception e){
e.printStackTrace();
}
8、方法重写时的桥接方法:子类重写父类的方法返回值可以是父类方法返回值的子类。
2.3.10、方法重写时的桥接方法
方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
示例:
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new B().m());
}
}
实际编译期间JVM会为我们进行桥接重写方法的实现:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 间接调用 public Integer m()
return m();
}
}
验证:jvm给我们生成的合成方法,桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,能够让我们编写的方法重写真正符合规则,我们可以通过反射来进行验证查看
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
//打印信息:
public java.lang.Integer com.changlu.JVM.B.m()
public java.lang.Number com.changlu.JVM.B.m()
2.3.11、匿名内部类(底层原理、引用常量值)
匿名内部类底层原理
源代码:
public class Candy11 {
public static void main(String[] args) {
//创建了一个匿名实例
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
编译转换后代码:
// 额外生成的类:根据`外部类的名称+$+数字`则为匿名内部类的类名
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();//同样也是对某个类进行实例化
}
}
引用局部变量匿名内部类
源代码:
public class Candy11 {
public static void test(final int x) { //常量
Runnable runnable = new Runnable() {
@Override
public void run() {
//这里应当传入静态或者常量,若是只是取值其他实例值也是可以的,但是一旦对该变量进行修改就会报提示错误
System.out.println("ok:" + x);
}
};
}
}
转换后代码:
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) { //可以看到对于在匿名内部类中使用外部的变量实际上是通过构造器传入的
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1
对象时,将 x 的值赋值给了Candy11$1
对象的 val$x
属性,所以 x 不应该再发生变化了,如果变化,那么val$x
属性没有机会再跟着一起变化。
2.4、类加载阶段
类加载阶段:加载(字节码入方法区) -》链接(验证-安全检查、准备-为static分配空间、解析) -》初始化
2.4.1、加载阶段
一句话:将类的字节码加载到方法区中。
①内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror:java镜像,镜像起到桥梁作用,java的对象想要访问class的信息不能直接访问,需要通过这个镜像mirror来访问它。举个例子你若是想要用java对象来访问String.class得先访问镜像对象(java_mirror),接着镜像对象才能够间接去访问instanceKlass,此时才能够间接知道内部的一些属性内容。
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
②如果这个类还有父类没有加载,先加载父类。
③加载和链接可能是交替运行的。
介绍对象、类、镜像类的关系:以一个Person类、Person对象来表示他们的关系。
- ①类字节码加载到方法区,在JDK8中方法区叫做元空间,占据操作系统中的内存空间。
- ②class类对象与instanceKlass关系:加载的同时会在Java的堆内存空间中创建一个叫做
_java_mirror
的对象,这个类对象是在堆中存储的,并且它持有了instanceKlass的指针地址,反过来在元空间中isntanceKlass里的_java_mirror也指向了Person.class的内存地址。 - ③创建的对象与class类对象关系:之后使用new关键字创建了一些实例对象,实际上每个实例对象都有自己的对象头(16个字节,8个字节对应着该对象的class地址),若是想通过对象获取class信息,其实就会访问这个对象的对象头,首先找到根据class地址找到_java_mirror对象(如图Person.class),接着通过该类对象间接的通过instanceKlass指向找到元空间中存储的instanceKlass。
总结:也就是无论我们是通过类.class还是对象实例的class来获取类对象的一些信息实际上都要从元空间中获取,通过对象实例获取class对象与从类名.class实际上获取的都是堆中的类对象,若是想要获取属性则都是要去访问元空间。
public static void main(String[] args) {
//方式一:类.class获取
Class<Main> mainClass = Main.class;
System.out.println(mainClass);
//方式二:类实例.getClass()获取
Main main = new Main();//class com.changlu.JVM.Main
Class<? extends Main> aClass = main.getClass();
System.out.println(aClass);//class com.changlu.JVM.Main
System.out.println(mainClass.equals(aClass));//true 同一个对象地址
}
注意点:instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中可以通过前面介绍的 HSDB 工具查看。
2.4.2、链接
验证
一句话:验证类是否符合 JVM规范,安全性检查。
我们任意修改 HelloWorld.class 的魔数信息,接着使用java工具进行执行字节码文件:java xxx
- 修改内容:
cafe babe => cafe baba
,更改魔数
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file Main
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
准备
为 static 变量分配空间,设置默认值:例如int类型的静态变量会存储四个字节在指定区域并设置默认值
- 存储位置:static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾(存储在堆中)。
- 动作:static 变量分配空间和赋值是两个步骤,分配空间在准备阶段(当前阶段)完成,赋值在初始化阶段(在
<cinit>
构造方法中)完成。- static方法被调用时就会进行初始化操作!
- 额外情况:
- 如果 static 变量是 final 的基本类型以及字符串常量(String),那么编译阶段值就确定了,赋值在准备阶段就已经完成。
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段(
<cinit>
)完成。
//源代码
public class Main {
static int a;//准备阶段:分配空间 初始化节点:赋值
static int b = 10;//准备阶段:分配空间 初始化节点:赋值
static final int c = 20;//准备阶段:分配空间、赋值
static final String d = "hello";//准备阶段:分配空间、赋值
static final Object o = new Object();//准备阶段:分配空间 初始化节点:赋值
public static void main(String[] args) {
}
}
//反编译 javap -v class类名
static int a;
descriptor: I
flags: ACC_STATIC
static int b;
descriptor: I
flags: ACC_STATIC
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20 //static常量基本类型初始化准备阶段赋值
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hello //static字符串常量基本在准备阶段赋值
static final java.lang.Object o;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
static {}; //类初始化方法过程
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 10 //赋值变量10
2: putstatic #2 // Field b:I
5: new #3 //对象初始化动作 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #4 // Field o:Ljava/lang/Object;
15: return
LineNumberTable:
line 10: 0
line 13: 5
解析
一句话:将常量池中的符号引用解析为直接引用。
- 符号引用仅仅只是一个符号,其并不知道是类啊、方法也有可能是属性并不知道这些符号所指向的内存的位置,但是经过解析以后变成直接引用,此时就能够确切的知道这个类啊、方法、属性在内存中的位置了。
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,
IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
2.4.3、初始化
基础点认识
<cinit>()V
方法:初始化即调用 <cinit>()V
,虚拟机会保证这个类的『构造方法』的线程安全。
发生的时机:触发与不触发。
概括得说,类初始化是【懒惰的】,会导致类发生初始化的动作如下:
- main 方法所在的类,总会被首先初始化(例如走static{},一些static变量)。
- 首次访问这个类的静态变量或静态方法时(只要调用方法就会初始化,无论这个方法返回的是不是常量)。
- 子类初始化,如果父类还没初始化,会引发父类先进行初始化。
- 子类访问父类的静态变量,只会触发父类的初始化。
- Class.forName。
- new 会导致初始化
不会导致类初始化的情况:
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化。
- 类对象.class 不会触发初始化。
- 创建该类的数组不会触发初始化。
- 类加载器的 loadClass 方法。
- Class.forName 的参数 2 为 false 时。
- ClassLoader.defineClass()
示例
package com.changlu.JVM;
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
public class Test {
static {
System.out.println("main init"); //main方法所在的类会进行初始化
}
public static void main(String[] args) throws ClassNotFoundException {
// // 1. 静态常量(基本类型和字符串)不会触发初始化
// System.out.println(B.b);
// // 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// // 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// // 4. 不会初始化类 B,但会加载 B、A
// ClassLoader cl = Thread.currentThread().getContextClassLoader();
// cl.loadClass("com.changlu.JVM.B");
// // 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("com.changlu.JVM.B", false, c2);// 若是为true,会执行初始化!
// // 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// // 2. 子类初始化,如果父类还没初始化,会引发父类初始化(先进行)
// System.out.println(B.c);
// // 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// // 4. 会初始化类 B,并先初始化类 A
Class.forName("com.changlu.JVM.B"); //底层走的也是forName0(xx,true,xx)
}
}
初始化练习(包装类静态属性、懒惰初始化单例)
包装类对象属性
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;//底层会走Integer.valueOf()实际上是分配了一个对象,此时就会进行初始化
static {
System.out.println("E 初始化...");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);//会进行初始化
}
}
结论:对于包装类静态常量在获取时会对该类进行初始化,因为其底层走了Integer的指定方法。
典型应用 - 完成懒惰初始化单例模式:借助内部类
特点:①懒惰实例化。②初始化时的线程安全是有保障的。
final class Singleton{
private Singleton(){}
static {
System.out.println("Singleton init ...");
}
public static void test(){
System.out.println("test");
}
private static class LazyHolder{//内部类能够调用外部类的私有构造器
static final Singleton INSTANCE = new Singleton();//静态对象实例存储在一个内部类中
static {
System.out.println("LazyHolder init ...");
}
}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}
public class Test {
public static void main(String[] args) {
// Singleton.test();//好处(用于测试):调用Singleton静态方法时只会触发Singleton的初始化
//懒加载Singleton对象实例:只有当真正去获取单例实例时才会对LazyHolder内部类进行初始化
Singleton.getInstance();
}
}
用于证明调用Singleton静态方法只会触发Singleton的初始化方法而不会触发LazyHolder初始化:
使用内部类来取到单例对象好处:只有当取单例对象的时候才会对内部类进行初始化
- 点赞
- 收藏
- 关注作者
评论(0)