目录
在面试中经常会遇到这样一道面试题“你知道类加载过程吗?什么是双亲委派”;下面我们就针对这个问题展开,看看类加载过程,以及双亲委派机制
类加载过程有三个阶段:加载、链接以初始化;其中链接阶段有可以细分为:验证、准备和解析。如图所示:
验证阶段主要工作就是验证类是否符合JVM虚拟机规范,是否合法以及安全性检查
准备阶段为类的静态变量分配空间并设置初始值,这里的初始值是指类型的初始值;比如如下语句在准备阶段a的值是0而不是1,那么什么时候才会为1呢?这个要等到初始化阶段才会赋值。
public static int a=1;
那么是不是所有的属性都是这样呢?如下一条语句在准备阶段就是1
public static final int a=1;
解析阶段主要工作就是将常量池中的符号引用转化为直接引用
初始化是类加载过程的最后一步,而这一步才是真正开始执行JAVA代码。初始化阶段是执行类的构造器<clinit>方法的过程。类构造器与类的初始化方法是不同的。
- public class ClinitTest {
- private static String property = "test";
-
- static {
- System.out.println("static block");
- }
-
- private int a = 1;
-
- public static void main(String[] args) {
- ClinitTest clinitTest = new ClinitTest();
- clinitTest.print();
- }
-
- private void print() {
- System.out.println(a);
- }
- }
上面这段代码编译后的字节码如下:
- Compiled from "ClinitTest.java"
- public class com.dora.jvm.ClinitTest
- minor version: 0
- major version: 61
- flags: (0x0021) ACC_PUBLIC, ACC_SUPER
- this_class: #8 // com/dora/jvm/ClinitTest
- super_class: #2 // java/lang/Object
- interfaces: 0, fields: 2, methods: 4, attributes: 1
- Constant pool:
- #1 = Methodref #2.#3 // java/lang/Object."<init>":()V
- #2 = Class #4 // java/lang/Object
- #3 = NameAndType #5:#6 // "<init>":()V
- #4 = Utf8 java/lang/Object
- #5 = Utf8 <init>
- #6 = Utf8 ()V
- #7 = Fieldref #8.#9 // com/dora/jvm/ClinitTest.a:I
- #8 = Class #10 // com/dora/jvm/ClinitTest
- #9 = NameAndType #11:#12 // a:I
- #10 = Utf8 com/dora/jvm/ClinitTest
- #11 = Utf8 a
- #12 = Utf8 I
- #13 = Methodref #8.#3 // com/dora/jvm/ClinitTest."<init>":()V
- #14 = Methodref #8.#15 // com/dora/jvm/ClinitTest.print:()V
- #15 = NameAndType #16:#6 // print:()V
- #16 = Utf8 print
- #17 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
- #18 = Class #20 // java/lang/System
- #19 = NameAndType #21:#22 // out:Ljava/io/PrintStream;
- #20 = Utf8 java/lang/System
- #21 = Utf8 out
- #22 = Utf8 Ljava/io/PrintStream;
- #23 = Methodref #24.#25 // java/io/PrintStream.println:(I)V
- #24 = Class #26 // java/io/PrintStream
- #25 = NameAndType #27:#28 // println:(I)V
- #26 = Utf8 java/io/PrintStream
- #27 = Utf8 println
- #28 = Utf8 (I)V
- #29 = String #30 // test
- #30 = Utf8 test
- #31 = Fieldref #8.#32 // com/dora/jvm/ClinitTest.property:Ljava/lang/String;
- #32 = NameAndType #33:#34 // property:Ljava/lang/String;
- #33 = Utf8 property
- #34 = Utf8 Ljava/lang/String;
- #35 = String #36 // static block
- #36 = Utf8 static block
- #37 = Methodref #24.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #38 = NameAndType #27:#39 // println:(Ljava/lang/String;)V
- #39 = Utf8 (Ljava/lang/String;)V
- #40 = Utf8 Code
- #41 = Utf8 LineNumberTable
- #42 = Utf8 LocalVariableTable
- #43 = Utf8 this
- #44 = Utf8 Lcom/dora/jvm/ClinitTest;
- #45 = Utf8 main
- #46 = Utf8 ([Ljava/lang/String;)V
- #47 = Utf8 args
- #48 = Utf8 [Ljava/lang/String;
- #49 = Utf8 clinitTest
- #50 = Utf8 <clinit>
- #51 = Utf8 SourceFile
- #52 = Utf8 ClinitTest.java
- {
- public com.dora.jvm.ClinitTest();
- descriptor: ()V
- flags: (0x0001) ACC_PUBLIC
- Code:
- stack=2, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: aload_0
- 5: iconst_1
- 6: putfield #7 // Field a:I
- 9: return
- LineNumberTable:
- line 3: 0
- line 10: 4
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 10 0 this Lcom/dora/jvm/ClinitTest;
-
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: (0x0009) ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=2, args_size=1
- 0: new #8 // class com/dora/jvm/ClinitTest
- 3: dup
- 4: invokespecial #13 // Method "<init>":()V
- 7: astore_1
- 8: aload_1
- 9: invokevirtual #14 // Method print:()V
- 12: return
- LineNumberTable:
- line 13: 0
- line 14: 8
- line 15: 12
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 13 0 args [Ljava/lang/String;
- 8 5 1 clinitTest Lcom/dora/jvm/ClinitTest;
-
- static {};
- descriptor: ()V
- flags: (0x0008) ACC_STATIC
- Code:
- stack=2, locals=0, args_size=0
- 0: ldc #29 // String test
- 2: putstatic #31 // Field property:Ljava/lang/String;
- 5: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
- 8: ldc #35 // String static block
- 10: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 13: return
- LineNumberTable:
- line 4: 0
- line 7: 5
- line 8: 13
- }
- SourceFile: "ClinitTest.java"
可以发现字节码中存在<clinit>和<init>两个方法
类加载是懒惰加载只有在需要的时候才会执行类加载,下面通过代码来看看是不是这样。
- public class ClassLoaderDemo {
-
- private static final String a = "final";
-
- private static String b = " static";
-
- static {
- System.out.println("static blocked");
- }
- }
- public class ClinitTest {
- private static String property = "test";
-
- static {
- System.out.println("static block");
- }
-
- private int a = 1;
-
- public static void main(String[] args) throws IOException {
- ClinitTest clinitTest = new ClinitTest();
- clinitTest.print();
- System.in.read();
- System.out.println(ClassLoaderDemo.class);
-
- }
-
- private void print() {
- System.out.println(a);
- }
- }
在等待输入的时候通过arthas去查看加载的类信息如下:可以发现此时并没有加载ClassLoaderDemo类
- [arthas@48513]$ sc com.dora.jvm.*
- com.dora.jvm.ClinitTest
- Affect(row-cnt:1) cost in 4 ms.
- [arthas@48513]$
键盘输入后的类加载信息如下:
- [arthas@48872]$ sc com.dora.jvm.*
- com.dora.jvm.ClassLoaderDemo
- com.dora.jvm.ClinitTest
- Affect(row-cnt:2) cost in 11 ms.
- [arthas@48872]$
我们通过上文的字节码就可以看到,静态属性和静态代码块是放在一起执行的,不过是先执行静态属性的初始化在执行静态代码块中的代码。
双亲委派机制流程:
注意:一个类的Class的是ClassLoader+类的权限定名为一,同一个类被不同的类加载器加载是不同的
获取数据库连接的代码如下:
Connection connection = DriverManager.getConnection(url, username, password);
这个DriverManager是JAVA内置,但是不同的数据库都是有不同的实现,比如Mysql的DriverManger实现是:com.mysql.cj.jdbc.Driver,我们看看它是如何加载的呢?
- public static <S> ServiceLoader<S> load(Class<S> service) {
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- return ServiceLoader.load(service, cl);
- }
-
在加载时候是调用Thread.currentThread().getContextClassLoader()去加载;可以通过如下语句将子类加载器放入到线程中
Thread.currentThread().setContextClassLoader(this.loader);
这样就就破坏了双亲委派,本来应该由父类加载器加载的类让子类加载器去加载了;解决了类的你逆向访问问题,通过这个SPI的方式可以有效提升系统的扩展性