类加载器是一个用来加载类文件的类。Java源代码经过javac编译器编译成类文件。而后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其余来源的类文件。java
Java类加载器的做用就是在运行时加载类。Java类加载器基于三个机制:委托、可见性和单一性。web
委托机制:将加载一个类的请求交给父类加载器,若是这个父类加载器不可以找到或者加载这个类,那么再加载它。bootstrap
可见性:子类的加载器能够看见全部的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。数组
单一性:仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。缓存
正确理解类加载器可以帮你解决NoClassDefFoundError和java.lang.ClassNotFoundException,由于它们和类的加载相关。tomcat
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
每种类加载器都有设定好从哪里加载类。
将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)方法来获取和设置线程的上下文类加载器。
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
Tomcat不能使用系统默认的类加载器
若是使用默认类加载器,能够直接操做系统目录,不安全。对于web应用服务器,类加载器的实现方式和通常的Java运用不一样。
每一个web应用都有一个对应类加载器实例。该类加载器也使用代理模式(不一样于双亲模式)所不一样的是它首先尝试去加载某个类,若是找不到再代理给父类加载器。这与通常类加载器的顺序是相反的,也是为了保证安全,这样核心库就不在查询范围内。
Tomcat为了安全须要实现本身的类加载器。为每一个webapp提供本身的加载器。
(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的一个实现框架