Java虚拟机(一)-Java类加载的方式
一、JVM三种预定义类型类加载器
当JVM启动的时候,Java开始使用如下三种类型的类加载器:
BootStrap ClassLoader
启动(Bootstrap)类加载器
:启动类加载器是用本地代码实现的类加载器,C++实现,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中,rt.jar这个jar包就是Bootstrap根类加载器负责加载的,其中包含了java各种核心的类如java.lang,java.io,java.util,java.sql等。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path")
查看。
例如
/** * BootStrap ClassLoader 加载的文件 */ public class Test { public static void main(String[] args)throws Exception{ System.out.println(System.getProperty("sun.boot.class.path")); } }
输出:
C:\Program Files\Java\jre1.8.0_201\lib\resources.jar; C:\Program Files\Java\jre1.8.0_201\lib\rt.jar; C:\Program Files\Java\jre1.8.0_201\lib\sunrsasign.jar; C:\Program Files\Java\jre1.8.0_201\lib\jsse.jar; C:\Program Files\Java\jre1.8.0_201\lib\jce.jar; C:\Program Files\Java\jre1.8.0_201\lib\charsets.jar; C:\Program Files\Java\jre1.8.0_201\lib\jfr.jar; C:\Program Files\Java\jre1.8.0_201\classes
EtxClassLoader
扩展(Extension)类加载器
:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)Java类实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")
查看。
例如
/** * EtxClassLoader 加载文件 */ public class Test { public static void main(String[] args)throws Exception{ System.out.println(System.getProperty("java.ext.dirs")); } }
输出:
C:\Program Files\Java\jre1.8.0_201\lib\ext;
AppClassLoader
系统(System)类加载器
:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器,我们写的代码默认就是由它来加载ClassLoader.getSystemClassLoader返回的就是它。具体可由系统类加载器加载到的路径可通过System.getProperty("java.class.path")
查看。
例如
/** * AppClassLoader 加载文件 */ public class Test { public static void main(String[] args)throws Exception{ System.out.println(System.getProperty("java.class.path")); } }
输出:
C:\Users\xxx\project\demo\target\classes
二.双亲委派机制
1.类加载器之间的关系:
应用程序都是由这3种类加载器互相配合进行加载的,如果有必要还可以加入自己定义的类加载器。这些类加载器之间的关系如下图:
图中的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的根类加载器以外,其余的类加载器都应该有自己的父类加载器(一般不是以继承实现,而是使用组合关系来复用父加载器的代码)。
如果一个类收到类加载请求,它首先请求父类加载器去加载这个类,只有当父类加载器无法完成加载时(其目录搜索范围内没找到需要的类),子类加载器才会自己去加载。
要理解双亲委派,可以查看ClassLoader.loadClass方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { // 没有被加载过 long t0 = System.nanoTime(); // 先委派给父类加载器加载 try { // 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器 if (parent != null) { c = parent.loadClass(name, false); } else { //如果父加载器不存在,则委托给启动类加载器 加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 如果父类加载器无法加载,自身才尝试加载 long t1 = System.nanoTime(); c = findClass(name); //jvm统计 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 连接类 if (resolve) { resolveClass(c); } return c; } }
先检查是否已经被加载过,若没有加载则调用父加载器的loadClass() 方法,若父加载器为空,则默认使用启动类加载器作为父加载器。如果父加载器失败,再调用自己的findClass 方法进行加载,因此到这里再次证明了类加载器的过程。
2.双亲委托模型好处
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object(存放于rt.jar中),是所有类的父类,所以任意一个类启动类加载时,都需要先加载Object类。在类加载器来看,所有的加载Object类的请求,都会逐级委托,最后都委托给Bootstrap根类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。(否则,系统中出现的Object类都不尽相同则会出现一片混乱)
3.类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达更通俗一些:比较两个类是否”相等”,只有再这两个类是有同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
4.用户自定义类加载器
通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程(可以结合后面问题解答一起看):
1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
3、调用本类加载器的findClass(…)方法,试图获取对应的字节码。如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败, 向上抛异常给loadClass(…), loadClass(…)转而调用findClass(…)方法处理异常,直至完成递归调用。
必须指出的是,这里所说的自定义类加载器是指JDK1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。整个加载类的过程如下图: