JVM上篇:类加载子系统

JVM类加载

1.内存结构概述

  1. 类加载器子系统
    1. Loading阶段,加载class文件
    2. Linking阶段
      1. 验证
      2. 准备
      3. 解析
    3. Initialization阶段,初始化
  2. 运行时数据区
    1. 方法区,把需要引用的类的字节码文件都会加载到方法区,存放类的信息,方法信息等等
    2. 堆(heap)区,创建java对象的主体都分配到堆空间中
    3. 虚拟机栈,每个线程一份
    4. pc寄存器(程序计数器),每个线程一份
    5. 本地方法栈,本地方法接口调用,使用本地方法栈
  3. 执行引擎
    1. 解释器
    2. 即时编译器
    3. 垃圾回收期

image-20220222215653975

2.类加载子系统概述

  1. 类加载器子系统的作用

    ClassLoader负责从文件系统或者从网络中加载Class文件,至于它是否可以运行,有Execution Engine来决定。

  2. 加载类到哪里?

    加载的类的信息存放在内存空间的“方法区”,除了类的信息外,方法区还会存放运行时的常量的映射等信息。

3.类的加载过程

2.1加载

  1. 通过类的全类名获取Class文件的二进制流
  2. 将加载进来的字节流转化为方法区运行时的数据结构
  3. 在内存中生成一个这个类的对象Class对象,作为方法区这个类的各种数据的访问入口

2.2Linking

2.2.1验证(Verify)
  1. 确保Class文件的字节流中的信息正确,保证类加载的正确性
  2. 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
  3. 例如下面一个元数据的验证,java字节码文件初始文件都是CA FE BA BE

image-20220216231806385

2.2.2准备(Prepare)
  1. 为类变量分配内存,并设置类变量的默认初始值,即零值
  2. final修饰的常量,在编译的时候就会分配值了
  3. 不会对实例变量进行初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到堆空间中
2.2.3解析(Resolve)
  1. 将常量池内的符号引用,转化为直接引用。

    例:Object类的引用

2.3初始化(Initlization)

  1. 初始化阶段就是执行类构造器方法<clinit>()的过程,他和类的构造器<init>()是不一样的。

    此方法是javac编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来

  2. 执行顺序按照编写的代码的顺序执行

  3. <init>为类的构造器方法,任何一个类声明以后,内部至少存在一个类的构造器

  4. 若被加载的类具有父类,JVM会保证子类的<clinit>()执行之前,父类的<clinit>()已经执行完毕

举例:

  1. 定义父类

    public class initTest {
            public static int A = 1;
            static {
                A=2;
            }
    }
    
  2. 定义子类

    public class Son extends initTest {
        public static int B =A;
        public static void main(String[] args) {
            System.out.println(B);
        }
    }
    
  3. 编译运行

    image-20220217222629431

  4. 查看字节码文件,可以看到加载B的时候先调了父类的clinit,加载了A

    image-20220217222811967

  5. 虚拟机必须保证一个类的<clinit>()方法在多线程的情况下被同步加锁

举例:

  1. 编写被调用的class

    public class initTest {
        static {
            //这里使用循环,目的是为了卡住<clinit>()方法,让别的线程等待
            if (true){
                System.out.println(Thread.currentThread().getName()+"进来了");
                while (true){
                }
            }
        }
    }
    
  2. 编写两个线程,两个线程都加载initTest,因为对于JVM来说,同一个类只会被加载一次,加载以后类信息等存放在方法区中

    public class Son {
        public static void main(String[] args) {
            Runnable r = () ->{
                System.out.println(Thread.currentThread().getName()+"开始");
                initTest initTest = new initTest();
                System.out.println(Thread.currentThread().getName()+"结束");
            };
    
            Thread t1 = new Thread(r,"线程1");
            Thread t2 = new Thread(r,"线程2");
    
            t1.start();
            t2.start();
        }
    }
    
  3. 编译运行,第一个加载initTest的线程会进去,而另一个加载initTest的线程则会在后面等待

    image-20220217225306944

4.类加载器的分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader),和自定义类加载器(User-Defined ClassLoader)

JVM将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

可能你会疑惑,拓展类加载器,和系统类加载器是什么类型,其实他们都是派生于ClassLoader类的,JVM均视为自定义类加载器

引导类加载器,拓展类加载器,系统类加载器,用户自定义加载器,这四者的关系是包含关系。不是子父继承的关系

3.1虚拟机自带的加载器

3.1.1引导类加载器(Bootstrap ClassLoader)
  1. 这个类加载器使用c/c++实现,嵌套与JVM内部
  2. 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resource.jar或者 sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类
  3. 它并不继承与java.lang.ClassLoader,c语言编写,无父加载器
  4. 它用来加载拓展类加载器和系统类加载器(应用程序类加载器),并指定Bootstrap为他们的父加载器
  5. 出于安全考虑,Bootstrap ClassLoader只加载包名为java,javax,sun等开头的类
