、什么是jvm的类加载机制
类加载机制是指我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种 特殊机制。那么我们可以得知,类加载的最终产品是数据访问入口。
加载类文件(即.class文件)的方式有以下几种:
从本地系统中直接加载。
通过网络下载的.class文件。比如Web Applet,也就是我们的小程序应用。
从war,jar等归档文件中加载class。
从专有数据库中提取.class文件。
将java源文件动态编译为.class文件,也就是运行时计算而成,java的动态代理技术就是这么实现的。
从加密文件中获取。典型的防止class文件被反编译的保护措施。
回到顶部
2、类装载的过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个过程包括:装载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析统称为链接。如下图
装载(Loading):
通过一个类的全限定名获取定义此类的二进制字节流,由上文可知,我们不一定从字节码文件中获取字节流,还能通过上述多种方式获取字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在java堆中生存一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入库。
获取类的二进制字节流的阶段是我们java程序员最关注的阶段,也是操作性最强的一个阶段,因为这个阶段我们可以对我们的类加载器进行操作 ,比如我们想自定义类加载器进行操作完成加载,又或者我们想通过java agent来完成我们的字节码增强操作。
连接(Linking):
验证(Verification):验证主要是为了保证被加载类的正确性,即装载的Class文件中的字节流保护的信息是否符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。这其中包括四个阶段:
文件格式的验证:第一阶段要验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理
元数据的验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
字节码验证:第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据做完校验后,这个阶段将对类对方法体进行校验分析,保证被校验类对方法在运行时不会做出危害虚拟机安全对事件。
符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段), 可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用 验证的目的是确保解析动作能正常执行。
准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。
解析(Resolution):
初始化(Initialization):
初始化阶段是执行类构造器()方法的过程。在准备阶段,类变量已赋过一次系统要求对初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,比如赋值。
在java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):
使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
虚拟机启动时,用户会先初始化要执行的主类(含有main)
jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
使用(Useing):
主动使用:
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射
初始化某个类的子类,则其父类也会进行初始化
java虚拟机启动时被标明为启动类的类(SpringbootApplication类)
被动使用:
引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
定义类数组,不会引起类的初始化
引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化 的)
卸载(Unloading):
在类使用完成之后,如果满足下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类对卸载过程其实就是在方法区中清空类信息,java类对整个生命周期就结束类。但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有极少的情况下才会被卸载。
回到顶部
3、类的加载器
java的类记载器是负责读取java字节码,并转换成java.lang.class类的一个实例的代码模块。类加载器除了用于加载类外,还可以用于确定类在java虚拟机的唯一性。
一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的, 这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍 然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。
java虚拟机自带有以下三种类加载器:
Bootstrap ClassLoader
负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由 C++实现,不是ClassLoader子类。
Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs 指定目录下的jar包。
Application ClassLoader
负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
除此之外,用户也可以自定义类加载器:
User ClassLoad
通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的 ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
回到顶部
4、双亲委派机制
“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标 类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
“双亲委派”机制加载Class的具体过程是:
ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托 给父类加载器。
父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖 父类加载器。
依此类推,直到始祖类加载器(引用类加载器)。
始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
依此类推,直到源ClassLoader。
源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应 该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。