类加载器原理
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。
Java类加载器的作用就是在运行时加载类。Java类加载器基于三个机制:委托、可见性和单一性。
委托机制:将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
可见性:子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
单一性:仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。
正确理解类加载器能够帮你解决NoClassDefFoundError和java.lang.ClassNotFoundException,因为它们和类的加载相关。
加载器树状结构
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。
有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库(jre/lib/rt.jar)或-Xbootclasspath参数指定路径的目录,是用原生C++代码来实现的,并不继承自java.lang.ClassLoader。(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库Java 虚拟机的实现会提供一个扩展库目录JRE/lib/ext或者java.ext.dirs指向的目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system/Application class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
自定义类加载器(custom class loader):除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
public static void main(String[] args) {
//application class loader
System.out.println(ClassLoader.getSystemClassLoader());
//extensions class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
//bootstrap class loader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
输出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null
image.png
每种类加载器都有设定好从哪里加载类。
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法去中的运行时数据结构,在怼中生成一个代表这个类的Class对象 ,作为方法去类数据的访问入口。
类缓存
标准的类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间,不过,JVM垃圾回收器可以回收这些Class。
双亲委托或代理机制
代理模式:交给其他加载器来加载指定类
双亲委托机制:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托交给父类加载器,父类加载器又将加载任务向上委托,直到最父类加载器,如果最父类加载器可以完成类加载任务,就成功返回,如果不行就向下传递委托任务,由其子类加载器进行加载。
双亲委托机制是为了保证Java核心库的类型安全。保证了不会出现用户自定义java.lang.Object类的情况。
类加载器除了用于加载类,也是安全的最基本的屏障
双亲委托机制是代理模式的一种。
并不是所有的类加载都是双亲模式,比如tomcat服务器也是使用代理模式,不同的是它首先尝试去加载类,如果找不到在代理给父类加载器,这与一般加载器是相反的。
Class.forname()是一个静态方法,最常用的是Class.forname(String className);根据传入的类的全限定名返回一个Class对象.该方法在将Class文件加载到内存的同时,会执行类的初始化.
ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化,该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.
自定义类加载器
1、继承java.lang.ClassLoader
2、首先检查请求的类型是否已经被这个类装载器装载到命名空间中,如果已经装载,直接返回。
3、委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例。
4、重写本类加载器的findClass(...)方法,试图获取对应字节码,如果获取得到,则调用defineClass(...)导入类型到方法区,如果获取不到对应的字节码或者其他原因失败,则终止加载过程。
注意:被两个类加载器加载的同一个类,JVM不认为是相同的类。
自定义文件系统类加载器
/**
* 自定义文件类加载器
*/
public class FileSysLoader extends ClassLoader {
private String rootDir;
public FileSysLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//检查有没有加载过这个类,如果已经加载直接返回这个类。
Class<?> c = findLoadedClass(name);
if (c!=null) {
return c;
}else{
ClassLoader parent = this.getParent();
try {
//委派给父类加载器
c = parent.loadClass(name);
} catch (Exception e) {
}
if(c!=null){
return c;
}else{
//如果父类也没加载,则自己加载,读取文件 进行加载
byte[] classData = getClassData(name);
if(classData==null){
//没有读取到文件,抛出异常
throw new ClassNotFoundException();
}else{
//生成Class对象
c = defineClass(name, classData, 0,classData.length);
}
}
}
return c;
}
/**
* 文件内容转为字节数组
*/
private byte[] getClassData(String classname){
String path = rootDir + File.separatorChar + classname.replace('.', File.separatorChar)+".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
is = new FileInputStream(path);
byte[] buffer = new byte[1024];
int temp=0;
while((temp=is.read(buffer))!=-1){
baos.write(buffer, 0, temp);
}
return baos.toByteArray();
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(baos!=null){
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用:
public class TestFileLoader {
public static void main(String[] args) throws Exception {
FileSysLoader loader = new FileSysLoader("C:/Person");
FileSysLoader loader1 = new FileSysLoader("C:/Person");
Class<?> c1 = loader.loadClass("com.temp.bytecodeop.Person");
Class<?> c2 = loader1.loadClass("com.temp.bytecodeop.Person");
Class<?> c3 = loader1.loadClass("com.temp.bytecodeop.Person");
Class<?> c4 = loader.loadClass("java.lang.String");
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
System.out.println(c4.hashCode());
System.out.println(c3.getClassLoader());
System.out.println(c1.getClassLoader().getParent());
System.out.println(c4.getClassLoader());
}
}
自定义网络类加载器
public class NetClassLoader extends ClassLoader {
private String rootUrl;
public NetClassLoader(String rootUrl){
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if(c!=null){
return c;
}else{
ClassLoader parent = this.getParent();
try {
c = parent.loadClass(name);
} catch (Exception e) {
}
if(c!=null){
return c;
}else{
byte[] classData = getClassData(name);
if(classData==null){
throw new ClassNotFoundException();
}else{
c = defineClass(name, classData, 0,classData.length);
}
}
}
return c;
}
private byte[] getClassData(String classname){
String path = rootUrl +"/"+ classname.replace('.', '/')+".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
URL url = new URL(path);
//通过URL获取流
is = url.openStream();
byte[] buffer = new byte[1024];
int temp=0;
while((temp=is.read(buffer))!=-1){
baos.write(buffer, 0, temp);
}
return baos.toByteArray();
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(baos!=null){
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
热部署类加载器
热部署就是利用同一个class文件不同的类加载器在内存创建出两个不同的class对象由于JVM在加载类之前会检测请求的类是否已加载过,如果被加载过,则直接从缓存获取,不会重新加载。
同一个加载器只能加载一次,多次加载将报错,因此实现的热部署必须让同一个class文件可以根据不同的类加载器重复加载,
实现所谓的热部署。自定义加载器后,直接调用findClass()方法,而不是调用loadClass()方法,因为ClassLoader中loadClass()方法体回调用findLoadedClass()方法进行了检测是否已被加载,因此我们直接调用findClass()方法就可以绕过这个问题。
前面的文件加载,使用了,父类委托方式,我们这里直接写自己加载。
/**
* 热部署类加载器
*/
public class FileDeployLoader extends ClassLoader {
private String rootDir;
public FileDeployLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//检查有没有加载过这个类,如果已经加载直接返回这个类。
byte[] classData = getClassData(name);
Class<?> c = null;
if(classData==null){
//没有读取到文件,抛出异常
throw new ClassNotFoundException();
}else{
//生成Class对象
c = defineClass(name, classData, 0,classData.length);
}
return c;
}
/**
* 文件内容转为字节数组
*/
private byte[] getClassData(String classname){
String path = rootDir + File.separatorChar + classname.replace('.', File.separatorChar)+".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
is = new FileInputStream(path);
byte[] buffer = new byte[1024];
int temp=0;
while((temp=is.read(buffer))!=-1){
baos.write(buffer, 0, temp);
}
return baos.toByteArray();
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(baos!=null){
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws ClassNotFoundException {
FileSysLoader loader = new FileSysLoader("C:/Person");
//直接调用findClass()方法,而不是调用loadClass()方法
//因为ClassLoader中loadClass()方法体回调用findLoadedClass()方法进行了检测是否已被加载
Class<?> c1 = loader.findClass("com.temp.bytecodeop.Person");
}
}
加密解密类加载器
/**
* 简单加密类
* 字节取反加密
*/
public class EncrptUtil {
public static void main(String[] args) {
//调用加密方法加密一个*.class文件
}
public static void encrpt(String src, String dest){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
int temp = -1;
while((temp=fis.read())!=-1){
fos.write(temp^0xff); //加密
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(fis!=null){
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(fos!=null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 加密解密类加载器
*/
class DecrptClassLoader extends ClassLoader {
private String rootDir;
public DecrptClassLoader(String rootDir){
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if(c!=null){
return c;
}else{
ClassLoader parent = this.getParent();
try {
c = parent.loadClass(name);
} catch (Exception e) {
}
if(c!=null){
return c;
}else{
byte[] classData = getClassData(name);
if(classData==null){
throw new ClassNotFoundException();
}else{
c = defineClass(name, classData, 0,classData.length);
}
}
}
return c;
}
private byte[] getClassData(String classname){
String path = rootDir +File.separatorChar+ classname.replace('.', File.separatorChar)+".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
is = new FileInputStream(path);
int temp = -1;
while((temp=is.read())!=-1){
baos.write(temp^0xff); //解密,再取反
}
return baos.toByteArray();
}catch(Exception e){
e.printStackTrace();
return null;
}finally{
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(baos!=null){
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
线程上下文类加载器
在Java中存在着很多服务提供者接口(Service Provider Interface,SPI),SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载。这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JCE、JAXP、JBI、JNDI等,而这些SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由系统加载器来加载,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库。
因此,线程类加载器是很好的选择,它为了抛弃双亲委派加载链模式。
每个线程都有一个关联的上下文加载器,如果new一个新的线程,如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。
image.png
rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
public class LoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = LoaderTest.class.getClassLoader();
System.out.println(loader);
ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
System.out.println(loader2);
Thread.currentThread().setContextClassLoader(new FileSysLoader("C:/Person"));
System.out.println(Thread.currentThread().getContextClassLoader());
Class<LoaderTest> c = (Class<LoaderTest>) Thread.currentThread().getContextClassLoader().loadClass("com.temp.bytecodeop.Person");
System.out.println(c);
System.out.println(c.getClassLoader());
}
}
输出:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$AppClassLoader@73d16e93
com.temp.classloader.FileSysLoader@6d06d69c
class com.temp.bytecodeop.Person
sun.misc.Launcher$AppClassLoader@73d16e93
服务器类加载器原理和OSGI介绍
Tomcat服务器的类加载机制
Tomcat不能使用系统默认的类加载器
如果使用默认类加载器,可以直接操作系统目录,不安全。对于web应用服务器,类加载器的实现方式和一般的Java运用不同。
每个web应用都有一个对应类加载器实例。该类加载器也使用代理模式(不同于双亲模式)所不同的是它首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的,也是为了保证安全,这样核心库就不在查询范围内。
Tomcat为了安全需要实现自己的类加载器。为每个webapp提供自己的加载器。
image.png
OSGI
(Open Service Gateway Initative)是面向Java的动态模块系统,它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。
OSGI已经被实现和部署在很多产品上,在开源社区也得到广泛的支持,Eclipse也是基于OSGI技术来构建的。
OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。
Equinox是OSGI的一个实现框架
©著作权归作者所有:来自51CTO博客作者huingsn的原创作品,如需转载,请注明出处,否则将追究法律责任