类的加载过程概述

类加载过程详解

   1,类的生命周期概述和大厂面试题

   2,Loading阶段

   3,Linking阶段

   4,Initialization阶段

   5,类的Using

   6,类的Unloading


 

类的生命周期概述

    1,概述:在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型有虚拟机预先定义,引用数据类型则需要进行类的加载。

    2,生命周期7个阶段

 

  3, 其中,验证,准备,解析 3 个部分统称为链接(Linking)

 

  ,4,大厂面试题

     蚂蚁金服:

        描述一下 JVM 加载 Class 文件的原理机制?

        一面:类加载过程

 

     百度:

        类加载的时机

        java 类加载过程?

        简述 java 类加载机制?

 

      腾讯:

        JVM 中类加载机制

 

      美团:

        Java类加载过程

        描述一下jvm加载class文件的原理机制


 

过程一:Loading(加载阶段)

     A:加载完成的操作

        加载的理解

        所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型  ---  类模板对象

        反射的机制即基于这一基础,如果JVM没有Java类的声明信息存储起来,则JVM在运行期也无法反射。

 

        加载完成的操作

        加载阶段,查找并加载类的二进制数据,生成Class的实例

        在加载类,Java虚拟机必须完成3件事情:

  •             通过类的全名,获取类的二进制数据流。
  •             解析类的二进制数据流为方法区内的数据结构(Java类模型)
  •             创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口。

    B:二进制流的获取方式

         二进制的获取方式

         对于类的二进制数据流,虚拟机可以通过多种途径产或获得。(只有所读取的字节码符合JVM规范即可)

  •             虚拟机可能通过文件系统读入一个class后缀的文件
  •             读入jar,zip等归档数据包,提取类文件。
  •             事先存放在数据库中的类的二进制数据。

 

          在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。

          如果输入数据不是ClassFile的结构,则会抛出ClassFormatError

 

    C:类模型与Class实例的位置

          加载的类在JVM中创建相应的类结构,类结构会存储方法区(jdk8之前:永久代;jdk8之后:元空间)

          类将.class文件加载至元空间,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区结构,Class对象是在加载类的过程中创建,每个类都对应有一个Class类型的对象。

 

 

 

   Class类的构造方法是私有的,只有JVM能够创建

   java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据,入口。通过Class可以获得目标类所关联.class文件中具体的数据:方法,字段等

package java11;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 *通过Class类,获取java.lang.String 类的所有方法信息,并打印方法访问标识
 *
 * @Author ylc
 * @Date 2022/4/5 11:33
 * @Version 1.0
 */
