JVM 类加载

类的生命周期

加载(Loading)

  • 交给虚拟机的具体实现来自由把控,大多数都是懒加载
  • 没有指定一定得从某个 class 文件中获取
  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

什么时候需要开始类第一个阶段“加载”,虚拟机规范没有强制约束,这点交给虚拟机的具体实现来自由把控。JVM 虚拟机的实现都是使用的懒加载

注意:比如“通过一个类的全限定名来获取定义此类的二进制字节流”没有指定一定得从某个 class 文件中获取,所以我们可以从zip 压缩包、从网络中获取、运行时计算生成、数据库中读取、或者从加密文件中获取等等。

验证(Verification)

是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备(Preparation)

为 static 修饰的变量赋零值

准备阶段是正式为类中定义的变量(被 static 修饰的变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这个阶段中有两个容易产生混淆的概念需要强调一下:

  • 首先,这时候进行内存分配的仅包括类变量(被static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
  • 其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value=123;那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 是后续的初始化环节。

解析(Resolution)

解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。直接引用的对象都存在于内存中。
解析大体可以分为:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

我们了解几个经常发生的异常,就与这个阶段有关。

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)

初始化(Initialization)

初始化主要是对一个 class 中的 static{} 语句进行操作(对应字节码就是 clinit 方法)。
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

有且只有 6 种情况必须立即对类进行“初始化”:

  1. 遇到 new, invokestatic, getstatic, putstatic 这 4 条字节码指令时
    1. 使用 new关键字实例化对象的时候
    2. 调用一个类的静态方法的时候
    3. 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,会先初始化要执行的主类(包含main()方法的那个类)
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化

示例:

  • SubClazz.superValue如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化(但是子类会被加载)
  • SuperClazz[] sca = new SuperClazz[10];使用数组的方式, 不会触发初始化(触发父类加载,不会触发子类加载)
  • SuperClazz.FINAL_VALUE不会触发类加载,在编译的时候,常量数据已经进入自己类的常量池
  • 如果使用常量去引用另外一个常量(这个值编译时无法确定,所以必须要触发初始化)

线程安全性:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点,比如通过静态内部类实现单例模式。

类加载器

负责:加载、验证、准备、解析、初始化

双亲委派机制

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系

例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

具体的加载过程在 ClassLoader#loadClass 方法。首先使用 parent 尝试进行类加载,parent 失败后才轮到自己。这个方法是可以被覆盖的,也就是双亲委派机制并不一定生效

// 首先使用 parent 尝试进行类加载
if (parent != null) {
	c = parent.loadClass(name, false);
} else {
	c = findBootstrapClassOrNull(name);
}

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的 Class 对象的 equals(), isAssignableFrom(), isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

不能重写覆盖类,如果你在项目代码里,写一个 java.lang 的包,然后改写 String 类的一些行为,编译后,发现并不能生效。JRE 的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。

自定义类加载器

打破双亲委派机制

Tomcat 类加载机制

tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。设置自定义加载器实现在同一个 JVM 里,运行不兼容的两个版本。

但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。

WebAppClassLoader

对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。

这个加载器用来隔绝不同应用的 .class 文件,比如两个应用可能会依赖同一个第三方的不同版本,它们是相互没有影响的。因为**WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器**。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。

SPI (Service Provider Interface)

是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。

SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。

JDBC

比如 MySQL,mysql-connector-java-8.0.11.jar!/META-INF/services/java.sql.Driver 文件内容为:com.mysql.cj.jdbc.Driver

如何打破的:

DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 BootstrapClassLoader,也就是最上层的那个。而具体的数据库驱动却属于业务代码,这个启动类加载器是无法加载的。通过ServiceLoader#load 的代码发现它把当前的类加载器,设置成了线程的上下文类加载器

public static <S> ServiceLoader<S> load(Class<S> service) {
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

对于一个刚刚启动的应用程序来说,它当前的加载器是启动 main 方法的那个加载器,所以我们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中发现存的就是 Application ClassLoader

this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值