众所周知,JAVA 包含四种 类加载器,分别是
Bootstrap ClassLoader 根类加载器 :
加载核心类库 lib 目录下的jar和class文件
可以通过如下语句打印根类加载的文件
System.out.println(System.getProperty("sun.boot.class.path"));
Extension ClassLoader 扩展类加载器:
加载lib/ext下的jar和class文件
可以通过如下语句打印扩展类加载的文件
System.out.println(System.getProperty("java.ext.dirs"));
AppClassLoader 应用类加载器;
加载应用程序中的classpath指定的jar和class文件
可以通过如下语句打印应用类加载的文件
System.out.println(System.getProperty("java.class.path"));
URLClassLoader 用来加载网络上远程的类
1、先简单说一下ClassLoader的原理
当执行 java ***.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是, Launcher$ExtClassLoader.class 与 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。
(引用 http://blog.csdn.net/feier7501/article/details/19133009)
首先我贴一下代码:
public class TestClassLoader {
public static void main(String[] args) throws Exception {
System.out.println("HelloWorld!");
ClassLoader cl=TestClassLoader.class.getClassLoader();
System.out.println(cl);
}
}
运行结果如下:
HelloWorld!
sun.misc.Launcher$AppClassLoader@593887c2
2、类加载过程
我们都知道,java的类加载器设计使用的是双亲委托模型,当前的类加载器(应用类加载器)收到一条加载指令,首先由顶层的类加载器即根类加载器进行加载,如果根类没有找到这个类,则交给扩展类加载器进行加载,如果还没有加载到,则交给应用类加载器进行加载,如果都加载不到,则抛出ClassNotFoundException 的异常。当加载类的时候,虚拟机需要完成三件事:
(1)通过类的全限定名来获取定义此类的二进制字节流
(2)将字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class的对象,作为方法区这个类的各种数据的访问入口/
注:获取二进制字节流的方法除了常规的通过java文件编译后的class 文件,也可以从网络中获取,zip读取,运行时计算生成,其他文件生成或者从数据库中读取。
完成了加载工作,下一步就是验证准备。验证是为了保证数据的合法性。
准备阶段则是类变量分配内存并设置类变量初始值的阶段。类变量是指类中定义的static修饰的变量,实例变量将会在对象实例化时随对象一起被分配在Java堆中。除此之外,如果变量被final修饰,则会为变量生成ConstatnValue属性,在准备阶段,虚拟机就会给变量赋值,准备阶段注意的是为变量赋初始值,比如int 类型的数据初始值是0,但是 int a=3 为a赋值3的操作并没有发生在准备阶段。
准备工作完成,则开始进行解析。解析工作主要是讲常量池内的符号引用替换为直接引用。 这个阶段目前还没有怎么研究透,笔者不多说。
解析完毕,则开始进行初始化,前面我们提到的static修饰的类变量,在初始化阶段完成初始化。初始化阶段是执行类构造器<clinit>()方法的过程。由此我们引出几个要点,
a、初始化时机
虚拟机规范了有且只有5中情况需要立即对类进行初始化
(1)遇到new、getstatic、putstatic、invokestatic四条指令时,在我们写程序的时候分别对应如下几种情况,new 对象,读取或设置一个类的静态字段(被final修饰的除外,被final修饰,编译的时候就把被修饰的数据放入常量池),调用定静态方法
(2)使用反射对类调用,类还没有进行过初始化,则先触发初始化
(3)初始化类的时候,父类还没有进行过初始化,则先进行父类的初始化
(4)虚拟机启动,包含main方法的主类
(5)不常见,jdk1.7动态语言支持,如果一个java.lang.invoke.MethodHandle的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则触发初始化
以上行为都称为对一个类的主动引用,、除此之外,还有一个被动引用的概念。
(1)对于静态字段,只有直接定义这个字段的类才会被初始化。父类定义静态字段,子类继承并使用这个字段只会触发父类的初始化,而不会触发子类 的初始化。
(2)创建数组, SuperClass[] sca=new SuperClass[10]; 这种情况下,不会触发SuperClass类的初始化动作。
(3)访问常量
b、clinit 方法介绍
(1) 该方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块汇总的语句合并产生的,收集顺序与源文件中出现的顺序一样,也就是我们定义static变量的先后顺序。静态语句块可以给静态变量赋值,但是如果该静态变量的访问发生在定义之前,则编译不过。
代码如下:
public class Test{
static{
i=0; // 编译通过
System.out.println(i);//编译提示非法向前引用
}
static int i=1;
}
(2)该方法与类的构造函数不同。,不需显示的调用父类构造器,虚拟机会保证在子类的clinit()方法执行之前,父类的clinit 方法已经执行完毕。那么由此可以判断,第一个被执行clinit 方法的类则是Object。
(3)clinit不是必需的,如果类汇总没有静态语句块,和静态变量赋值操作,则编译器不申城clinit 方法
(4)接口中如果有静态变量赋值,也会生成clinit方法,但是如果有父接口,只有在使用父接口定义的静态变量时,才会初始化父接口,另外实现类在初始化时也不会执行接口的clinit方法。
(5)虚拟机会保证一个类的clinit 方法在多线程环境中被正确的加锁、同步,如果多线程同时去初始化一个类,那么只有一个线程会去执行这个类的clinit方法,其他线程需要阻塞等待,
c、加载顺序
父类静态代码(代码块和静态变量出现的先后顺序)--》子类的静态代码块--》父类非静态--》父类构造--》子类非静态--》子类构造
3、自定义类加载器实现
class MyClassLoader extends ClassLoader{
//类加载器名称
private String name;
//加载类的路径
private String path = "D:/";
private final String fileType = ".class";
public MyClassLoader(String name){
//让系统类加载器成为该 类加载器的父加载器
super();
this.name = name;
}
public MyClassLoader(ClassLoader parent, String name){
//显示指定该类加载器的父加载器
super(parent);
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return this.name;
}
/**
* 获取.class文件的字节数组
* @param name
* @return
*/
private byte[] loaderClassData(String name){
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
this.name = this.name.replace(".", "/");
try {
is = new FileInputStream(new File(path + name + fileType));
int c = 0;
while(-1 != (c = is.read())){
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
/**
* 获取Class对象
*/
@Override
public Class<?> findClass(String name){
byte[] data = loaderClassData(name);
return this.defineClass(name, data, 0, data.length);
}
}
4、自定义类加载器使用场景
一般来讲,由JAVA提供的类加载器已经可以满足大部分功能需要,但是为什么还需要自定义类加载器呢?综合考虑,可能有以下几个方面的原因:
(1)文件加密,java 编译后的文件可以通过反编译获取到源码,处于安全的考虑,可能需要对class文件加密,这样的话当得到了加密后的class文件,通过算法解密之后,再通过类加载器加载。
(2)非标准的类数据获取来源,前文提到,类加载器获取定义类的二进制流的时候,除了常规的class文件,也可以从网络中、zip、数据库中来加载。
(3)动态创建
5、Tomcat6自定义类加载器实现
public synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// 检查当前类加载器有没有加载过该类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// 检查父类加载器有没有加载过该类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//试图通过系统类加载器来
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
<span style="white-space:pre"> </span>//通过标志位判断是否传给父类进行加载
boolean delegateLoad = delegate || filter(name);
/
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;//首先获取父类加载器
if (loader == null)<span style="white-space:pre"> </span> //如果父类加载器是空的, 则默认加载器是系统类加载器
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
// 自身加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
// 父类加载器加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
throw new ClassNotFoundException(name);
}
基本上tomcat加载的思路是,先判断类是否被已经被加载,先判断当前类加载器,再判断父类加载器,如果没有,则首先由系统类加载器进行加载,如果还没有成功,根据delegate标志位判断是否由父类加载器加载,如果是,则先由父类加载器进行加载,加载成功,返回,加载失败则由当前类加载器进行加载,如果还是失败,则交由给父类进行加载,如果加载不成功,则抛出异常,也就是说,当发生加载行为的时候,tomcat的webappClassLoader的做法是先由当前类加载器进行加载,然后交给父类,与我们所了解的双亲委派模式设计的加载顺序不同,
7、补充类加载过程的几个实验程序
public class TestClassLoader {
public static void main(String[] args) throws Exception {
System.out.println(Child.p1);
}
}
class Parent {
public static int p1=3;
public static Parent c=new Child();
static {
System.out.println("父类静态代码块");
}
public Parent(){
System.out.println("父类构造函数");
}
}
class Child extends Parent{
public static int c2=4;
public static Parent p=new Parent();
public static final Single s=new Single();
static{
System.out.println("子类静态代码块");
}
public Child(){
System.out.println("子类构造函数");
}
}
运行结果如下:
父类构造函数
Single
子类静态代码块
父类构造函数
子类构造函数
父类静态代码块
3
这里需要注意的情况则是,父类初始化过程中如果遇到对象创建的指令,即父类代码走到 new Child()的时候,子类中有个 new Parent的指令,但此时父类尚未完全初始化完毕,至于java如何处理这种情况,目前尚未找到相关资料来说明这点,这块先放着!!!