public class LoadingTest {
    public static void main(String[] args) {
        try {
            Class clazz = Class.forName("java.lang.String");
            //获取当前运行时类声明的所有方法
            Method[] ms = clazz.getDeclaredMethods();
            for (Method m : ms) {
                //获取方法的修饰符
                String mod = Modifier.toString(m.getModifiers());
                System.out.println(mod + "");
                //获取方法返回值类型
                String returnType = m.getReturnType().getSimpleName();
                System.out.println(returnType + "");
                //获取方法名
                System.out.println(m.getName() + "");
                //获取方法的参数列表
                Class<?>[] ps = m.getParameterTypes();
                if (ps.length == 0) {
                    System.out.println(')');
                    for (int i = 0; i < ps.length; i++) {
                        char end = (i == ps.length-1) ? ')':',';
                        //获取参数的类型
                        System.out.println(ps[i].getSimpleName() + end);
                    }
                    System.out.println();
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

    D:数组类的加载

      因为数组类本身并不是由类加载负责创建,而是有JVM在运行时根据需要而直接创建的,但数组的元素类型仍然要依靠类加载器去创建。

     

      1,如果数组的元素类型是引用类型,遵循定义的加载过程递归加载和创建数组A的元素类型。

      2,JVM使用指定的元素类型和数组维度来创建新的数据类

 

      如果数据的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。

 


 

 链接阶段

   1,环节1:链接阶段之Verification(验证)

   

 

    整体说明:

    验证内容则涵盖了类数据信息的格式验证,语义检查,字节码验证,以及符号引用验证等。

       其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载。

       格式验证之外的验证操作将会在方法区中进行。

    

     链接阶段的验证虽然拖慢了加载速度,但是它避免了字节码运行时还需要进行各种检查。(磨刀不误砍柴工)

 

   2,环节2:链接阶段之Preparation(准备)

    准备阶段,为类的静态变量分配内存,并将其初始化为默认值。

    当一个类验证通过时,虚拟机就会进入准备阶段。虚拟机就会为这个类分配相应的内存空间。

 

  注意:Java并不支持boolean类型,对于boolean类型,内存实现是int,由于int的默认值为0,故对于的是false。

  注意:

         这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显示赋值。

         注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

         在这个阶段并不会像初始化中那样有初始化或者代码执行。

        

package java11;

/**
 * @Author ylc
 * @Date 2022/4/6 11:32
 * @Version 1.0
 *
 * 过程二:链接阶段
 *
 * 基本数据类型:非final修饰的标量,在解析环节进行默认初始值。
 *            final修饰以后,在解析环节直接进行显示赋值。
 *
 *拓展:如果使用字面量的方式定义一个字符串的常量的话,也是解析环节直接进行显示赋值。
 */
public class Linking {
    private static long id;
    private static final int num = 1;

    public static final String constStr = "CONST";
    public static final String constStr1 = new String("CONST");
    
}

   3,环节3:链接阶段之Resolution(解析)

      解析阶段,将类,接口,字段和方法的符号引用转为直接引用。

 

通过解析操作,符号引用就可以 转为目标方法在类中方法表中的位置,从而使得方法被成功调用。

 

   总结:

       所谓解析就是将符号引用转为直接引用,也就就是得到类,字段,方法在内存的指针或者偏移量。可以说,如果直接引用存在,那么可以肯定系统中存在该类,方法,或者字段。但只存在符号引用,不能确定系统总一定存在该结构。

 

   字符串复习:

        当Java代码中直接使用字符串常量时,就会在类中出现CONSTANT_String,它表示字符串常量,并且会引用一个CONSTANT_UTF8常量项

        在Java虚拟机内部运行中的常量池中,会维护一张符流表,它会保存所有出现过的字符串常量,并且没有重重复项。


 

Initialization(初始化)阶段

    1,static与final的搭配问题

       初始化阶段,为类的静态变量赋予正确的初始值。

 

     具体描述

        类的初始化是类装载的最后一个阶段。如果起那么可以,那么表示可以顺利装载到系统中,这样类才会开始执行Java字节码(即:到了初始化阶段,才真正开始执行类中定义的 JAVA 程序代码)

        初始化阶段的重要工作是执行类的初始化方法:<clinit>()方法。

             该方法仅有Java编译器生成并由JVM调用,无法自定义一个同名方法,更无法直接在JAVA程序使用方法

             它是由类静态成员的赋值语句以及static语句块合并产生的。

 

      说明:

          在加载一个类之前,虚拟机试图加载该类父类,因此父类的<clinit>总是在子类<clinit>之前被调用,父类的static块优先级高于子类。

          那些类在编译为字节码后,字节码文件中将不会含<clinit>()方法?

               一个类中并没有声明任何的类变量,也没有静态代码块时

               一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。

               一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译是常量表达式。

 

      初始化阶段赋值和准备阶段赋值

 说明:使用static + final修饰字段的显示赋值的操作,到底是在哪个阶段进行赋值?
* 情况1:在链接阶段的准备环节赋值
* 情况2:在初始化阶段<clinit>()方法中赋值
*
* 结论:
* 在链接阶段的准备环节赋值的情况
* 1,对于基本数据类型的字段来说,如果使用static final修饰,则显示赋值通常是在链接阶段的准备阶段赋值
* 2,对于String来说,如果使用字面量的方法赋值,使用static final修饰的话是在链接阶段的准备阶段赋值
*
* 在初始化阶段<clinit>()中赋值的情况
* 排除上述的在准备阶段的情况之外情况
*
* 最终结论:使用static + final 修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型
 String类型的显示赋值,是在链接阶段的准备环节进行

 

public class InitializationTest2 {
    public static int a = 1;//<clinit>()中赋值
    public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

    public static final Integer INT_CONSTANT1 = Integer.valueOf(100);//<clinit>()
    public static  Integer INT_CONSTANT2 = Integer.valueOf(100);//<clinit>()

    public static final String s = "hello";//链接准备阶段
    public static final String s1 = new String("hello");//<clinit>()

    public static String s2 = "hello";//<clinit>()

    public static final int NUM1 = new Random().nextInt(10);//<clinit>()
}

 

    2,<clinit>()的线程安全性

       对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。

       虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确地加锁,同步,如果多个线程同时去初始化一个类,那么

          只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()完毕

 

    3,类的初始化情况:主动使用vs被动使用: 主动使用就会调用<clinit>()方法,被动使用就不会调用<clinit>()方法    -XX:TreaceClassLoading 追踪加载信息

     一,主动使用

      Class只有在必须首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或一个接口在初次使用前,必须进行初始化。这里值的 “使用”,是指主动使用,主动使用只有下来几种清理。

       1,当创建一个类的实例时,比如使用new关键字,或者通过反射,克隆,反序列化。

       2,当调用类的静态方法时,即当使用字节码invokestatic指令。

       3,当使用类,接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量,赋值变量操作)

       4,当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName("com.ylc,java.Test")。

       5,当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发器父类的初始化。

       6,如果一个接口定义为default方法,那么直接实现或者间接实现该接口的类初始化,该接口要在其之前被初始化。

       7,当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

       8,当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

 

       二,针对5,补充说明

       当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不使用与接口。

           1,在初始化一个类时,并不会初始化它所实现的接口

           2,在初始化一个接口时,并不会先初始化它的父接口

       因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,会导致该接口的初始化。

 

        三,针对7,说明

        JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用main()之前被接口和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。

 

        一,被动使用

  1,当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
     a,当通过子类引用父类的静态变量,不会导致子类初始化
  2,通过数组定义类引用,不会触发此类的初始化

 

public class PassiveUse1 {
    @Test
    public void test1(){
        System.out.println(Child.num);
    }

    @Test
    public void test2(){
        Paren[] parens = new Paren[10];
        System.out.println(parens.getClass());
        System.out.println(parens.getClass().getSuperclass());

        parens[0] = new Paren();
    }
}

class Paren{
    static {
        System.out.println("Paren的初始化过程");
    }

    public static int num = 1;
}

class Child extends Paren{
    static {
        System.out.println("Child初始化过程");
    }
}

  3,引用常量不会触发此类或者接口的初始化。因为常量在链接阶段就已经被显示赋值
  4,调用ClassLoader类的LoadClass()方法加载一个类,并不会对类的主动使用。

 

public class PassiveUse2 {
    @Test
    public void test1(){
        System.out.println(Person.NUM);
    }

    @Test
    public void test2(){
        try {
           Class clazz =  ClassLoader.getSystemClassLoader().loadClass("java11.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}


class Person{
    static {
        System.out.println("Person");
    }

    public static final int NUM = 1;//在链接过程的准备阶段就已经被赋值为1
    public static final int NUM1 = new Random().nextInt(10);//在链接过程的准备阶段就已经被赋值为1

}

 

类的Using(使用)

    任何一个类型在使用之前都必须经历完整,链接和初始化3个类加载。一旦经过这三个步骤,就等着使用。

     我们可以在程序中访问和调用它的静态类成员信息(如:静态字段,静态方法),或者使用new关键字为器创建对象。


 

类的Unloading(卸载)

 

loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接则直接引用它。

 

如果程序运行过程中,将三个引用变量都置为null,此时Sample对象结束声明周期,MyClassLoader对象结束声明周期,代表Sample类的Class对象也结束声明周期,Sample类在方法区内的二进制数据被卸载。

 

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不足重新加载,如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例。

 

类的卸载

     1,启动类加载器加载的类型在整个运行期间是不可能被卸载的

      2,被系统类加载器和扩展类加载的类型在运行期间不可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期总能直接或者间接的访问的到,其达到unreachable的可能性极小。

      3,被我们自定义的类加载器实例加载的类型只有很简单的上下文环境才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集器功能才可以做到。

 

 

  

 

 

    

posted on 2022-04-07 23:17  只想做加法(ylc)  阅读(171)  评论(0)    收藏  举报

导航