【jvm-2】类加载机制

一:类加载子系统

作用:

负责加载.class文件,将类信息等放到方法区,如实例化的对象则放在堆区。

如上图,就是从类加载到最终运行的流程图。 当然,只是对于加载来说,类加载子系统只管加载,能不能运行由执行引擎决定。


 

类加载的执行过程: 类从被加载进jvm内存,执行,卸载出内存,总共包含7个阶段:
 

 

在使用阶段之前,都属于加载阶段。下面来看一看:

  • 加载:
  • 预加载
    • 虚拟机启动时候就加载,JAVA_HOME/lib下的rt.jar下的.class文件。
  • 运行时加载
    • jvm在使用到一个.class文件时候,先看看内存中有没有被加载,没有就根据全限定名加载。  这阶段做了三件事:
      1. 获取.class文件二进制流。
      2. 类信息、静态变量、字节码、常量这些 内容放入方法区中 (方法区在jdk8中分别存在于:堆和metaspace两个部分)
      3. 在内存中生成一个代表.class文件的java.lang.Class对象,作为在方法区中,各种数据访问该类的入口。
  • 连接:

包含验证Verification , 准备Preparation , 解析Resolution 三个过程。

  • 验证Verification
    • 验证是否符合虚拟机规范,不会危害虚拟机自身安全。
    1. 文件格式验证
    2. 元数据验证
    3. 字节码验证
    4. 符合引用验证
  • 准备Preparation
    • 在方法区中,为类变量分配内存和设置初始值。 注意两点
    1. 分配内存只是类变量-static修饰
    2. 赋初始值是指 不被final修饰的static变量(因为final修饰的不能再改变了,所以在这里就给真实值),如:public static int value = 123,这里赋初始值为0,后面的初始化阶段才会赋值为123。
    • 注意:这里说的是类变量设初始值,对于局部变量是没有这个过程的。 因此,static int a; 可以赋初始值0,但是在方法中 int a; 则编译不通过。
  • 解析Resolution
    • 虚拟机将常量池内的符号引用替换为直接引用的过程,  大体工作分为: 类或接口的解析 类方法解析 接口方法解析 字段解析
  • 初始化:

前面阶段都是jvm主导,初始化阶段才真正执行代码,交给程序主导。

初始化简单来说,就是调用类构造器 <cinit>方法, 用于类变量的赋值(真实赋值,不是在准备阶段的赋值)或静态块的执行。换句话说,如果没有静态变量需要赋值,也没有static代码块需要执行,那么编译器不会为当前类生成 <cinit>方法。

<cinit>方法的执行顺序,先父类后子类,同类中按顺序执行。 优先实例对象的构造方法,示例如下:

 

执行顺序:

  1. 父类A的<cinit>方法(及静态代码块)
  2. 子类B的<cinit>方法
  3. 第一次调用 new SonB()时,先调用父类A的构造方法,再调用B的构造方法
  4. 第二次调用 new SonB()时,先调用父类A的构造方法,再调用B的构造方法

 

类加载器:

  • 作用:

 使用某个类时,将.class文件二进制数据加载到内存并放到方法区中,生成java.lang.Class封装其结构。 注意,使用的时候才加载。

  • 加载器分类:

  • 启动类加载器(引导类加载器): 

由C编写,嵌套在jvm内部,加载jvm自身需要的类。 

目录:JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路径下的内容

  • 扩展类加载器:

加载jre/lib/ext下的类,父类为启动类加载器

  • 系统类加载器:

它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库,也就是我们程序中的类。

  • 用户自定义类加载器:

特定场景下,我们需要自定义类加载器。


 

双亲委派机制:

当一个类加载器收到加载请求后,依次委托给父类加载器。 如果所有的上层加载器都不能加载,再由子类加载器加载。  这样保证特定的类只能由特定的加载器加载。

  • 双亲委派加载流程

ClassLoader 类默认的 loadClass实现了双亲委派的流程:

 

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

 

protected Class<?> loadClass(String name, boolean resolve)
    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) { // 到这里还没有加载成功,自己加载
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);  // 调用findClass方法自己加载,所以,如果自定义类加载器,就需要重写该方法了

                // 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;
    }
}
  • 自定义类加载器

在上面,我们知道自定义需要冲洗findClass方法,将指定名称等到Class对象。 需要做两步:

  • 将指定名称路径文件转化为流,读取为字节数组。
  • 调用Java 提供了 defineClass 方法把数组转化为Class对象。

 

