类的加载过程详解(类的生命周期)

加载阶段(Loading)

获取二进制流的方法

类模型与class实例的位置

类模型的位置

加载的类在JVM中创建类结构,类结构会存储在方法区

Class实例的位置

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

外部可以通过访问order的class对象来获取order的类数据结构

数组类的加载

链接阶段(Linking)

验证阶段

验证的目的是保证加载的字节码是合法、合理并且是符合规范的

注意:

  • 格式验证会和加载阶段同时进行,验证通过,类加载器才会将而二进制数据信息加载到方法区中
  • 格式检查之外的验证都是在方法区内进行

准备阶段

为类的静态变量(包含引用类型)分配内存空间,并将其初始化为默认值

注意

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

    • 准备阶段是为变量分配空间,所以final修饰的是常量,所以直接对其赋值
  • 基本数据类型:

    • 非final修饰的变量,在准备环节进行默认初始化赋值。
    • final 修饰以后,在准备环节直接进行显示赋值。
    • 拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备坏节直接进行显示赋值。
  • 这里并不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中

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

解析阶段

将类、接口、字段、和方法的符号引用转化为直接引用

就是逻辑地址转化为物理地址

初始化

初始化过程和类的初始化

为类的静态变量赋予正确的初始值

类的初始化时类装载得最后一个环节

到了初始化阶段,才真正开始执行类中定义得java字节码

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

说明

  1. 在加载一个类之前往往会先加载给类的父类,因此父类的<clinit>总是在子类的<clinit>之前执行,换句话说,即使父类的static块的优先级高于子类的static的代码块
  2. 并不是所有的类都会执行 <clinit> 不会执行clinit方法的有:
    • 一个类中并没有声明任何的类变量,也没有静态代码块时
    • 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
    • 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式

初始化阶段赋值与准备阶段赋值的比较

/**
 * 哪些场景下,java编译器就不会生成<clinit>()方法
 */
public class InitializationTest1 {
    //场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public int num = 1;
    //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
    public static int num1;
    //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public static final int num2 = 1;
}



package com.jvm.中篇.chapter03.src.com.atguigu.java;

import java.util.Random;

/**
 * 说明:使用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 INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
    public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值

    public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
    public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值

    public static String s2 = "helloworld2";   //在链接阶段的准备环节赋值

    public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
}

<clinit>的线程安全

如果多线程同时区初始化一个类,那么只会有一个线程区执行这个类的<clinit>方法,其他线程均需要阻塞等待,直到活动线程执行<clinit>方法结束

如果之前的线程成功加载了类,则当使用这个类时,jvm会直接返回给他已经准备好的信息

类的主动和被动使用

主动使用:会执行初始化阶段 调用 <clinit>方法 (会执行类的静态块)

被动使用

  •  Snipaste_2023-06-02_14-24-49

    注:第二点 new数组的内部成员的对象会存在主动引用 第三点 这里的常量指的是字面量 第四点 使用forName()会导致类的主动使用

类的使用Using

开发人员可以在程序中访问和调用它的静态类成员信息,或者使用new关键在为其创建对象实例

类的卸载 Unloding

一个类何时结束生命周期,取决于它的class对象和时结束生命周期

类的加载器

类加载器是jvm执行类加载机制的前提。

ClassLoader的作用

  • classLoader是java的核心组件,负责将Class信息的二进制流读入到jvm内部,转化为与目标类对应的java.lang.class对象实例。然后交给jvm进行链接、初始化等操作。
  • 所有的class都是由类加载器进行加载的。
  • 类加载器只能影响到加载阶段,无法改变类的链接、初始化阶段
  • 类加载器并没有绑定在jvm内部,可以更加灵活和动态地执行类加载操作。

类加载分类:显示加载和隐式加载

加载方式是指JVM加载class文件到内存的方式

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getclassLoader( ).loadclass()加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
public class UserTest {
    public static void main(String[] args) {
        User user = new User(); //隐式加载

        try {
            Class clazz = Class.forName("com.atguigu.java.User"); //显式加载
            ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java.User");//显式加载
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

类加载器的必要性

命名空间

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。



package com.jvm.中篇.chapter04.src.com.atguigu.java;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author shkstart
 * @create 14:22
 */
public class UserClassLoader extends ClassLoader {
    private String rootDir;

