Java虚拟机将描述类的Class文件加载到内存中,并进行一定的验证,解析和初始化,最终形成可以被虚拟机识别的Java类型,这个过程我们叫做Java的类加载机制。
类加载过程
一个类从被加载到内存到最终的卸载主要分为以下7个阶段,如下:
其中验证、准备和解析三个阶段也被称之为连接。
【加载】
在加载阶段,虚拟机主要完成三件事情:
- 通过全限定名称找到Class文件的二进制流。
- 将二进制流的静态数据结构转化为运行时数据结构。
- 在内存中生成一个Class对象,作为方法区访问这个对象数据的入口。
类加载解阶段是用户可控最强的阶段,主要是可以使用系统的类加载器,也可以使用自定义类加载器加载。
【验证】
这一步的目的是确保Class文件中的字节流符合《Java虚拟机规范》的约束要求,如验证数组是否越界等等。
如果校验失败:抛出一个java.lang.VerifyError异常或其子类异常
验证阶段主要完成的四项:
- 文件格式校验,如JDK版本,Class文件魔数。
- 元数据校验,如是否存在父类,是否能被继承。
- 字节码校验,如控制流是否正确,不会跳出方法体外。
- 符号引用验证,如符号引用转化为直接引用,是否能有权访问,非private。
【准备】
这一步是为类中定义的变量(即静态变量, 被static修饰的变量) 分配内存并设置类变量初始值的阶段。
注意: 1. 内存分配的仅包括类变量, 而不包括实例变量。 2. 初始值“通常情况”下是数据类型的零值。个别情况非0值,如下: public static int value = 123; 会初始为0值。 public static final int value = 123; 会直接初始化为123 即:final修饰的类变量。
【解析】
这一步是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References) :符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可。
直接引用(Direct References) :直接引用是可以直接指向目标的指针、 相对偏移量或者是一个能间接定位到目标的句柄。
【初始化】
进行准备阶段时, 变量已经赋过一次系统要求的初始零值, 而在初始化阶段, 则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。即:初始化阶段就是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的。
上述描述了类加载在每个阶段所做的事情,那么什么时候触发类加载呢?
有且只有6种情况:
- 遇到new、 getstatic、 putstatic或invokestatic这四条字节码指令时,典型的编码场景:
- 使用new关键字实例化对象。
- 读取或设置一个类型的静态字段。
- 调用一个类型的静态方法的时候。
- 反射调用。
- 父类还没有进行过初始化。
- 虚拟机启动时,要执行的主类。
- 接口的实现类发生了初始化, 那该接口要在其之前被初始化。
- 对方法句柄对应的类进行初始化。
类加载器
什么是类加载器?
把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节
流”这个动作放到Java虚拟机外部去实现, 实现这个动作的代码被称为“类加载器”(Class Loader) 。
那么类和类加载器什么关系呢?
- 任意一个类,都必须由他的类加载器和这个类本身来确定其唯一性(通过equal 判断)。
如下代码所示:
/**
* @Author huangjia
* @Date: 2021/2/26 14:35
* Describe:
*/
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String className = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(className);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("com.seach.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.seach.ClassLoaderTest);
}
}
结果:
class com.seach.ClassLoaderTestfalse
结果显示,类也是ClassLoaderTest,那为什么是false呢?
主要是以下原因:
- obj对象是由我们自定义的类加器加载的对象。
- com.seach.ClassLoaderTest是由系统类加载器加载的对象。
同样的对象,为什么一个是系统加载,另一个是自定义类加载器加载呢?
这里涉及到双亲委派模型了。
那什么是双亲委派模型呢?
各种类加载器之间的层次关系被称为类加载器的双亲委派模型, 如下所示:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载。
如上图所示,类加载器的类别有:
- 启动类加载器:负责加载<JAVA_HOME>\lib目录。
- 扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录。
- 应用程序类加载器:负责加载用户类路径(ClassPath) 上所有的类库。
- 用户自定义类加载器:复责加载用户指定的类,如上代码所示。
既然有了这种模型,为什么还需要破坏双亲委派呢?
因为在某些情况下父类加载器需要委托子类加载器去加载Class文件。受到加载范围的限制,父类加载去无法加载到需要的文件,如Driver接口为例,由于Driver接口定义在Jdk中,而具体的实现由对应的厂商来实现,比如Mysql的驱动,但是DriverManager由类加载器加载,只能加载<JAVA_HOME>\lib下的文件,而实现是由应用程序类加载器加载,从而需要破坏双亲委派模型。
那如何破坏呢?
如果不想打破双亲委派模型,只需要重写ClassLoader类的findClass()方法即可。
如果想破坏双亲委派模型,那么重写loadClass()方法。
一道面试题:是否我们可以自己写个类叫做:java.lang.System?答案:通常不可以,但可以采取另类方法达到这个需求。作者:Jemon37177链接:https://juejin.cn/post/6844903564804882445来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
解释:
为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
— 完 —
原创内容,未经账号授权,禁止随意转载。
点这里关注我,记得标星,么么哒~
本文暂时没有评论,来添加一个吧(●'◡'●)