【JVM】JVM07(类加载阶段详细解析)

⭐️写在前面


  • 这里是温文艾尔の学习之路
  • 如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 博客主页 温文艾尔の学习小屋
  • ⭐️更多文章‍请关注温文艾尔主页
  • 文章发布日期:2022.02.08
  • java学习之路!
  • 欢迎各位点赞评论收藏⭐️
  • 新年快乐朋友们
  • jvm学习之路!
  • ⭐️上一篇内容:【JVM】JVM05(从字节码角度分析i++和++i的执行流程))

文章目录

  • ⭐️1.类加载阶段
  • ⭐️1.1加载Loading
  • ⭐️1.2连接Linking
  • ⭐️1.2.1连接阶段-验证
  • ⭐️1.2.2连接阶段-准备
  • ⭐️1.2.3连接阶段-解析:
  • ⭐️1.3初始化阶段Initialization
  • 相关练习1
  • 相关练习2


⭐️1.类加载阶段

⭐️1.1加载Loading

JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,生成一个代表该类的java.lang.Class对象

将类的字节码载入方法区中,内部采用C++的instanceKlass描述java类,它的重要field有:

_java_mirror即java的类镜像,例如对String来说,就是String.class,作用是吧klass暴露给java使用
_super即父类
_fields即成员变量
_methods即方法
_constants即常量池
_class_loader即类加载器
_vtable虚方法表
_itable接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

注意

  • nstanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中

比如现在有两个person的实例对象,每个实例对象都有自己的对象头(16字节)其中8个字节对应着class地址,如果想通过对象获取class信息,就会访问对象的对象头,通过class地址找到Person.class类对象,再通过类对象间接的去元空间(Metaspace)找到信息
【JVM】JVM07(类加载阶段详细解析)_第1张图片

⭐️1.2连接Linking

分为三个小的步骤

⭐️1.2.1连接阶段-验证

  1. 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机中的要求,并且不会危害虚拟机自身的安全
  2. 包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据校验、字节码验证和符号引用验证
  3. 可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间

例如使用UE等支持二进制的编辑器修改HelloWorld.class的魔数,在控制台运行

public class Demo01 {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

将.class用sublime打开,修改
【JVM】JVM07(类加载阶段详细解析)_第2张图片
修改之后运行,因修改之后格式规范错误,验证阶段没有通过
【JVM】JVM07(类加载阶段详细解析)_第3张图片

⭐️1.2.2连接阶段-准备

为static变量分配空间,设置默认值

  • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段就确定了,赋值再准备阶段完成
  • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成

代码示例

class A{
    //连接阶段-准备
    //1.n1是实例属性,不是静态变量,因此在连接阶段-准备时不会被分配内存
    //2.n2是静态变量,分配内存 n2 是默认在此阶段初始化,值为0,只有在连接阶段后的初始化阶段值才会被初始化为20
    //3.n3是常量,他和静态变量不一样,因为一旦赋值就不会改变,所以n3此阶段值为30
    //4.String准备阶段值为null,赋值操作在初始化阶段完成,如果加上final的话,赋值操作就会在准备阶段完成
    public int n1=10;
    public static int n2=20;
    public static final int n3=30;
    public final String n4 = "hello";
}

jdk1.8及以后,静态变量和类对象存储在一起,存储在堆中,在早期的jvm(jdk1.6以前)里,静态变量在存储在方法区,从1.7开始到1.8静态变量转移到堆中
【JVM】JVM07(类加载阶段详细解析)_第4张图片

⭐️1.2.3连接阶段-解析:

虚拟机将常量池内的符号引用解析为直接引用的过程

比如A类中有B类的引用,但只作为符号存在前,只有在解析阶段才会将这类的符号引用解析为直接引用

举例

public class Demo02 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Demo02.class.getClassLoader();
        //loadClass方法不会导致类的解析和初始化
        Class<?> c = classLoader.loadClass("com.wql.jvm.ClassStep.C");
        System.in.read();
    }
}

class C{
    D d = new D();
}
class D{

}

loadClass("com.wql.jvm.ClassStep.C");不会使C类中D加载触发,而new C()则会触发D

⭐️1.3初始化阶段Initialization

< cinit>()v方法

初始化即调用< cinit>()v,虚拟机会保证这个类的【构造方法】的线程安全

发生的时机

概括的说,类初始化是【懒惰的】

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时会触发
  • 子类初始化,如果父类还没初始化,会触发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化

案例

public class Demo03 {
    static {
        System.out.println("main init");
    }

    public static void main(String[] args) throws Exception {
        //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 c1 = Thread.currentThread().getContextClassLoader();
        c1.loadClass("com.wql.jvm.ClassStep.B");
        //5.不会初始化类B,但会加载B,A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("com.wql.jvm.ClassStep.B",false,c2);


        // 1. 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("com.wql.jvm.ClassStep.B");
    }
}

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");
    }
}

相关练习1

从字节码分析,使用abc者三个常量是否会导致E初始化

public class Demo04 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

答案:不会,不会,会

因为a是基本类型,而b是字符串常量,赋值操作在准备阶段就触发,不会触发初始化

而c是包装类型,触及装箱拆箱操作Integer c = Integer.valueOf(20),在初始化阶段完成

相关练习2

典型应用-完成懒惰初始化单例模式

public class Demo05 {
    private Demo05(){}
    //保存单例
    private static class LazyHolder{
        static final Demo05 INSTANCE = new Demo05();
    }

    //第一次调用getInstance方法,才会导致内部类加载和初始化其静态成员
    public static Demo05 getInstance(){
        return LazyHolder.INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

你可能感兴趣的:(jvm,java,后端,面试,jvm,java字节码)