同步操作将从 turnon/blog 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
title: JVM 类加载
date: 2020-06-17 15:06:46
categories:
- Java
- JavaSE
- JVM
tags:
- Java
- JavaSE
- JVM
permalink: /pages/17aad9/
类是在运行期间动态加载的。
类的加载指的是将类的 .class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class
对象,Class
对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
Java 类的完整生命周期包括以下几个阶段:
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始。而解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
类加载过程是指加载、验证、准备、解析和初始化这 5 个阶段。
加载是类加载的一个阶段,注意不要混淆。
加载,是指查找字节流,并且据此创建类的过程。
加载过程完成以下三件事:
Class
对象,作为方法区这个类的各种数据的访问入口。其中二进制字节流可以从以下方式中获取:
java.lang.reflect.Proxy
中,就是用了 ProxyGenerator.generateProxyClass
的代理类的二进制字节流。更详细内容会在 3. ClassLoader 介绍。
验证是链接阶段的第一步。验证的目标是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成 4 个阶段的检验动作:
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
类变量是被 static 修饰的变量,准备阶段为 static 变量在方法区分配内存并初始化为默认值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
public static final int value = 123;
准备阶段有以下几点需要注意:
0
、0L
、null
、false
等),而不是被在 Java 代码中被显式地赋予的值。假设一个类变量的定义为:public static int value = 3
;
那么变量 value 在准备阶段过后的初始值为 0,而不是 3,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 3 的public static
指令是在程序编译后,存放于类构造器()
方法之中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
这里还需要注意如下几点:
- 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被 static 和 final 修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被 final 修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型 reference 来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即 null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
ConstantValue
属性,即同时被 final 和 static 修饰,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。假设上面的类变量 value 被定义为: public static final int value = 3
;
编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据ConstantValue
的设置将 value 赋值为 3。我们可以理解为 static final 常量在编译期就将其结果放入了调用它的类的常量池中
在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。
解析阶段目标是将常量池的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
在 Java 代码中,如果要初始化一个静态字段,可以在声明时直接赋值,也可以在静态代码块中对其赋值。
如果直接赋值的静态字段被 final
所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >
。
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
只有主动引用类的时候才会导致类的初始化。
(1)主动引用
类的主动引用包括以下六种:
new
对象Class.forName(“com.shengsiyuan.Test”)
Java Test
),直接使用java.exe
命令来运行某个主类(2)被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
Object
的子类,其中包含了数组的属性和方法。SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
类初始化 <clinit>()
方法的细节:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
<init>()
)不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>()
方法运行之前,父类的 <clinit>()
方法已经执行结束。因此虚拟机中第一个执行 <clinit>()
方法的类肯定为 java.lang.Object
。<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码:static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。
}
<clinit>()
方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>()
方法。<clinit>()
方法。但接口与类不同的是,执行接口的 <clinit>()
方法不需要先执行父接口的 <clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>()
方法。<clinit>()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>()
方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>()
方法完毕。如果在一个类的 <clinit>()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。ClassLoader
即类加载器,负责将类加载到 JVM。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
JVM 加载 class
文件到内存有两种方式:
ClassLoader
来加载一个类到内存中。如何判断两个类是否相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个 ClassLoader
都拥有一个独立的类名称空间。
这里的相等,包括类的 Class
对象的 equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果为 true,也包括使用 instanceof
关键字做对象所属关系判定结果为 true。
Bootstrap ClassLoader
,即启动类加载器 ,负责加载 JVM 自身工作所需要的类。
Bootstrap ClassLoader
会将存放在 <JAVA_HOME>\lib
目录中的,或者被 -Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
Bootstrap ClassLoader
是由 C++ 实现的,它完全由 JVM 自己控制的,启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null
代替即可。
ExtClassLoader
,即扩展类加载器,这个类加载器是由 ExtClassLoader(sun.misc.Launcher\$ExtClassLoader)
实现的。
ExtClassLoader
负责将 <JAVA_HOME>\lib\ext
或者被 java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
AppClassLoader
,即应用程序类加载器,这个类加载器是由 AppClassLoader(sun.misc.Launcher\$AppClassLoader)
实现的。由于这个类加载器是 ClassLoader
中的 getSystemClassLoader()
方法的返回值,因此一般称为系统类加载器。
AppClassLoader
负责加载用户类路径(即 classpath
)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器可以做到如下几点:
假设,我们需要自定义一个名为 FileSystemClassLoader
的类加载器,继承自 java.lang.ClassLoader
,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class
文件),然后读取该文件内容,最后通过 defineClass()
方法来把这些字节代码转换成 java.lang.Class
类的实例。
java.lang.ClassLoader
类的方法 loadClass()
实现了双亲委派模型的逻辑,因此自定义类加载器一般不去覆写它,而是通过覆写 findClass()
方法。
ClassLoader
常用的场景:
ClassLoader
来实现自身的热部署。【示例】自定义一个类加载器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
理解双亲委派之前,先让我们看一个示例。
【示例】寻找类加载示例
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@19e1023e
null
从上面的结果可以看出,并没有获取到 ExtClassLoader
的父 Loader,原因是 Bootstrap Loader
(引导类加载器)是用 C 语言实现的,找不到一个确定的返回父 Loader 的方式,于是就返回 null。
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的 Bootstrap ClassLoader 外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
(1)工作过程
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。
(2)好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一:
例如: java.lang.Object
存放在 rt.jar 中,如果编写另外一个 java.lang.Object
的类并放到 classpath
中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object
比在 classpath
中的 Object
优先级更高,因为 rt.jar 中的 Object
使用的是启动类加载器,而 classpath
中的 Object
使用的是应用程序类加载器。正因为 rt.jar 中的 Object
优先级更高,因为程序中所有的 Object
都是这个 Object
。
(3)实现
以下是抽象类 java.lang.ClassLoader
的代码片段,其中的 loadClass()
方法运行过程如下:
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent;
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) {
// 如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器加载失败,会抛出 ClassNotFoundException
}
if (c == null) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
【说明】
ClassNotFoundException
,此时尝试自己去加载。在生产环境上启动 java 应用时,通常会指定一些 ClassLoader
参数,以加载应用所需要的 lib:
java -jar xxx.jar -classpath lib/*
ClassLoader
相关参数选项:
参数选项 | ClassLoader 类型 | 说明 |
---|---|---|
-Xbootclasspath |
Bootstrap ClassLoader |
设置 Bootstrap ClassLoader 搜索路径。【不常用】 |
-Xbootclasspath/a |
Bootstrap ClassLoader |
把路径添加到已存在的 Bootstrap ClassLoader 搜索路径后面。【常用】 |
-Xbootclasspath/p |
Bootstrap ClassLoader |
把路径添加到已存在的 Bootstrap ClassLoader 搜索路径前面。【不常用】 |
-Djava.ext.dirs |
ExtClassLoader |
设置 ExtClassLoader 搜索路径。 |
-Djava.class.path 或 -cp 或 -classpath
|
AppClassLoader |
设置 AppClassLoader 搜索路径。 |
类加载有三种方式:
Class.forName()
方法动态加载ClassLoader.loadClass()
方法动态加载Class.forName()
和 ClassLoader.loadClass()
区别
Class.forName()
将类的 .class
文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static
块;ClassLoader.loadClass()
只干一件事情,就是将 .class
文件加载到 jvm 中,不会执行 static
中的内容,只有在 newInstance
才会去执行 static
块。Class.forName(name, initialize, loader)
带参函数也可控制是否加载 static
块。并且只有调用了 newInstance()
方法采用调用构造函数,创建类的对象 。ClassNotFoundException
异常出镜率极高。ClassNotFoundException
表示当前 classpath
下找不到指定类。
常见问题原因:
Class
的 forName()
方法,未找到类。ClassLoader
中的 loadClass()
方法,未找到类。ClassLoader
中的 findSystemClass()
方法,未找到类。【示例】执行以下代码,会抛出 ClassNotFoundException
异常:
public class ClassNotFoundExceptionDemo {
public static void main(String[] args) {
try {
Class.forName("NotFound");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
解决方法:检查 classpath
下有没有相应的 class 文件。
常见问题原因:
解决方法:现代 Java 项目,一般使用 maven
、gradle
等构建工具管理项目,仔细检查找不到的类所在的 jar 包是否已添加为依赖。
这个异常倒不是很常见,但是出错的话,通常是在 JVM 启动的时候如果一不小心将在 JVM 中的某个 lib 删除了,可能就会报这个错误了。
【示例】执行以下代码,会抛出 UnsatisfiedLinkError
错误。
public class UnsatisfiedLinkErrorDemo {
public native void nativeMethod();
static {
System.loadLibrary("NoLib");
}
public static void main(String[] args) {
new UnsatisfiedLinkErrorDemo().nativeMethod();
}
}
【输出】
java.lang.UnsatisfiedLinkError: no NoLib in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at io.github.dunwu.javacore.jvm.classloader.exception.UnsatisfiedLinkErrorDemo.<clinit>(UnsatisfiedLinkErrorDemo.java:12)
ClassCastException
异常通常是在程序中强制类型转换失败时出现。
【示例】执行以下代码,会抛出 ClassCastException
异常。
public class ClassCastExceptionDemo {
public static void main(String[] args) {
Object obj = new Object();
EmptyClass newObj = (EmptyClass) obj;
}
static class EmptyClass {}
}
【输出】
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to io.github.dunwu.javacore.jvm.classloader.exception.ClassCastExceptionDemo$EmptyClass
at io.github.dunwu.javacore.jvm.classloader.exception.ClassCastExceptionDemo.main(ClassCastExceptionDemo.java:11)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。