类加载到卸载的全过程分析
在Java代码中,类型的加、连接与初始化过程都是在程序运行期间完成的。其中类型指我们定义的一个class、interface、enum,此时并未包含对象。这一点提供了更大的灵活性、增加了更多的可能性。每一个类都是由类加载器class loader 加载到内存当中的。
1. Java虚拟机的生命周期
JVM虚拟机最最本质上是一个进程,所以JVM和普通的进程一样,都是有生命周期的。Java虚拟机和程序的生命周期,在如下几种情况下,Java虚拟机的将结束生命周期:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
对于上述给出的第三点给出进一步详细的例子描述,因为其十分常见,比如说我们调用一个程序其会向上抛出异常,但是直至main方法我们还是采取向上抛出异常的异常处理机制,那么此时程序就会结束运行,这是一种很常见的JVM结束生命周期的方式。
示例代码:
System.exit()
:可以看出,在此方法之后所定义的方法并没能够被有效执行,因为此时虚拟机以及被关闭了。
class GfG
{
public static void main(String[] args)
{
int arr[] = {
1, 2, 3, 4, 5, 6, 7, 8};
for (int i = 0; i < arr.length; i++)
{
if (arr[i] >= 5)
{
System.out.println("exit...");
// Terminate JVM
System.exit(0);
}
else
System.out.println("arr["+i+"] = " +
arr[i]);
}
System.out.println("End of Program");
}
}
控制台输出:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
exit...
2. 类的加载、连接与初始化的概括摘要
2.1 类的加载、连接、初始化的流程说明
- 加载:查找并加载类的二进制数据
- 连接:连接是一个比较复杂的分步步骤,具体可以分为以下三步:
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用(涉及变量和方法)
- 初始化:为类的静态变量赋予正确的初始化值
静态变量:就是在Java代码中使用static修饰的值。
默认值:int类型的默认值为0,boolean类型的默认值为false, 引用的默认值为null。注意,如果我们使用static int a =10;
,但是在连接阶段的准备阶段,a变量的值被赋值为0;
下图和上图表达的意思实际上是一样的:
2.2 符号引用与直接引用的区别
号引用/直接引用之间的区别:如果想仔细了解这两个概念的区别,不妨查看R大对此的回答R大。如果简单点说,就是JVM在加载完二进制数据之后,并未完成类的内存分配问题,这样一来我们就不能通过内存偏移量来查找方法以及变量了。符号引用(以方法为例)是一个包含类信息、方法名、方法参数的字符串,例如:java/io/PrintStream.println:(Ljava/lang/String;)V
,我们根据这个字符串就可以准确地找到相关类。但是,此实现方式速度上还是不够快,所以就出现了基于内存地址偏移量的直接引用:运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
3.类的加载
备注:在IDEA中查询类有无被加载的方法,请移步第7.3小节。
类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.class对象(其即是被我们俗称的类对象,JVM虚拟机规范并没有说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中),类对象用来封装类在方法区内的数据结构。JVM规范也没有指定从哪里来加载Class文件。
整个过程可以使用下图表示:
类的加载的最终产品是位于内存中的Class对象(不是我们在Java代码中调用构造方法所产生的对象)。 Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内数据结构的接口。Class对象是整个反射的入口,它就好像是一面镜子一样,能够洞悉出类文件中的所有信息。
3.1 加载.clss文件的的源头
- 从本地系统中直接加载(位于本地磁盘额的类路径中被调用,这是我们最常见的一种调用方法)
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件(第三包架包就是这样被我们调用的)
- 从专有的数据库中提取.class文件(比较少)
- 将Java源文件中动态便以为.class文件(web开发中常用)
注意:这些都是可能的源头,实际上在不同的开发环境下应当会有不同的源头,而不同的源头就要求我们使用不同的字节处理流,比如说文件流:FileInputStream
3.2 类加载器的分类以及自带加载器的概念
有两种类型的类加载器:
- Java虚拟机自带的类加载器
- 根类加载器(Bootstrap)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
- 用户自定义的类加载器
- 其一定是java.lang.ClassLoader抽象类(这个类本身就是提供给自定义加载器继承的)的子类
- 用户可以定制的加载方式
默认情况下,我们自定义的类的加载器是系统类加载器。
java虚拟机自带的几种加载器:
(1) 根(Bootstrap)类加载器:该类加载器没有父加载器,他负责加载虚拟机的核心类库,如java.lang.*等。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,他并没有继承java.lang.ClassLoader类。比如说java.lang.Object就是由根类加载器加载的。
(2)扩展(Extension)类加载器:它的父类加载器为根类加载器。他从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动有扩展类加载器加载。扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。
(3) 系统(System)类加载器:也称为应用加载器,他的父类加载器为扩展类加载器。他从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。他是用户自定义的类加载器的默认父加载器。系统类加载器是纯java类,是java.lang.ClassLoader子类。
上图是类加载器的层次关系图。从表象上看这些加载器是一种继承关系,但是实际上是一种包含关系。比如说,系统类加载器加载一个类,首先会委托给扩展类加载器,后者又委托给根类加载器,如果根类加载器加载失败,那么就委托回扩展类加载器,如果还不行,那么就系统类加载器加载,最后还不行,则抛出异常。但是实际上系统类加载器包含了扩展类加载器,后者又包含了根类加载器。
上述类加载器父子(非继承中的父子关系)结构的代码证明:
public class MyTest13 {
public static void main(String[] args) {
/**
* 默认情况下,系统类加载器是用户自定义加载器的双亲,典型情况下其是应用启动的类加载器。
*
*/
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
System.out.println(classLoader);
while (null != classLoader) {
classLoader = classLoader.getParent();
System.out.println(classLoader);
}
}
}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
注意:应用类加载器就是系统类加载器,启动类加载器被null表示,所以就证明了这个加载器的层次关系(但注意:这不是子类和父类关系)。
3.3 类加载和类初始化的关系
类加载完成时类初始化的必要条件,但是类被加载了不意味着其一定会被初始化。类加载器并不需要等到某个类被“首次主动使用”时再加载它。JVM规范允许类加载器再预料某个类将要被使用时就预先加载它,如果再预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须再程序首次主动使用该类时才报告错误(LinkageError链接错误)。如果一个类一直没有被程序主动使用,那么类加载器就不会报告错误(即使我们已经将类文件预先加载了)。
3.4 获得类加载器的方法
小总结:获得ClassLoader的途径,有:
-
获得当前类的ClassLoader,clzz为类的类对象,而不是普通对象
clazz.getClassLoader();
-
获得当先线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
-
获得系统的ClassLoader
ClassLoader.getSystemClassLoader();
-
获得调用者的ClassLoader
DriverManager.getCallerClssLoader();
两个概念(了解即可,基于3.5小节中的双亲委托机制才有这两种叫法):
- 定义类加载器:若有一个类加载器能成功加载Test类(自己写的),那么这个类加载器被称为定义类加载器
- 初始类加载器:所有能够成功返回Class对象引用的类加载器(包括自定义类加载器)都被称为初始类加载器
下面我们给出一个使用根类加载器的一段代码:
public class MyTest7 {
public static void main(String[] args) throws ClassNotFoundException {
Class clazz;
clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());
}
}
上述方法在控制台输出:null
,预示着这个String类由根类加载器加载。
每个类在被加载时都会对应一个类对象,而Class.forName()方法就是返回一个类对象。
getClassLoader()
是Class对象(类对象)的方法,有以下性质:
- 返回类对象所对应的类或接口的加载器,如果加载器是根加载器可能会返回null,也可能不会,主要看相关类的实现(返回null所用的加载器就是根加载器);
- 如果类对象对应的是基本类型以及void,那么一定返回null;
第二个代码块:
public class MyTest7 {
public static void main