类加载器是干什么的?
负责将class文件(Java编译后的字节码文件)读取到内存,并转换为java.lang.Class的一个实例。
常见类加载器有哪些?分别是什么作用?
从虚拟机角度,分为启动类加载器和非启动类加载器。
- 启动类加载器(BootstrapClassLoader)是由C++实现,是虚拟机的一部分
- 其他的类加载器,都是由Java实现,全部继承自ClassLoader。
Java系统提供的类加载器主要有如下三种:
- BootstrapClassLoader(启动类加载器):加载 ${JAVA_HOME}\lib 或 -Xbootclasspath指定的目录下的类。
- ExtClassLoader(扩展类加载器):加载${JAVA_HOME}\lib\ext或环境变量java.ext.dir指定目录的类。
- AppClassLoader(应用程序类加载器):加载用户项目指定的classpath里的类,可通过ClassLoader的getSystemClassLoader()方法获得,所以也会称为系统类加载器。
如何确定类的唯一性?
问题等同于:如何判断两个类是否是同一个类?
对于任何一个类,由类加载器和类本身确立其在虚拟机中的唯一性。
- 首先类本身信息要相同,如类的全限定名。
- 其次必须是同一个类加载器加载的。
相同的类,被不同的类加载器加载,会被视为不同的类。
为什么要这样设计?
涉及到安全性问题,要配合双亲委派模型的机制来解释。
双亲委派(Parents Delegation)模型是什么?
直接看ClassLoader的loadClass()方法的源码就很直白。
- 除了启动类加载器,其他每个类加载器都有一个父类加载器(在ClassLoader源码中体现为类型为ClassLoader的parent成员变量,是组合关系而非继承关系)。
- 加载类时(ClassLoader的loadClass方法)会先通过父类加载器加载类,层层传递到顶层的启动类加载器(parent为null就通过native方法调用启动类加载器加载类)。
- 只有当父类加载器无法加载类,才会用当前的类加载器尝试加载类。
这个逻辑是Java设计者推荐的加载方式,并不是强制约束,开发者可以自定义类加载器复写loadClass()方法来改变这一流程。
双亲委派解决了什么问题?为何要这样设计?
是为了基础核心类加载的安全性考虑。
像java.lang.Object
这种系统是存放在rt.jar中的,无论哪个类加载器要加载这个类,都会委派启动类加载器加载Object,这样可以保证在各种环境下,加载出的Object都是<JAVA_HOME>\lib\rt.jar
的。
如果没有双亲委派机制,用户自己也定义了一个java.lang.Object
,写了有问题的代码,放在用户项目的Classpath,那就会影响所有类的基础行为,因为Object是所有类的父类。所以也会要求不同的类加载器加载同一个类属于不同的类。
双亲委派模型很好的解决了各个类加载器的基础类统一问题,越基础越公共的类越是由上层的类加载器加载。同时保证了基础类的不会被随意的篡改,保证安全感。
双亲委派会有哪些无法解决的问题?应该怎么解决?
当基础类是接口,需要加载不同的接口实现类,实现类并不在当前类加载器管控的类的范围里,双亲委派的类加载顺序就要反过来,由父类加载器去请求子类加载器加载接口实现类。
典型的场景是SPI(Service Provider Interface)依赖注入框架。
SPI约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
SPI的核心类ServiceLoader是jdk的类,位于rt.jar中,由BootstrapClassLoader加载,接口实现类肯定是不能由BootstrapClassLoader加载,BootstrapClassLoader只加载jdk核心类,只能由AppClassLoader或用户自定义的ClassLoader来加载,按照双亲委派机制的话,这样就无法完成了,只能逆向请求加载。
解决之道就是使用线程上下文加载器(ContextClassLoader),SPI在加载服务接口实现类时,调用Thread的getContextClassLoader()来加载实现类,可以通过Thread的setContextClassLoader(ClassLoader cl)方法来设置线程上下文类加载器,如果没有手动设置过,默认会继承父线程的上下文类加载器,如果父线程也没有设置过,则默认采用AppClassLoader
ServiceLoader.load()方法中要去加载实现类,需要用一个ClassLoader来加载,但是ServiceLoader是系统类,获取到的ClassLoader是BootstrapClassLoader,所以需要从一个地方获取实现类所处位置的ClassLoader,通过Thread的contextClassLoader就可以实现这个效果。
一个类加载器明明只有一个parent,Parents Delegation为什么被翻译为双亲委派?
翻译错误,以讹传讹。
由于除了启动类加载器其他类加载器都有parent,也就是每个非启动类加载器都有多个祖先,所以应该翻译为祖先委派更妥当
参考:
Class.forName和ClassLoader的loadClass有什么区别?动态加载一个类时,应该选用哪一个方式?
ClassLoader在loadClass后仅将字节码加载到内存,不会对类进行初始化。
Class.forName()在加载类到内存后后,会对类进行初始化,即执行类的static代码块和static变量的赋值,也可以传参控制不初始化。底层也是用ClassLoader来loadClass的。
Class.forName()用的调用者类的类加载器加载类,也可以手动传递classLoader参数
如果期望一个类加载后要执行static代码块做初始化操作,应该使用Class.forName(),如jdbc里的初始化。
参考: