虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备和解析三个部分统称为连接。

  • 加载:加载是类加载的第一个阶段,这个阶段,首先要根据类的全限定名来获取定义此类的二进制字节六,讲字节六转化为方法区运行时数据结构。在 Java 堆生成一个代表这个类的 java.lang.class 对象,作为方法区的访问入口。

  • 验证:这一步的的目的是确保 class 文件的字节六包含的信息符合虚拟机的要求。

  • 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都会在方法区中进行分配。仅仅是类变量,不包括实例变量。

    public static int value = 123;

    变量在准备阶段过后的初始值为0而不是123,123的赋值要在变量初始化以后才会完成。

  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 初始化:初始化是类加载的最后一步,这一步会根据程序员给定的值去初始化一些资源。

加载是我们使用一个类的第一步,加载是如何完成的那?

类加载器

虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到 Java 虚拟机外部去实现,以便让程序自己去决定如何获取所需要的类,这个动作的代码模块称为类加载器

对于一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,比较两个类是否相等需要在这两个类是由同一个类加载器加载的前提下才有意义。

  • 启动类加载器(Bootstrap ClassLoader):这个类负责将 \lib 目录中的类库加载到内存中,启动类加载器无法被 Java 程序直接引用。
  • 扩展类加载器(Extension ClassLoader):负责加载 \lib\ext 目录中的类。开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是 ClassLoader 中 getSystemClassLoader() 方法的返回值,所以一般称为系统类加载器。如果没有自定义过加载器,一般情况下这个就是默认的类加载器。
  • 自定义类加载器(User ClassLoader):通过自定义类加载器可以实现一些动态加载的功能,比如 SPI。

双亲委派模型

JVM 在加载类时默认采用的是双亲委派模型机制。通俗地讲,某个特定的类加载器在接到加载类的请求时,首先讲加载任务委托给父类加载器,因此所有加载请求最总都应该传送到顶层的启动类加载器中。如果父类无法完成加载请求,子类才会尝试自己加载。

所以,越基础的类会由越上层的加载器加载。

如果不使用双亲委派模型,用户自己写一个 Object 类放入 ClassPath,那么系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证。现在你可以尝试自己写一个名为 Object 的类,可以被编译,但永远无法运行。因为最后加载时都会先委派给父类去加载,在 rt.jar 搜寻自身目录时就会找到系统定义的 Object 类,所以你定义的 Object 类永远无法被加载和运行。

双亲委派模型的好处是保证了核心类库不配覆盖和篡改。

打破双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器实现方式。Java 世界中大部分的类加载器都遵循这个模型。

双亲委派模型第一次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的 API。那如果基础类又要调用回用户的代码,怎么办?

比如 JNDI 服务,JNDI 现在是 Java 的标准服务,他的代码由启动类加载器去加载(rt.jar),但 JNDI 需要由独立厂商实现并部署在应用程序的 Class Path 下的 JNDI 接口提供者的代码,启动类加载器不可能认识这些代码,因为启动类加载器的搜索范围找不到用户应用程序类。

为了解决这个问题,Java 团队设计了一个不太优雅的设计:线程上下文加载器这个类加载器可以通过java.lang.Thread类的setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。

有了这个线程上下文加载器,JNDI 服务使用线程上下文加载器去加载所需要的 SPI 代码。也就是父类加载器请求子类加载器完成类加载的动作,这就打破了双亲委派模型。典型的例子有 JNDI 和 JDBC 等。

Tomcat 类加载机制

Tomcat 的类加载机制是违反了双亲委派原则的,对于一些为加载的非基础类(Object,String)等,各个 web 应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给 commonClassLoader 走双亲委派模型。整体结构如下:

这其中 JDK 提供的类加载器分别是:

  • Bootstrap-启动类加载器,JVM 的一部分,加载 <JAVA_HOME>/lib/ 目录下特定的文件
  • Extension-扩展类加载器,加载 <JAVA_HOME>/lib/ext/ 目录下的类库。
  • Application-应用程序类加载器,也叫系统类加载器,加载 CLASSPATH 指定的类库。

Tomcat 自定义类加载器分别是:

  • Common - 父加载器是 AppClassLoader,默认加载 ${catalina.home}/lib/ 目录下的类库
  • Catalina - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 server.loader 配置的资源,一般是 Tomcat 内部使用的资源
  • Shared - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 shared.loader 配置的资源,一般是所有 Web 应用共享的资源
  • WebappX - 父加载器是 Shared 加载器,加载 /WEB-INF/classes 的 class 和 /WEB-INF/lib/ 中的 jar 包
  • JasperLoader - 父加载器是 Webapp 加载器,加载 work 目录应用编译 JSP 生成的 class 文件

JDBC 为什么要破坏双亲委派模型?

在 JDBC 4.0 之后我们需要使用 Class.forName 来加载驱动程序了,我们只需要把驱动的 jar 包放到工程的类加载路径里,那么驱动就会被自动加载。