public class MyClassLoader extends ClassLoader {
    private String codePath;

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

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //1.字节码路径
            String fileName = codePath + name + ".class";
            //2.获取输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //3.获取输出流
            baos = new ByteArrayOutputStream();
            //4.io读写
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //5.获取内存中字节数组
            byte[] byteCode = baos.toByteArray();
            //6.调用defineClass 将字节数组转成Class对象,这个也是父类方法
            Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
            return defineClass;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

当然了,也可以直接重写loadClass方法,但是这样会破坏双亲委派机制。 看实际情况而定,一般推荐重写findClass方法。

 

二:tomcat类加载机制

详解:https://www.cnblogs.com/aspirant/p/8991830.html

了解了jvm双亲委派机制,我们知道可以严格保证唯一性和安全性,下面来看一下tomcat类加载机制。

如图: 在前面部分是运用了双亲委派机制的,用于加载基础类。 但是对于web服务器,可以要求一个服务上部署多个web程序实例,每个实例中可以应用不同的类,或者同一类的不同版本,也就是说,类可以不唯一,就需要相互隔离另外,对于一些共用类,需要唯一,不能加载多次。 这就是tomcat设计自己的类加载器(下面灰色部分)的目的:

  • CommonClassLoader:Tomcat最基本的类加载器,加载路径/common/*中的class,可以被Tomcat容器本身以及各个Webapp访问。
  • CatalinaClassLoader: Tomcat容器私有的类加载器,加载路径/server/*中的class,加载路径中的class对于Webapp不可见。
  • SharedClassLoader: 各个Webapp共享的类加载器,加载路径/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)中的class,只对于所有Webapp可见。

注:上面3个路径在tomcat6后都合并到tomcat的lib目录了。

  • WebAppClassLoader: 各个Webapp私有的类加载器,加载路径/WebApp/WEB-INF/classes和/WebApp/WEB-INF/lib中的class(classes的先加载,存放程序的类;lib后加载,存放程序引用的第三方jar),只对当前Webapp可见;(项目经过tomcat编译后有此目录)
  • 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

显然,tomcat类加载机制,破坏了jvm原生的双亲委派机制(半破坏),目的是为了实现隔离性。 在这种不遵守双亲委派机制情况下,如果commonClassLoader想去加载WebAppClassLoader中的类怎么办(就是上层类加载器想加载下层的类)?  那么只需要使用线程上下文传递即可,也就是父类加载器通过当前线程请求子类加载器去加载。 这种应用很多,java 的 SPI就是: 

SPI就是父类定义接口,各子程序自己去实现,在父类中也能加载到想要的子类。 结合数据库驱动类接口Driver来看看:

首先,Driver接口定义在jdk当中,由各个数据库的服务商来提供实现, 比如mysql的就写了MySQL Connector,平时我们通过DriverManager来获取数据库的Connection

String url = "jdbc:mysql://localhost:3306/testdb";

Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

但是,DriverManager也是jdk提供,按理说由启动类加载器加载,但是又是怎么能加载到具体的实现类呢?

首先,在调用DriverManager的时候,会先初始化类,调用其中的静态块:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

在loadInitialDrivers()中

 

ServiceLoader的实现:

 

那么这个线程上下文加载器是什么呢? 在哪里设置到线程的上下文中的?

往下一直查找,其实在Launcher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器。  也就是说,DriverManager最终使用了应用加载器AppClassLoader去加载实现类。 我们接着看,在哪个路径下加载的?继续往下理:ServiceLoader.load(service, cl),在ServiceLoader中定义了加载路径:

最终加载方法是,通过此路径和接口名,将所有实现类加载到一个迭代器中,最终根据配置取出要使用的那一个。

所以,这就是为什么使用SPI要把实现类的全限定名配置到META-INF/services/目录下。 在mysql的驱动包中也配置了:

假如这时候我们使用的是springboot,会在配置文件指定:

datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/promotion_test?createDatabaseIfNotExist=true&useSSL=false&characterEncoding=utf-8&rewriteBatchedStatements=true
  username: aa
  password: bb

所有的SPI使用方式,其实都是破坏了双亲委派机制的。   所以,双亲委派和破坏双亲委派,在不同的场景下,都有自己的用处。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值