【java虚拟机】类加载器ClassLoader相关知识
参考:https://blog.csdn.net/briblue/article/details/54973413
一、双亲委派模式
sun.misc.Launcher
sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候回准备应用程序运行中需要的类加载器。
Launcher作为JAVA应用的入口,根据双亲委派模型,Laucher是由JVM创建的,它类加载器应该是BootStrapClassLoader, 这是一个C++编写的类加载器,是java应用体系中最顶层的类加载器,负责加载JVM需要的一些类库(<JAVA_HOME>/lib)
Launcher类创建了另两个类加载器:ExtClassLoader 和 AppClassLoader
1、双亲委派模式概念
当一个类加载器收到一个类加载的请求,它首先会将该请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该被传入到顶层的启动类加载器(Bootstrap ClassLoader)中,只有当父类加载器反馈无法完成这个列的加载请求时(它的搜索范围内不存在这个类),子类加载器才尝试加载。
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:

上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。
我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。

2、Bootstrap ClassLoader
主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
3、Extended ClassLoader
扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
4、AppClass ClassLoader
加载当前应用的classpath的所有类。
=========重要===============
2. 如果一个类引用的其他的类,那么这个其他的类由谁来加载?
3. 既然类可以由不同的加载器加载,那么如何确定两个类如何是同一个类?
对于动态加载jar而言,JVM默认会使用第一次加载该jar中指定类的类加载器作为默认的ClassLoader。
假设我们现在存在名为sbbic的jar包,该包中存在ClassA和ClassB这两个类(ClassA中没有引用ClassB).现在我们通过自定义的ClassLoaderA来加载在ClassA这个类,那么此时此时ClassLoaderA就成为sbbic.jar中其他类的默认类加载器.也就是,ClassB也默认会通过ClassLoaderA去加载.
那么如果ClassA中引用了ClassB呢?当类加载器在加载ClassA的时候,发现引用了ClassB,此时类加载如果检测到ClassB还没有被加载,则先回去加载.当ClassB加载完成后,继续回来加载ClassA.换句话说,类会通过自身对应的类加载器加载其他引用的类.
JVM规定,对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立在java虚拟机中的唯一性。
通俗点就是说,在jvm中判断两个类是否是同一个类取决于类加载器和类本身,也就是同一个类加载器实例加载的同一份Class文件生成的Class对象才是相同的,类加载器不同,或者加载器的实例不同,那么这两个类一定不相同。
二、ClassLoader 的算法实现
loadClass()
JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
上面是方法原型,一般实现这个方法的步骤是
执行findLoadedClass(String)去检测这个class是不是已经加载过了。
执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。
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) { //父加载器不为空则调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空则调用Bootstrap Classloader 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. long t1 = System.nanoTime(); //父加载器没有找到,则调用findclass c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { //调用resolveClass() resolveClass(c); } return c; } }
代码解释了双亲委托。
另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。
另外
if (parent != null) { //父加载器不为空则调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空则调用Bootstrap Classloader c = findBootstrapClassOrNull(name); }
前面说过ExtClassLoader的parent为null,所以它向上委托时,系统会为它指定Bootstrap ClassLoader。
自定义ClassLoader
不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个classloader。
自定义步骤
1、编写一个类继承自ClassLoader抽象类。
2、复写它的findClass()方法。
3、在findClass()方法中调用defineClass()。
defineClass()
这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
注意点:
**一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 **
上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
三、自定义类加载器
1、在系统的用户目录下定义一个java类,HelloWord 并编译成功
public class HelloWord{ public void sayHello(){ System.out.println("shangxiaofei hello word"); } }
2、在项目代码里定一个classLoader,并加载指定类,执行其方法
public class MyClassLoader extends ClassLoader { /** * 从其他地方加载类的信息 */ private static final String path="/Users/shangxiaofei"; @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String fileName = getFileName(name); File file = new File(path,fileName); try { FileInputStream is = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); } } catch (IOException e) { e.printStackTrace(); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(name,data,0,data.length); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.findClass(name); } //获取要加载 的class文件名 private String getFileName(String name) { // TODO Auto-generated method stub int index = name.lastIndexOf('.'); if(index == -1){ return name+".class"; }else{ return name.substring(index+1)+".class"; } } }
public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { MyClassLoader myClassLoader = new MyClassLoader(); Class<?> cls = myClassLoader.loadClass("HelloWord"); Object obj = cls.newInstance(); Method method=cls.getMethod("sayHello"); method.invoke(obj,null); } }
四、类加载的过程
类的的整个生命周期:加载,验证,准备,解析,初始化,使用,卸载
1、加载
- 通过1个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2、验证
验证字节流的合法性(处于语言的安全性考虑 )
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证。
3、准备
- 主要为类变量赋初始的零值。(static修饰的变量,不包含fianl的常量)
- public static int value=123. 在此节点value为0
- 因为123,在程序编译为字节码时,已经被移动至类构造器<clinit()>方法中。
4、解析
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
5、初始化、
- 执行<clinit()> 执行源文件中的static静态块,静态变量的赋值。

浙公网安备 33010602011771号