3.1.2拓展类加载器(Extension ClassLoader)
  1. java语言编写,为sun.misc.Launcher的一个内部类
  2. 派生于ClassLoader类
  3. 父加载器为引导类加载器
  4. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK安装目录的jre/lib/ext子目录下加载类库,如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
3.1.3系统类加载器(应用程序类加载器 AppClassLoader)
  1. java语言编写,为sun.misc.Launcher的内部类
  2. 派生于ClassLoader类
  3. 父类加载器为拓展类加载器
  4. 它负责加载环境遍历classpath或者是系统属性java.class.path指定路径下的类库
  5. 该类加载器是程序中默认的类加载器,一般来说,我们编写的Java应用的类都是由它来完成加载
  6. 通过ClassLoader#getSystemClassLoader()的方法,可以获取到该类加载器

举例:

import sun.misc.Launcher;

import java.net.URL;
import java.security.Provider;

public class ClassLoaderTestFyp {
    public static void main(String[] args) {
//        获取引导类加载器加载的路径
        System.out.println("====================引导类加载器加载的路径==========================");
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL.toExternalForm());
        }
        //从上面的路径中随意选择一个类,看一下这个类的类加载器是什么
        System.out.println("====================获取引导类加载器===============================");
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader); //应该为null,因为引导类加载器是c和c++编写,我们无法获取到

//        拓展类加载器
        System.out.println("====================拓展类加载器加载的路径==========================");
        String extDirs = System.getProperty("java.ext.dirs");
        System.out.println(extDirs);

//        系统类加载器classpath
        System.out.println("====================系统类加载器加载的路径==========================");
        String classpath = System.getProperty("java.class.path");
        for (String s : classpath.split(";")) {
            System.out.println(s);
        }
    }
}
3.1.4用户自定义类加载器

为什么要自定义类的加载器:

  1. 隔离加载类,(主要解决不同版本jar包的版本冲突)
  2. 修改类加载的方式
  3. 扩展加载源
  4. 防止源码泄露

自定义类加载器实现步骤:

  1. 继承java.lang.ClassLoader抽象类
  2. 在jdk1.2之后,建议把自定义的类加载器逻辑写在findclass当中
  3. 如果没有太过于复杂的需求,可以继承URLClassLoader,可以避免自己去读取字节码流的方式,避免自己写URLClassLoader

5.ClassLoader介绍

ClassLoader是一个抽象类,其后所有的类加载器都继承自ClassLoader(除引导类加载器)

img

获取ClassLoader的途径
  • 方式一:获取当前ClassLoader
clazz.getClassLoader()
  • 方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
  • 方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
  • 方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()

6.双亲委派机制

Java虚拟机对class文件采用的按需加载的方式,也就是说当需要使用此类的时候,才会把这个类的class文件加载到内存,生成class对象。且,在加载某个类的class文件的时候,Java虚拟机采用的是双亲委派模式,即把请求交给父类加载器处理,它是一种任务委派模式。

工作原理
  1. 如果一个类加载器收到了类加载的请求,它并不会先自己去加载这个类,而是把这个请求委托给父类的加载器去执行
  2. 如果父类加载器还存在其父类加载器,则依次向上委托,请求最终将到达顶层的引导类加载器
  3. 如果父类加载器可以完成类加载的任务,则就成功返回,如果父类加载器无法完成类加载的任务,则子加载器才会自己尝试去加载类,这就是双亲委派模式。

image-20220222131735350

举例:

当我们加载jdbc.jar包用于实现数据库连接的时候,首先我们要知道的是jdbc.jar是基于SPI接口进行实现的,所以在加载的时候会进行双亲委派,从引导类加载器加载SPI核心的类,然后再加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类jdbc.jar的加载。

image-20220222212254372

优势
  1. 避免类的重复加载

  2. 保护程序安全,防止核心的API被随意的修改

    举例:假设我们自己实现了一个java.lang.String,如果给这个类写个main方法,去运行,是不行的,因为当类加载器收到类加载请求的时候会向上委托,会加载核心的String类,而核心的String类无此方法,所以会报错为方法找不到,这也称为沙箱安全机制

7.其他

  1. 如何判断两个Class对象是否相等

    在JVM中表示两个Class对象是否为同一个类,有两个必要的条件

    • 类的全类名必须相同
    • 加载这个类的ClassLoader必须相同

    也就是说,即使两个类对象来自同一个class文件,但是加载他们的类加载器不同,那这两个对象也是不想等的。

8.类的主动使用和被动使用

java程序对类的使用方式分为:主动使用和被动使用

主动使用,又分为7种情况:

  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对这个静态变量赋值。
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动的时候被标明为启动类的类
  • JDK 7 开始提供的动态语言的支持

除了以上7种情况,其他使用java类的方式,都看作是对类的被动使用不会导致类的初始化

posted @ 2022-03-08 20:20  范育萍  阅读(343)  评论(0编辑  收藏  举报