• 【八股文】五分钟掌握类加载过程


    目录

    类加载过程

    加载

    链接

    验证

    准备

    解析

    初始化

    clinit特点

    init方法特点

    什么时候执行类加载

     静态属性和静态代码块初始化顺序

    双亲委派

    为什么需要双亲委派

    什么时候需要破坏双亲委派


    在面试中经常会遇到这样一道面试题“你知道类加载过程吗?什么是双亲委派”;下面我们就针对这个问题展开,看看类加载过程,以及双亲委派机制

    类加载过程

    类加载过程有三个阶段:加载、链接以初始化;其中链接阶段有可以细分为:验证、准备和解析。如图所示:

    加载

    1. 通过类的全限定名获取定义这个类的字节码,并载入方法区;创建类的Class对象
    2. 如果类的父类没有加载需要先加载父类

    链接

    验证

    验证阶段主要工作就是验证类是否符合JVM虚拟机规范,是否合法以及安全性检查

    1. 文件格式验证,比如验证魔数“0xCAFEBABE”以及虚拟机主副版本号等
    2. 元数据验证,这是对字节码语义分析保证语义也是符合虚拟机规范
    3. 字节码验证,主要是验证字节码控制流程验证,保证字节码不会出现危害虚拟机的操作
    4. 符号引用验证 ,主要验证符号引用的类是否存在,是否可以被访问       

    准备

    准备阶段为类的静态变量分配空间并设置初始值,这里的初始值是指类型的初始值;比如如下语句在准备阶段a的值是0而不是1,那么什么时候才会为1呢?这个要等到初始化阶段才会赋值。

    public static int a=1;

    那么是不是所有的属性都是这样呢?如下一条语句在准备阶段就是1

    public static final int a=1; 

    解析

     解析阶段主要工作就是将常量池中的符号引用转化为直接引用

    初始化

    初始化是类加载过程的最后一步,而这一步才是真正开始执行JAVA代码。初始化阶段是执行类的构造器<clinit>方法的过程。类构造器与类的初始化方法是不同的。

    1. public class ClinitTest {
    2. private static String property = "test";
    3. static {
    4. System.out.println("static block");
    5. }
    6. private int a = 1;
    7. public static void main(String[] args) {
    8. ClinitTest clinitTest = new ClinitTest();
    9. clinitTest.print();
    10. }
    11. private void print() {
    12. System.out.println(a);
    13. }
    14. }

    上面这段代码编译后的字节码如下:

    1. Compiled from "ClinitTest.java"
    2. public class com.dora.jvm.ClinitTest
    3. minor version: 0
    4. major version: 61
    5. flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    6. this_class: #8 // com/dora/jvm/ClinitTest
    7. super_class: #2 // java/lang/Object
    8. interfaces: 0, fields: 2, methods: 4, attributes: 1
    9. Constant pool:
    10. #1 = Methodref #2.#3 // java/lang/Object."<init>":()V
    11. #2 = Class #4 // java/lang/Object
    12. #3 = NameAndType #5:#6 // "<init>":()V
    13. #4 = Utf8 java/lang/Object
    14. #5 = Utf8 <init>
    15. #6 = Utf8 ()V
    16. #7 = Fieldref #8.#9 // com/dora/jvm/ClinitTest.a:I
    17. #8 = Class #10 // com/dora/jvm/ClinitTest
    18. #9 = NameAndType #11:#12 // a:I
    19. #10 = Utf8 com/dora/jvm/ClinitTest
    20. #11 = Utf8 a
    21. #12 = Utf8 I
    22. #13 = Methodref #8.#3 // com/dora/jvm/ClinitTest."<init>":()V
    23. #14 = Methodref #8.#15 // com/dora/jvm/ClinitTest.print:()V
    24. #15 = NameAndType #16:#6 // print:()V
    25. #16 = Utf8 print
    26. #17 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
    27. #18 = Class #20 // java/lang/System
    28. #19 = NameAndType #21:#22 // out:Ljava/io/PrintStream;
    29. #20 = Utf8 java/lang/System
    30. #21 = Utf8 out
    31. #22 = Utf8 Ljava/io/PrintStream;
    32. #23 = Methodref #24.#25 // java/io/PrintStream.println:(I)V
    33. #24 = Class #26 // java/io/PrintStream
    34. #25 = NameAndType #27:#28 // println:(I)V
    35. #26 = Utf8 java/io/PrintStream
    36. #27 = Utf8 println
    37. #28 = Utf8 (I)V
    38. #29 = String #30 // test
    39. #30 = Utf8 test
    40. #31 = Fieldref #8.#32 // com/dora/jvm/ClinitTest.property:Ljava/lang/String;
    41. #32 = NameAndType #33:#34 // property:Ljava/lang/String;
    42. #33 = Utf8 property
    43. #34 = Utf8 Ljava/lang/String;
    44. #35 = String #36 // static block
    45. #36 = Utf8 static block
    46. #37 = Methodref #24.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
    47. #38 = NameAndType #27:#39 // println:(Ljava/lang/String;)V
    48. #39 = Utf8 (Ljava/lang/String;)V
    49. #40 = Utf8 Code
    50. #41 = Utf8 LineNumberTable
    51. #42 = Utf8 LocalVariableTable
    52. #43 = Utf8 this
    53. #44 = Utf8 Lcom/dora/jvm/ClinitTest;
    54. #45 = Utf8 main
    55. #46 = Utf8 ([Ljava/lang/String;)V
    56. #47 = Utf8 args
    57. #48 = Utf8 [Ljava/lang/String;
    58. #49 = Utf8 clinitTest
    59. #50 = Utf8 <clinit>
    60. #51 = Utf8 SourceFile
    61. #52 = Utf8 ClinitTest.java
    62. {
    63. public com.dora.jvm.ClinitTest();
    64. descriptor: ()V
    65. flags: (0x0001) ACC_PUBLIC
    66. Code:
    67. stack=2, locals=1, args_size=1
    68. 0: aload_0
    69. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
    70. 4: aload_0
    71. 5: iconst_1
    72. 6: putfield #7 // Field a:I
    73. 9: return
    74. LineNumberTable:
    75. line 3: 0
    76. line 10: 4
    77. LocalVariableTable:
    78. Start Length Slot Name Signature
    79. 0 10 0 this Lcom/dora/jvm/ClinitTest;
    80. public static void main(java.lang.String[]);
    81. descriptor: ([Ljava/lang/String;)V
    82. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    83. Code:
    84. stack=2, locals=2, args_size=1
    85. 0: new #8 // class com/dora/jvm/ClinitTest
    86. 3: dup
    87. 4: invokespecial #13 // Method "<init>":()V
    88. 7: astore_1
    89. 8: aload_1
    90. 9: invokevirtual #14 // Method print:()V
    91. 12: return
    92. LineNumberTable:
    93. line 13: 0
    94. line 14: 8
    95. line 15: 12
    96. LocalVariableTable:
    97. Start Length Slot Name Signature
    98. 0 13 0 args [Ljava/lang/String;
    99. 8 5 1 clinitTest Lcom/dora/jvm/ClinitTest;
    100. static {};
    101. descriptor: ()V
    102. flags: (0x0008) ACC_STATIC
    103. Code:
    104. stack=2, locals=0, args_size=0
    105. 0: ldc #29 // String test
    106. 2: putstatic #31 // Field property:Ljava/lang/String;
    107. 5: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
    108. 8: ldc #35 // String static block
    109. 10: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    110. 13: return
    111. LineNumberTable:
    112. line 4: 0
    113. line 7: 5
    114. line 8: 13
    115. }
    116. SourceFile: "ClinitTest.java"

    可以发现字节码中存在<clinit>和<init>两个方法

    clinit特点

    1. 如果有父类则先执行父类的<clinit>,再执行子类的<clinit>
    2. 如果类中没有静态属性也没有静态代码块可以不生成<clinit>
    3. <clinit>线程安全,在多线程环境下,只有一个线程可以执行<clinit>其他线程阻塞到该方法执行完毕,同时其他线程也不会在执行该方法,这就保证了一个类在同一个加载器中,只会初始化一次。

    init方法特点

    1. 只有new一个对象时,即执行类的构造方法时候才会执行,就比如上文中字节码中是在main方法中调用的
    2. init方法可以执行多次,每次new一个对象时都会执行init犯法
    3. 如果有父类则先执行父类的<init>再执行子类的<init>

    什么时候执行类加载

      类加载是懒惰加载只有在需要的时候才会执行类加载,下面通过代码来看看是不是这样。

    1. public class ClassLoaderDemo {
    2. private static final String a = "final";
    3. private static String b = " static";
    4. static {
    5. System.out.println("static blocked");
    6. }
    7. }
    1. public class ClinitTest {
    2. private static String property = "test";
    3. static {
    4. System.out.println("static block");
    5. }
    6. private int a = 1;
    7. public static void main(String[] args) throws IOException {
    8. ClinitTest clinitTest = new ClinitTest();
    9. clinitTest.print();
    10. System.in.read();
    11. System.out.println(ClassLoaderDemo.class);
    12. }
    13. private void print() {
    14. System.out.println(a);
    15. }
    16. }

     在等待输入的时候通过arthas去查看加载的类信息如下:可以发现此时并没有加载ClassLoaderDemo类

    1. [arthas@48513]$ sc com.dora.jvm.*
    2. com.dora.jvm.ClinitTest
    3. Affect(row-cnt:1) cost in 4 ms.
    4. [arthas@48513]$

    键盘输入后的类加载信息如下:

    1. [arthas@48872]$ sc com.dora.jvm.*
    2. com.dora.jvm.ClassLoaderDemo
    3. com.dora.jvm.ClinitTest
    4. Affect(row-cnt:2) cost in 11 ms.
    5. [arthas@48872]$

     静态属性和静态代码块初始化顺序

            我们通过上文的字节码就可以看到,静态属性和静态代码块是放在一起执行的,不过是先执行静态属性的初始化在执行静态代码块中的代码。 

    双亲委派

     双亲委派机制流程:

    1. 首先判断这个类有没有被加载过
    2. 如果没有被加载过,如果父类加载器不为空则交给父类加载
    3. 如果父类为空,则交给Bootstrap classloader加载
    4. 如果父类都没有加载则有当前类加载器加载过

    注意:一个类的Class的是ClassLoader+类的权限定名为一,同一个类被不同的类加载器加载是不同的

    为什么需要双亲委派

    1. 通过使用双亲委派机制,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会重新加载这个类
    2. 通过使用双亲委派机制,保证JVM的安全性。Bootstrap ClassLoader 只会加载JAVA_HOME 中的jar,可以避免JVM中核心API被替换。

    什么时候需要破坏双亲委派

      获取数据库连接的代码如下:

    ​  Connection connection = DriverManager.getConnection(url, username, password);

    这个DriverManager是JAVA内置,但是不同的数据库都是有不同的实现,比如Mysql的DriverManger实现是:com.mysql.cj.jdbc.Driver,我们看看它是如何加载的呢?

    1. public static <S> ServiceLoader<S> load(Class<S> service) {
    2.          ClassLoader cl = Thread.currentThread().getContextClassLoader();
    3.          return ServiceLoader.load(service, cl);
    4.     }

    在加载时候是调用Thread.currentThread().getContextClassLoader()去加载;可以通过如下语句将子类加载器放入到线程中

    Thread.currentThread().setContextClassLoader(this.loader);

    这样就就破坏了双亲委派,本来应该由父类加载器加载的类让子类加载器去加载了;解决了类的你逆向访问问题,通过这个SPI的方式可以有效提升系统的扩展性 

  • 相关阅读:
    驱动比例多路阀控制器
    Mysql系列二:Mysql里的锁
    来自北大算法课的Leetcode题解:8. 字符串转换整数(atoi)
    Springboot自定义@Import自动装配
    sklearn包中对于分类问题,如何计算accuracy和roc_auc_score?
    如何注册一个 DA 为 10 的高价值老域名
    ssm毕设项目学生公寓信息管理系统4g400(java+VUE+Mybatis+Maven+Mysql+sprnig)
    第4章 基于注解开发Spring AOP
    Redis7.0 编译安装以及简单创建Cluster测试服务器的方法 步骤
    WordPress画廊插件Envira Gallery v1.9.7河蟹版下载
  • 原文地址:https://blog.csdn.net/zhangwei_david/article/details/125415126