干货-深入理解Java虚拟机-3-虚拟机类加载机制

男娘i 2022-10-11 12:25 431阅读 0赞

前言

基础不牢,地动山摇,菜如老哥还经常巩固自己的基本功,你就更要努力学习了。
最近博主在复习Java虚拟机,对Java虚拟机的理解又有了一个更深层次的理解,记录下一些笔记及重点摘要,让我们一起学习一下吧!

现在用不上,不代表以后就能用上,一句话,学,就行了。

学习JVM有什么意义和作用?

1、学习JVM能更深入的理解Java这门语言,能理解Java语言底层的执行过程。

2、学习JVM,为了项目上线后去排查一些程序log日志中无法呈现的问题,可以通过GC日志来排查项目问题以及进行调优。

3、能够利用一些工具,jmap, jvisualvm, jstat, jconsole等工具可以辅助你观察Java应用在运行时堆的布局情况,由此你可以通过调整JVM相关参数提高Java应用的性能。

4、学习之前面试官虐待你你会觉得很痛苦,学完之后再被虐待时你会觉得很享受。

链接附上:《干货-深入理解Java虚拟机》

在这里插入图片描述

正文

1. 类加载的时机

类的生命周期(面试常问):

加载、验证、准备、解析、初始化、使用和卸载,其中验证、准备、解析部分统称为连接。
需要注意的是加载、验证、准备、初始化和卸载这五个阶段的顺序是固定的,而其他的两个阶段则不一定,解析有可能在初始化之前或者之后。

在这里插入图片描述

对于第一个阶段加载,Java虚拟机规范中没有进行强制规范什么时间开始,但是对于初始化阶段虚拟机则强制规定了有且只有五种情况必须对类进行初始化(加载、验证、准备自然也要在这之前执行):

(1)遇到new、getstatic、putstatic或者invokestatic这4个字节码指令时,如果类没有进行初始化,则需要先进行初始化。这四条字节码指令对应的Java场景分别是:使用new关键字新建对象、读取或设置一个类的静态字段(被final修饰的除外)以及调用一个类的静态方法的时候

(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,要先进行初始化。

(3) 当初始化一个类的时候,如果他的父类还没有进行过初始化,要先初始化他的父类。

(4) 当虚拟机启动的时候,用户需要指定一个主类(含main方法的那个类) 作为程序入口,虚拟机会先初始化这个主类。

(5)当使用JDK1.7的动态预言支持,如果 一个java.long.invoke.MethodHandle实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄对应的类没有进行过初始化,则要先进行初始化。

Java中明确限定,有且只有这五种情况会触发类的初始化。这五种场景中的行为成为对类的主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。


下面对被动引用做一些代码示例(也是面试考点):

被动引用示例1:通过子类引用父类静态字段,不会导致子类初始化

  1. /** * @Author: Daisen.Z * @Date: 2021/6/30 15:42 * @Version: 1.0 * @Description: 父类 */
  2. public class SuperClass {
  3. static {
  4. // 在类初始化时static代码块会被执行
  5. System.out.println("SuperClass init");
  6. }
  7. public static int value = 123;
  8. }
  9. /** * @Author: Daisen.Z * @Date: 2021/6/30 15:42 * @Version: 1.0 * @Description: 子类 */
  10. public class ChildClass extends SuperClass {
  11. static {
  12. // 在类初始化时static代码块会被执行
  13. System.out.println("ChildClass init");
  14. }
  15. }
  16. /** * @Author: Daisen.Z * @Date: 2021/6/30 15:45 * @Version: 1.0 * @Description: 调用父类静态字段 */
  17. public class Demo1 {
  18. public static void main(String[] args) {
  19. // 调用父类静态字段
  20. System.out.println(ChildClass.value);
  21. }
  22. }

这时输出结果不会有”ChildClass init”,证明只有通过ChildClass调用他父类的静态字段不会初始化ChildClass。这时由于对于静态字段直接属于类,调用静态字段时只会加载静态字段属于的类。


被动引用示例2:通过数组引用类,不会触发类的初始化(只有在新建数组不会触发,当引用数组元素时会触发)

  1. public class Demo1 {
  2. public static void main(String[] args) {
  3. // 调用父类静态字段
  4. SuperClass[] sca = new SuperClass[10];
  5. }
  6. }

这里复用示例1中的SuperClass,创建数组后发现没有输出”SuperClass init”,说明没有触发SuperClass初始化,但是这段代码会触发一个”[Lorg.com.example.demo.test.SupperClass“的类初始化,它是一个由虚拟机自动生成的、直接继承于Object的子类,创建动作由新建数组触发,这个类代表了一个元素类型为SupperClass的一维数组。


被动引用示例3:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,所以这种情况也不会触发初始化。

  1. /** * @Author: Daisen.Z * @Date: 2021/6/30 16:56 * @Version: 1.0 * @Description: */
  2. public class ConstClass {
  3. static {
  4. // 在类初始化时static代码块会被执行
  5. System.out.println("ConstClass init");
  6. }
  7. public static final String HELLO = "Hello world";
  8. public static final int value = 123;
  9. }
  10. public class Demo1 {
  11. public static void main(String[] args) {
  12. // 调用父类静态字段
  13. System.out.println(ConstClass.HELLO);
  14. System.out.println(ConstClass.value);
  15. }
  16. }

上面的代码虽然引用了ConstClass类中的静态常量HELLO,但其实在编译器通过常量的传播优化,已经将此常量的值”Hello world“存储到了Demo1的常量池中,编译完成后Demo1引用HELLO都是引用的自己常量池中的值,与ConstClass不再有任何关系,所以调用该值不会触发ConstClass的初始化。

接口也有初始化过程,但与类的加载略有不同: 编译器也会为接口生成类构造器,用于初始化接口中所定义的成员变量。接口与类真正的区别是前面说到的五种有且仅有中的第3种:当一个类在初始化时,要求其父类先初始化,但接口的初始化不同,一个接口在初始化时不需要要求其父接口初始化,只有在真正使用到父接口(调用方法、引用常量)的时候才会初始化


2. 类加载过程

这个主要需要了解双亲委派机制,也是面试常问的点,大家可以看我之前写的一篇博客来学习:
https://blog.csdn.net/weixin_43464964/article/details/88650730


以上就是博主本次关于虚拟机的学习分享,谢谢大家观看,记得一键三连哦!

另外,附上学习书籍:《深入理解Java虚拟机》,需要某宝可以进行购买
在这里插入图片描述

你只有通过自身不懈的努力,才能受到社会不断的毒打,干就完了!

发表评论

表情:
评论列表 (有 0 条评论,431人围观)

还没有评论,来说两句吧...

相关阅读