- 类的生命周期
- 类加载器
- 双亲委派机制
类的生命周期
类从被加载到虚拟机开始,到卸载出内存为止,它的整个的生命周期包括:加载、连接(验证、准备、解析)、初始化、使用和卸载七个阶段。
加载过程
将编译之后的Class文件加载至虚拟机,并存储在方法区。
连接过程
连接被分为了验证、准备、解析三部分。
验证:Class文件中的信息是否符合Java虚拟机的要求,是否安全(可能会做出危害虚拟机的行为)。
准备:为类变量分配内存,并设置类变量的初始值。不包含实例变量的内存,实例变量的内存将会被分配到堆中。
这里的初始值指的是数据类型的零值,并非程序员设置的值,比如static int i = 1;此时得到的i是0,赋值为1将会在初始化阶段执行。
解析:将常量池中的符号引用替换为直接引用(内存地址)。
a.run();代码表示对A类的run()方法的
符号引用
,由run()方法的全名 和 相关描述符组成。 解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向A类的run()方法在方法区的内存位置,这个指针就是直接引用
。
初始化
前面的类加载过程中,除了通过自定义类加载器参与之外,其余的动作其实都是由虚拟机主导和控制的。
初始化是类加载的最后一步,真正开始执行程序中代码的步骤,比如将i赋值为1的过程,初始化类变量,加载类的静态语句块的过程。
在准备阶段赋值过一次了,那次赋值是系统要求的初始值,跟我们个人的设定无关,而初始化的赋值才是真正根据程序员设计而主导的初始化。
使用和卸载
使用类的阶段就是new对象的过程,我们会使用到各种类型对象,以及使用完毕对这个类型的卸载,卸载一般发生在程序关闭的时候。
程序正常执行结束、遇到异常关闭、或者操作系统出问题导致程序关闭这些都会卸载这些类的加载。
类加载器
从JVM的角度看,类加载器主要有两类:
- Bootstrap ClassLoader和其他类加载,Bootstrap ClassLoader是C 语言实现,是虚拟机自身的一部分;
- 其他类加载器都是Java语言实现,不属于虚拟机,全部继承自抽象类java.lang.ClassLoader
从Java开发者的角度看,需要了解类加载器的双亲委派模型,如下图所示:
启动类加载器Bootstrap ClassLoader
这个类加载器将负责存放在/lib目录中,并且是虚拟机会识别的jar类库加载到内存中。更直白点说,就是我们常用的java.lang开头的那些类,一定是被Bootstrap ClassLoader加载的
扩展类加载器Extension ClassLoader
它负责加载/lib/ext目录中的、或者被java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器Application ClassLoader
它负责加载用户CLASSPATH环境变量指定的路径中的所有类库。如果应用程序中没有自定义过自己的类加载器,这个就是一个Java程序中默认的类加载器。
用户自定义的类加载器
用户在需要的情况下,可以实现自己的自定义类加载器,一般而言,在以下几种情况下需要自定义类加载器
- 隔离加载类。某些框架为了实现中间件和应用程序的模块的隔离,就需要中间件和应用程序使用不同的类加载器
- 修改类加载的方式。类加载的双亲委派模型并不是强制的,用户可以根据需要在某个时间点动态加载类
- 扩展类加载源,例如从数据库、网络进行类加载
- 防止源代码泄露。Java代码很容易被反编译和篡改,为了防止源码泄露,可以对类的字节码文件进行加密,并编写自定义的类加载器来加载自己的应用程序的类
双亲委派机制
向上委托,向下加载
如果一个类加载器收到了类加载的请求,不考虑们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有就把请求委托给父加载器去完成,依次向上。
因此,所有的类加载请求最终都被传递到顶层的启动类加载器BootstrapClassLoader。这时候开始考虑自己是否能加载了,如果在它的搜索范围中没有找到所需的类时,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
源码
“java.lang”包下的ClassLoader类中loadClass方法代码如下:
代码语言:javascript复制public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
双亲委派机制的作用
1、避免类的重复加载。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保护程序安全,保证核心类不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
- 自定义类:java.lang.String (没用)
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
为了系统类的安全,类似java.lang.String这种,jvm需要保证 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。系统的类已经被Bootstrap classLoader加载过了,所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
如果我们有一个类想要通过自定义的类加载器来加载,可以通过重写ClassLoader中的loadClass方法,实现自定义类加载器。改变类的类加载器的时候要注意,如果全部改了,找不到Object.class这种顶级类,就加载不了了。
参考:
通俗易懂的双亲委派机制:https://blog.csdn.net/codeyanbao/article/details/82875064
Java 符号引用 与 直接引用:https://www.cnblogs.com/mzzcy/p/7223405.html
如何破坏双亲委派:https://mp.weixin.qq.com/s/k0_K627ENyHeE92vW5HIiw