一、类加载器
1、分类
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
1、启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
2、所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
1、启动类加载器:Bootstrap ClassLoader,
跟上面相同,它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
主要加载JVM自身工作所需的类,完全由JVM自己控制。没有父加载器,也没有子加载器,只是一个类加载工具。根类加载器从System.getProperty(“sun.boot.class.path”)所指定的目录中加载类库。
2、扩展类加载器:Extension ClassLoader,
该加载器由sun.misc.Launcher$ ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由System.getProperty(“java.ext.dirs”)指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
3、应用程序类加载器:Application ClassLoader,
该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(System.getProperty(“java.class.path”),就是我们常用的classpath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件
特别注意:父加载器不等同于父类(加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。)。我们实现自己的类加载器(不论是继承ClassLoader、还是URLClassLoader或者其他子类),其父加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为其父加载器,而getSystemClassLoader()方法获取到的正是AppClassLoader。
2、类加载器类继承关系图
①ClassLoader:抽象类,类中未定义如何去找到指定类并把它的字节码加载到内存,需要ClassLoader的子类去实现。(findClass方法)
二、类加载时机
JVM 规范没有强制约束类加载过程的第一阶段(加载)什么时候开始,但对于“初始化”阶段,有着严格的规定。
1、主动引用
有且仅有 5 种情况必须立即对类进行“初始化”:
1.在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化。
2.对类进行反射调用时,如果类还没有初始化,则需要先触发初始化。
3.初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
4.虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
5.当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。
这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。
2、几种被动引用:
1.通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
2.通过数组定义来引用类,不会触发此类的初始化。
3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
编译通过之后,常量存储到 NotInitialization 类的常量池中,NotInitialization 的 Class 文件中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就没有任何联系了。
3、关于接口加载
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
三、类加载机制
双亲委派模型的工作流程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
好处:
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
loader1首先从自己的命名空间中查找Sample类是否已经被加载,如果已经加载,就直接返回代表Sample类的Class对象的引用。若所有的父加载器及loader1本身都不能加载,则抛出ClassNotFoundException异常。
定义类加载器、初始类加载器:
定义类加载器:若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器。
初始类加载器:所有能成功返回Class对象的引用的类加载器(包括定义类加载器,即包括定义类加载器和它下面的所有子加载器)都被称为初始类加载器。
命名空间
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
在同一个命名空间中,不会出现类的全限定名相同的两个类。
在不同的命名空间中,有可能会出现类的全限定名相同的两个类。
运行时包
由同一类加载器加载的属于相同包的类组成了运行时包。
只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员。
这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。
四、类加载
将类的class文件中的二进制数据从硬盘读取到内存中,将其放置在运行时数据区的方法区(虚拟机规范并未要求,但hotspot虚拟机将其放置在虚拟机中)内,然后在内存中创建唯一一个类的class对象,用以封装类在方法区中的数据结构,无论new出多少个对象,最终对应的class对象只有一个。
1、JVM加载class文件到内存的方法
①隐式加载:通过JVM自动加载需要的类到内存中
②显式加载:通过写代码去调用ClassLoader类来加载 1)forName() 2)loadClass() 3)findClass()
2、加载过程
①加载
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
②验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
1、文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
2、元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
3、字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
4、符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
③准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。
④解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。
⑤初始化
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
总结:
整个类加载过程中,除了在加载阶段用户应用程序可以自定义类加载器参与之外,其余所有的动作完全由虚拟机主导和控制。到了初始化才开始执行类中定义的Java程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于 < clinit >()方法。类加载过程中主要是将Class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始。
3、查看加载类
jvm虚拟机参数:都以-XX:开始
-XX:+ 表示开启option选项 ,
-XX:- 表示关闭option选项,
-XX:=,表示将option值设置为value
例如:
-XX:+TraceClassLoading
有时候我们需要监控系统中哪些类被加载进来,什么样的类加载的比较频繁,什么样的类加载的比较少,可以使用这个参数来配置打印出程序执行过程中类的加载信息。
4、类的卸载
参考 类的卸载机制
双向关联关系:
加载器和Class对象:
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。
另一方面,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()方法,就能获得它的类加载器。
由此可见,Class实例和加载它的加载器之间为双向关联关系。
当类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。由用户自定义的类加载器加载的类是可以被卸载的。
参考
https://www.cnblogs.com/zxguan/p/7798381.html
https://www.cnblogs.com/hqlong/p/6814373.html