一:类加载子系统
作用:
负责加载.class文件,将类信息等放到方法区,如实例化的对象则放在堆区。
如上图,就是从类加载到最终运行的流程图。 当然,只是对于加载来说,类加载子系统只管加载,能不能运行由执行引擎决定。
在使用阶段之前,都属于加载阶段。下面来看一看:
- 加载:
- 预加载
- 虚拟机启动时候就加载,JAVA_HOME/lib下的rt.jar下的.class文件。
- 运行时加载
- jvm在使用到一个.class文件时候,先看看内存中有没有被加载,没有就根据全限定名加载。 这阶段做了三件事:
- 获取.class文件二进制流。
类信息、静态变量、字节码、常量这些 内容放入方法区中 (方法区在jdk8中分别存在于:堆和metaspace两个部分) 在内存中生成一个代表.class文件的java.lang.Class对象,作为在方法区中,各种数据访问该类的入口。
- 连接:
包含验证Verification , 准备Preparation , 解析Resolution 三个过程。
- 验证Verification
- 验证是否符合虚拟机规范,不会危害虚拟机自身安全。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符合引用验证
- 准备Preparation
- 在方法区中,为类变量分配内存和设置初始值。 注意两点
- 分配内存只是类变量-static修饰
- 赋初始值是指 不被final修饰的static变量(因为final修饰的不能再改变了,所以在这里就给真实值),如:public static int value = 123,这里赋初始值为0,后面的初始化阶段才会赋值为123。
- 注意:这里说的是类变量设初始值,对于局部变量是没有这个过程的。 因此,static int a; 可以赋初始值0,但是在方法中 int a; 则编译不通过。
- 解析Resolution
- 虚拟机将常量池内的符号引用替换为直接引用的过程, 大体工作分为: 类或接口的解析 类方法解析 接口方法解析 字段解析
- 初始化:
前面阶段都是jvm主导,初始化阶段才真正执行代码,交给程序主导。
初始化简单来说,就是调用类构造器 <cinit>方法, 用于类变量的赋值(真实赋值,不是在准备阶段的赋值)或静态块的执行。换句话说,如果没有静态变量需要赋值,也没有static代码块需要执行,那么编译器不会为当前类生成 <cinit>方法。
<cinit>方法的执行顺序,先父类后子类,同类中按顺序执行。 优先实例对象的构造方法,示例如下:
执行顺序:
- 父类A的<cinit>方法(及静态代码块)
- 子类B的<cinit>方法
- 第一次调用 new SonB()时,先调用父类A的构造方法,再调用B的构造方法
- 第二次调用 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使用方式,其实都是破坏了双亲委派机制的。 所以,双亲委派和破坏双亲委派,在不同的场景下,都有自己的用处。