    public UserClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 编写findClass方法的逻辑
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class对象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
     */
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            // 读取类文件的字节码
            while ((len = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     */
    private String classNameToPath(String className) {
        return rootDir + "\\" + className.replace('.', '\\') + ".class";
    }

    public static void main(String[] args) {
        String rootDir = "D:\\code\\workspace_idea5\\JVMDemo1\\chapter04\\src\\";

        try {
            //创建自定义的类的加载器1
            UserClassLoader loader1 = new UserClassLoader(rootDir);
            Class clazz1 = loader1.findClass("com.atguigu.java.User");

            //创建自定义的类的加载器2
            UserClassLoader loader2 = new UserClassLoader(rootDir);
            Class clazz2 = loader2.findClass("com.atguigu.java.User");

            System.out.println(clazz1 == clazz2); //clazz1与clazz2对应了不同的类模板结构。 false
            
            这里相当于new了两个类加载器,类型相同,但是在堆中的内存位置是不同的,所以信息也不同
                
            System.out.println(clazz1.getClassLoader());
            System.out.println(clazz2.getClassLoader());

            //######################
            Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java.User");
            System.out.println(clazz3.getClassLoader());


            System.out.println(clazz1.getClassLoader().getParent());

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


    }

}

类的加载机制的三个基本特征

  • 双亲委派机制
  • 可见性
    • 子类加载器可以访问父类加载器加载的类型 但反过来不允许
  • 单一性
    • 因为父类加载器的类型对于子类是可见的,所以父类加载器中加载过的类型,就不会在子类加载器中重复加载
    • 但是注意,类加载器“邻居”间,同一类型任然可以被加载多次,因为互相并不可见

类的加载器详解

类加载器分为两种 引导类加载器 和 自定义加载器

  • 所有派生于抽象类ClassLoader的类加载器都是自定义加载器

  • 除了顶层的启动类加载器外,其余的类加载器都应当由自己的“父类加载器”。

  • 不同类加载器看似是继承关系,实际上是包含关系,在下层加载器中,包含着上层加载器的引用

class ClassLoader{
                                    ClassLoader parent; //父类加载器
                                    public ChildClassLoader(ClassLoader parent){
                                    this parent = parent
                                    }
                                    }

                                    class ParentClassLoader extends ClassLoader{
                                    public ParentClassLoader(ClassLoader parent){
                                    super(parent);
                                    }
                                    }

                                    class ChildClassLoader extends ClassLoader{
                                    public ChildClassLoader(ClassLoader parent){// parent = new ParentClassLoader();
                                        换句话说就是父类加载器作为子类加载器的属性出现
                                    super(parent);
                                    }
                                    }

引导类加载器

  • Bootstrap类加载器包名为java、javax、sun等开头的类
  • 没有父类加载器 本身是c++编写 不会有java的继承结构
  • 加载扩展类加载器和应用程序类加载器
  • 没有嵌套在jvm中

扩展类加载器

  • java语言编写
  • 继承于ClassLoader

应用程序类加载器

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过classLoader的getSystemClassLoader()方法可以获取到该类加载器

用户自定义加载器

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 同时,自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于classLoader。

关于数组类型的加载,使用的类的加载器于数组元素使用的加载器相同

基本数据类型不需要类的加载器

ClassLoader原码解析

ClassLoader主要方法

findClassload是URLClassLoad里面的方法 在 扩展类加载器和应用程序加载器中没有该方法

满足双亲委派机制 采用的是递归的思想

                                    protected Class<?> loadClass(String name,
                                    boolean resolve) //true-加载class的同时进行解析操作
                                    throws ClassNotFoundException
                                    {
                                    synchronized (getClassLoadingLock(name)) {
                                    //同步操作,保证只能加载一次
                                    // 首先,在缓存中判断是否已经加载到同名的类
                                    Class<?> c = findLoadedClass(name);
                                    if (c == null)
                                    {
                                    long t0 = System.nanoTime();
                                    try {
                                    // 获取当前加载器的父类加载器
                                    if (parent != null) {
                                    // 如果存在父类加载器,就调用父类加载器进行类加载
                                    c = parent.loadClass(name, false);
                                    } else {
                                    // 不存在父类加载器 即父类加载器就是引导类加载器 就使用启动类加载器进行加载
                                    c = findBootstrapClassOrNull(name);
                                    }
                                    } catch (ClassNotFoundException e) {
                                    // ClassNotFoundException thrown if class not
                                        found
                                    // from the non-null parent class loader
                                    }

                                    if (c == null)
                                    { // 父类加载器没有加载该类 或者该类的加载器没有加载该类
                                    //调用当前类加载器的findClass()
                                    long t1 = System.nanoTime();
                                    c = findClass(name);

                                    // this is the defining class loader; record the
                                        stats
                                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                                    sun.misc.PerfCounter.getFindClasses().increment();
                                    }
                                    }
                                    if (resolve) { //
                                        是否进行解析操作
                                    resolveClass(c);
                                    }
                                    return c;
                                    }
                                    }
 protected Class<?> findClass(final String name)throws ClassNotFoundException{

 ******
if (res != null) {
      try {
            //直接defind查询到的结果返回出去作为fiandclass的结果
          return defineClass(name, res);
      } catch (IOException e) {
          throw new ClassNotFoundException(name, e);
      }
  } else {
      return null;
  }
     
     ****
 }

SecureClassLoader与URLClassLoader

Class.forName()与ClassLoader.loadClass()

  • class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化
    • 如:class.forName( “com.atguigu.java.Helloworld” );
  • classLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到个classLoader对象,所以可以根据需要指定使用哪个类加载器.
    • 如:ClassLoader cl=…… .;
      c1.loadclass( “com.atguigu.java.Helloworld” );

双亲委派机制

双亲委派机制,这种机制能够更好的保证Java平台的安全

向上委托,向下加载

优势

  1. 避免类的重复加载,保证了一个类全局的唯一性
    • 体现在 在某一个类加载器中只能加载一次 同时 不能有多个类加载器都可以加载该类
  2. 保证了核心类库不被随意修改,保证程序安全

双亲委派机制代码体现

loadClass源码

双亲委派机制弊端

检查类是否接受委托是单向的,使得结构非常清晰,但会带来一个问题就是 顶层的类加载器不能访问底层的Classloader所加载的类

如果一个类被底层的类加载器加载了,那么顶层的类加载器确实无法直接访问这个类。但是,如果顶层的类加载器需要加载一个与底层类加载器加载的类同名的类时,它会从自己已经加载的类中查找,而不会去查找底层类加载器中已经加载的类。这意味着,同名的类在不同类加载器中是两个不同的类,它们并不相互影响。

思考

破坏双亲委派机制的行为

默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类

package com.jvm.中篇.chapter04.src.com.atguigu.java1;

                                    import java.lang.reflect.Method;


                                    public class
                                    LoopRun {
                                    public static
                                    void main(String
                                        args[]) {
                                    while (true)
                                    {
                                    try {
                                    //1. 创建自定义类加载器的实例
                                    MyClassLoader loader = new MyClassLoader("D:\\code\\workspace_idea5\\JVMDemo1\\chapter04\\src\\");
                                    //2. 加载指定的类
                                    Class clazz = loader.findClass("com.atguigu.java1.Demo1");
                                    //3. 创建运行时类的实例
                                    Object demo = clazz.newInstance();
                                    //4. 获取运行时类中指定的方法
                                    Method m = clazz.getMethod("hot");
                                    //5. 调用指定的方法
                                    m.invoke(demo);
                                    Thread.sleep(5000);
                                    } catch (Exception e) {
                                    System.out.println("not find");

                                    try {
                                    Thread.sleep(5000);
                                    } catch (InterruptedException ex) {
                                    ex.printStackTrace();
                                    }

                                    }
                                    }
                                    }

                                    }


                                    package
                                    com.jvm.中篇.chapter04.src.com.atguigu.java1;

                                    public class
                                    Demo1 {
                                    public void
                                    hot()
                                    {
                                    System.out.println("OldDemo1--->
                                        NewDemo1");
                                    }

                                    }

                                    

沙箱安全机制

Java安全模型的核心就是Java沙箱

沙箱机制就是将java代码限定在虚拟机特定的运行范围内,并且严格限制对本地系统资源的访问

自定义加载器

设置自定义加载器的原因

  • 隔离加载类
    • 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境中
    • 防止类仲裁 导致类冲突(不同jar包版本冲突)
  • 修改类的加载方式
    • 类的加载模型并非强制,除引导类加载器外,其他类加载器动态的按需加载
  • 扩展加载源
    • 可以从 数据库、网络、甚至是机顶盒中进行加载
  • 防止源码泄露
    • Java代码容易被篡改和编译、可以进行加密编译,所以类加载也需要自定义,还原加密的字节码

注意:

  • 在Java类型转化的时候,只有两个类型都是由同一个加载器所加载,才能进行类型转化,否则转化时会发生异常

实现自定义加载器

所有用户自定义的加载器都需要继承ClassLoader类

在定义ClassLoader的子类时可以有两种做法:

  1. 重写findclass() 建议
  2. 重写loadclass()

这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改
loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。

  • loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
  • 当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。
package com.jvm.中篇.chapter04.src.com.atguigu.java2;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author shkstart
 * @create 15:20
 * 自定义ClassLoader
 */
public class MyClassLoader extends ClassLoader {
    private String byteCodePath;

    public MyClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }

    public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.byteCodePath = byteCodePath;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //获取字节码文件的完整路径
            String fileName = byteCodePath + className + ".class";
            //获取一个输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //获取一个输出流
            baos = new ByteArrayOutputStream();
            //具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //获取内存中的完整的字节数组的数据
            byte[] byteCodes = baos.toByteArray();
            //调用defineClass(),将字节数组的数据转换为Class的实例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;


    }
}


package com.jvm.中篇.chapter04.src.com.atguigu.java2;

public class MyClassLoaderTest {
    public static void main(String[] args) {
        MyClassLoader loader = new MyClassLoader("d:/");

        try {
            Class clazz = loader.loadClass("Demo1");
            System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());

            System.out.println("加载当前Demo1类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

java9 jvm新特性