类是在运行期间动态加载的。
类的加载指的是将类的.class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class
对象,Class
对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
Java 类的完整生命周期包括以下几个阶段:
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始。而解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
类加载过程是指加载、验证、准备、解析和初始化这 5 个阶段。
加载是类加载的一个阶段,注意不要混淆。
加载过程完成以下三件事:
Class
对象,作为方法区这个类的各种数据的访问入口。其中二进制字节流可以从以下方式中获取:
java.lang.reflect.Proxy
中,就是用了 ProxyGenerator.generateProxyClass
的代理类的二进制字节流。验证是链接阶段的第一步。验证的目标是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成 4 个阶段的检验动作:
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
类变量是被 static 修饰的变量,准备阶段为 static 变量在方法区分配内存并初始化为默认值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次)
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
public static final int value = 123;
准备阶段有以下几点需要注意:
假设一个类变量的定义为: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 常量在编译期就将其结果放入了调用它的类的常量池中
解析阶段目标是将常量池的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化阶段才真正开始执行类中的定义的 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
来实现自身的热部署。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(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()
方法运行过程如下:
【说明】
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()
方法,未找到类。 解决方法:检查 classpath
下有没有相应的 class 文件。
常见问题原因:
解决方法:现代 Java 项目,一般使用 maven
、gradle
等构建工具管理项目,仔细检查找不到的类所在的 jar 包是否已添加为依赖。
这个异常倒不是很常见,但是出错的话,通常是在 JVM 启动的时候如果一不小心将在 JVM 中的某个 lib 删除了,可能就会报这个错误了。
ClassCastException
异常通常是在程序中强制类型转换失败时出现。
原文:https://www.cnblogs.com/turbosha/p/13252234.html