这个自动加载采用的技术叫 SPI,可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。我们只需要下面这一句话就可以创建数据库连接:

Connection con =
DriverManager.getConnection(url , username , password ) ;

因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到所需要的文件,这时候就需要委托子类加载器去加载 class 文件

JDBC 的 Driver 接口定义在 JDK 中,其实现由各个数据库服务商来提供,比如 MySQL 的驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说 Bootstrap 类加载器还要去加载 jar 包中的 Driver 接口的实现类。Bootstrap 只负责 /lib/rt.jar 里面所有的 class,所以需要子类加载器去加载 Driver,这就破坏了双亲委派模型。

查看 DriverManager 类的源码,看到使用 DriverManager 的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用 ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。

    static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
} private static void loadInitialDrivers() { AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
}); }
    public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的上下文加载器。

public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}

可以看到,在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是系统加载器

Tomcat 为什么要破坏双亲委派模型

每个 Tomcat 的 webappClassLoader 加载自己目录的 class 文件,不会传递给父类加载器

事实上,Tomcat 之所以造出了一堆自己的 classLoader,大致是出于三个目的:

  1. 对于各个 weapp 中的 class 和 lib,需要相互隔离,不能出现一个应用中加载的类影响另一个应用的情况。
  2. 与 JVM 一样出于安全考虑,使用单独的 classLoader 去装载 Tomcat 自身的类库,防止恶意或者无意的破坏。
  3. 热部署。

JVM:Java 类的加载机制的更多相关文章

  1. JVM:java类的加载机制

    原文连接:https://www.cnblogs.com/ityouknow/p/5603287.html 类加载机制的奥妙. 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读 ...

  2. jvm系列(一):java类的加载机制

    java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...

  3. JVM(1):Java 类的加载机制

    原文出处: 纯洁的微笑 java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang. ...

  4. jvm系列一、java类的加载机制

    一.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  5. 02 Java类的加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  6. Java 类的加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  7. Java虚拟机(三):Java 类的加载机制

    1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...

  8. java类的加载机制

    什么是类装载器ClassLoader ClassLoader是一个抽象类 ClassLoader的实例将读入Java字节码将类装载到JVM中 ClassLoader可以定制,满足不同的字节码流获取方式 ...

  9. 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

    目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...

随机推荐

  1. HTML5中新增的主体结构元素

    article元素 article元素代表文档.页面或应用程序中独立的.完整的.可以独自被外部引用的内容. 它可以使一篇博客或者报刊中的文章,一篇论坛帖子.一段用户评论或独立的插件,或其他任何独立的内 ...

  2. centOS+DJango+mysql_nginx部署流程记录

    安装Python3.6.2: https://www.jianshu.com/p/7a76bcc401a1 安装MySQL: https://www.cnblogs.com/luohanguo/p/9 ...

  3. git 删除分支和回退到以前某个提交版本

    1.git 创建和删除分支: 创建:git branch 分支名字 本地删除:git branch -D 分支名字 远程删除:git push origin :分支名字 2.git 回退到以前提交的版 ...

  4. Struts2学习(七)

    Servlet实现下载 1.Servlet 3.1之前实现文件上传 package ecut.request; import java.io.BufferedReader; import java.i ...

  5. Java基础 -3.4

    反码(~) 在计算机中,负数以其正值的补码形式表达. 什么叫补码呢?这得从原码,反码说起. 原码:一个整数,按照绝对值大小转换成的二进制数,称为原码. 比如 00000000 00000000 000 ...

  6. shell脚本基础及重定向!

    重定向: 补充:/dev/null(名叫黑洞)就是把输出的文件混合重定向到黑洞从而不显示在屏幕 yum -y install http &> /dev/null 重定向输入: 重定向输出 ...

  7. Ubuntu14 安装JDK 8

    参考: [1]Ubuntu安装JDK7/JDK8 [2]Oracle官网安装JDK10 安装包安装 本文采用安装包安装方式 1.下载JDK安装包 JDK8下载 ,根据所使用系统选择安装包(这里选.ta ...

  8. 嵊州普及Day6T3

    题意:n个点,对于q个询问,有t秒及一个矩形的范围.在此矩形内的数每秒加1,若等于c,则下一秒变为0. 思路:t可能很大,%c+1就可以了.然后一个一个加起来就可以了. 见代码: #include&l ...

  9. 在fragment中实现返回键单击提醒 双击退出

    最近在练习一个小项目,也就是郭霖大神的开源天气程序,尝试用mvp架构加dagger2来重写了一下,大致功能都实现了,还没有全部完成. 项目地址 接近完成的时候,想在天气信息页面实现一个很常见的功能,也 ...

  10. json序列化(重要)

    (1)同(2)public JsonResult JsonUserGet() { DataSet ds = Web_User.P_LG_User_Get(nUserId); return Json(J ...