Java类加载器及tomcat类加载器解析

一、类的加载过程

Java类的加载分为三分阶段:装载类,链接类,类的初始化,其中链接又经历验证、准备、解析三个过程。

1) 装载:类加载器在对应的类缓存空间中查找类并加载类的二进制数据。
1.通过类的全限定类名查找该类并加载类的二进制流
2.将此二进制里所代表的静态存储结构转化为java内存方法区运行时数据结构
3.在内存堆中生成此类的class对象,作为该类的访问入口
2) 链接:
验证:确保类的正确性,确保所装载的二进制字节流信息符合jvm的要求,验证大致有以下的验证:
文件格式验证
源数据验证
字节码验证
符号引用验证
准备:位类的静态变量分配内存,并初始化默认值(仅包含类变量)
解析:将类中的符号引用转换为直接引用
3) 初始化:类装载过程的最后一步,到此才真正意义上地执行类中定义的程序代码;主要是为类中的静态变量赋予正确的初始值,当是调用new来装载类时,会调用类的构造器。

注意:一个java源文件经过jdk编译通过以后肯定是符合java字节码格式的,那为什么需要验证这一步骤呢?如果有高手或者大神编写的代码是用于恶意攻击的,那么在验证这个步骤就可以避免这样的情况,验证不通过则不初始化该类。
准备阶段看似与初始化阶段矛盾,其实不然;例如:static int a = 10;该语句的的执行过成是这样的,首先字节码被装载进内存以后,进行类的链接,验证类的正确性,进入准备阶段,因为a变量是static int类型,所以为其分配内存,并处化默认值为0,然后解析(后面介绍),并初始化该类,也就是将其真正的值10赋给a。

类的初始化:
类什么时候被初始化:
  1.创建类的实例,也就是调用类的构造函数new一个对象
  2.访问类或者接口的静态变量,或者是给静态变量赋值
  3.调用类的静态方法
  4.使用反射加载类(Class.forName("className");)
  5.初始化一个类的子类,则需先加载该类
初始化过程:
  1.如果该类还没有加载,则先装载和链接该类
  2.如果该类存在父类,并且父类还没有被初始化(在一个类加载器中,一个类只能初始化一次),则先初始化父类
    3.执行类中的初始化语句(static变量或者static块),按语句的顺序执行

二、类的加载器

类的加载过程是指将类的字节码以二进制的方式装载进内存后,在java内存的方法区中形成运行时数据结构,并在java内存堆中创建该类的Class对象,该对象用于封装方法区中的数据对象。
 1
 2

java类加载的最终产品是class对象,该对象对外提供了访问类的数据结构的各种发方法。

jdk中存在种类加载器:bootstrapClassLoader(启动类加载器),ExtClassLoader(扩展类加载器),AppClassLoader(系统类加载器),java中类的加载使用双亲委派模型,其加载模型如下:

图 3

  bootstrapClassLoader:主要加载的是jdk中开发环境相关的类 ,位于JAVA_HOME/jre/lib/*中,例如rt.jar。可以通过String dir = System.getProperty("sun.boot.library.path");查看。
ExtClassLoader:主要加载jdk扩展功能相关的类:位于JAVA_HOME/jre/lib/ext/*中,可以通过
String dir = System.getProperty("java.ext.dirs");查看。
AppClassLoader:主要加载classpath中的类,例如项目中程序员自己写的类以及加入到buildpath中类,可以通过
String dir = System.getProperty("java.class.path");查看。

双亲委派模型概述:
从我们学习java的时候就知道,一个类是由它的包名+类名唯一确定一类的,也就是一个类的全限定类名;其实并不是
这样的;每一个类加载器都有自己的命名空间,有自己的独立的类缓存空间,可以理解为当该类加载器加载类的时候,
加载成功的类都是放在它自己的缓存空间中,当下一次加载相同类名的类时,只需要从它的缓存中查找即可,不需要重新加载;
在一个类加载器中,一个类的全限定类名是唯一的,也就是一个全限定类名 在一个类加载器中能够唯一确定一个类。
但是当在另一个类加载器中却可以有跟这个类加载器具有相同限定类名的类。所以唯一确定一个类是由类对应的类加载器
+类的全限定类名唯一确定。
然而在java中,同一个java项目中是不允许不同类加载器中的类具有相同的限定类名的,不信?我们来验证一下:
看下面给的类,我新建一个项目,新建一个java.lang.Object类如下:
package java.lang;

public class Object {
public static void main(String[] args) {
System.out.println("3333333333");
}
}
运行后得到结果:
Error: Main method not found in class java.lang.Object, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

看到错误的类型是Error,错误的信息大概是java.lang.Object中没有定义main方法,因此可以断定真正执行的是jdk中的
java.lang.Object类,并非我们自己的的Object类。我们再来看以下这个类:
package java.lang;

public class Object1 {
public static void main(String[] args) {
System.out.println("3333333333");
}
}
该类不是jdk中的类但是却package却跟jdk中的包名java.lang一样,运行结果如下:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
java.lang包名是被禁止使用的。

从第一个例子的分析我们知道,当我们写一个跟jdk中的类同名时,真正被加载执行的是jdk中的类,而并非是我们
自己写的类。这是为什么呢?这是因为java类加载器加载类的时候遵循一个原则,也就是我们要说的双亲委派模型()
结合图 3进行理解:
当一个类加载器第一次接收到一个类加载请求时,它先不加载该类。而是先找到它的父亲,让它的父亲去加载这个类,
而它的父亲也会先让它的父亲的父亲去加载这个类,以此类推,这就是图3中的自低向上的加载检查,。当最顶层的类加载器
(jvm中可以理解就是boostrapClassLoader) 从它对应的路径中找不到这个类时,就会让其下一层的类加载器去加载该类,
以此类推,直到最后回到当前的类加载器都加载不到时,就会抛出classNotFoundException异常。以上只有有一层的类加载器
能够加载到该类,则返回对应的Class对象。这就是java中双亲委派模型,使用双亲委派的机制保证不同的类加载器中的类
具有不同的限定类名。

通过查看源码理解类加载器以及双亲委派模型:
java类加载器继承结构:
所有的classLoader都有一个父亲,在类加载器基本类ClassLoader中有一个parent属性,指定了该类加载器的父亲,
jdk中跟类加载相关的类主要就是sun.misc.Launcher类,在该类中有两个内部类ExtClassLoader和AppClassLoader
通过查看源码可以知道ExtClassLoader是AppClassLoader父亲。
ExtClassLoader localExtClassLoader;
try
{
localExtClassLoader = ExtClassLoader.getExtClassLoader();
}
catch (IOException localIOException1)
{
throw new InternalError("Could not create extension class loader", localIOException1);
}
try
{
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
}

我们知道bootstrapClassLoader是ExtClassLoader的父亲,但是从源码中我们并没有看到,其实bootstrapClassLoader
不是java中的类,它是c++中的类,请看一下源码:

synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
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();
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(c);
}
return c;
}
当当前类加载器的父亲是null时,则调用native方法f
indBootstrapClassOrNull(name);说明当一个类加载器的
父亲是null时,则该类加载器的父亲是bootstrapClassLoader,看ExtClassLoader中对其的初始化:
public ExtClassLoader(File[] paramArrayOfFile)
throws IOException
{
super(null, Launcher.factory);
SharedSecrets.getJavaNetAccess()
.getURLClassPath(this).initLookupCache(this);
}
可见, ExtClassLoader的父亲是bootstrapClassLoader。

之前我们讨论过我们自己写的类都是由AppClassLoader来加载的,这是因为我们自己写的类都会在当前项目的classpath中,
可以通过以下方法获得当前项目的classpath:
// private static String buildClassPath() {
// String classpath = null;
// StringBuilder sb = new StringBuilder();
// for (URL url : ((URLClassLoader)LoaderTest.class.getClassLoader()).getURLs()) {
// String p = url.getFile();
// sb.append(p).append(File.pathSeparator);
// }
// classpath = sb.toString();
// return classpath;
// }
可以通过以下方法获得加载当前类的类加载器以及父亲:
System.out.println(Test2.class.getClassLoader());
System.out.println(Test2.class.getClassLoader().getSystemClassLoader());
System.out.println(Test2.class.getClassLoader().getParent());
输出结果:
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$ExtClassLoader@2a139a55

三、自定义类加载器
一般自定义一个类加载器时,选择继承java.lang.ClassLoader,然后重写findClass方法,做法是传入字节码,调用
defineClass方法定义类。如下
class MyClassLoader extends ClassLoader {
private byte[] results;

private MyClassLoader(){
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if(this.getResults() == null || this.getResults().length==0){
return null;
}

Class<?> clazz = null;
try {
clazz = defineClass(name, results, 0, results.length);
} catch (NoClassDefFoundError e) {
throw new ClassNotFoundException("",e);
}catch (ClassFormatError e) {
throw new ClassNotFoundException("",e);
}

return clazz;
}

public byte[] getResults() {
return results;
}

public void setResults(byte[] results) {
this.results = results;
}
}

但是这种自定义的classClassLoader只能在jvm中运行使用,一旦部署到tomcat或者是其他容器中运行则可能报
ClaaNotFoundException异常(当所加载的类中需要import进程序人员自己写的类或者是第三方的类时),原因在于像
tomcat等服务器的类加载机制并不是完全遵循java类加载的双亲委派机制,因为tomcat有自己的一套加载器机制。当需要
使自定义的类加载能够在tomcat中运行时,可以选择重写loadClass方法(在以上的程序中增加以下代码):
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//从当前加载器命名空间已加载的类中查找
Class<?> clazz = findLoadedClass(name);
if(clazz != null){
if(resolve){
resolveClass(clazz);
}
return clazz;
}

//交给父亲去加载
if(null != getParent()){
try {
clazz = getParent().loadClass(name);
if(clazz != null){
if(resolve){
resolveClass(clazz);
}
return clazz;
}
} catch (Exception e) {
// TODO Auto-generated catch block
// e.printStackTrace();
}
}

//使用当前线程的类加载器来加载类
try {
clazz = Class.forName(name, true, Thread.currentThread().getContextClassLoader());
if (clazz != null){
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (Exception e1) {

}

//依然找不到该类,则使用当前浏览器加载
try {
clazz = findClass(name);
} catch (ClassNotFoundException e) {
throw new ClassNotFoundException("",e);
}

return clazz;
}
以上自定义的类加载器是直接继承ClassLoader来实现,当我们存在需要在类加载的时候可以指定jar路径时的需求时,
以上自定义的类加载器是无法实现的。需要选择继承URLClassLoader,如下:
//已定义classLoader
class MyClassLoader extends URLClassLoader {

public MyClassLoader(URL[] urls) {
super(urls);
}

private MyClassLoader(){
super(new URL[]{});
}

private byte[] results;

@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//从当前加载器命名空间已加载的类中查找
Class<?> clazz = findLoadedClass(name);
if(clazz != null){
if(resolve){
resolveClass(clazz);
}
return clazz;
}

//交给父亲去加载
if(null != getParent()){
try {
clazz = getParent().loadClass(name);
if(clazz != null){
if(resolve){
resolveClass(clazz);
}
return clazz;
}
} catch (Exception e) {
// TODO Auto-generated catch block
// e.printStackTrace();
}
}

//使用当前线程的类加载器来加载类
try {
clazz = Class.forName(name, true, Thread.currentThread().getContextClassLoader());
if (clazz != null){
if (resolve) {
       resolveClass(clazz);
   }
   return clazz;
}
} catch (Exception e1) {

}
            
            //使用父类的findClass方法进行查找
            try {
clazz = findClass(name);
if(clazz != null){
if(resolve){
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
}
            
            //使用自定义的findClass查找
            try {
clazz = findMyClass(name);
} catch (Exception e) {
throw new ClassNotFoundException("",e);
}

return clazz;
}


protected Class<?> findMyClass(String name) throws ClassNotFoundException {
if(this.getResults() == null || this.getResults().length==0){
return null;
}

Class<?> clazz = null;
try {
clazz = defineClass(name, results, 0, results.length);
} catch (NoClassDefFoundError e) {
throw new ClassNotFoundException("",e);
}catch (ClassFormatError e) {
throw new ClassNotFoundException("",e);
}

return  clazz;
}

public void addUrl(URL url){
this.addURL(url);
}


public byte[] getResults() {
return results;
}


public void setResults(byte[] results) {
this.results = results;
}
}

该自定义类加载器可以在Tomcat中使用,实现了以上多种需求。自定义类加载器时,我们一般是使用默认构造函数,
然后在其中调用父类的构造函数,这样的自定义类加载器,它的父亲默认是AppClassLoader。我们可以从源码中看出来:
以下是ClassLoader中的默认构造函数:
protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

getSystemClassLoader()是parent参数,我们看看放回的是谁,继续往下:

 @CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }
返回的scl,继续看initSystemClassLoader()
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
scl引用的是Launcher类中的getClassLoader()方法返回的类加载器,继续看Launcher类:
 public ClassLoader getClassLoader()
  {
    return this.loader;
  }
this.loader的初始化:
 try
    {
      this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    }
到此可以看出我们自定义的类加载器,当没有给父类传指定的parent时,其默认的parent就是AppClassLoader。

四、类的卸载
我们知道在java中存在非常智能的垃圾回收机制,一个对象被回收的条件是unreachble不可达,也就是堆中的某个对象
在桟中不存在任何引用时,垃圾回收器就会回收该对象。这是对象的回收,那么对于一个已经被类加载器加载成功以后的类
是否能够被回收呢?答案是肯定的(从内存性能优化的角度考虑)。
类被卸载的条件:
1.该类对应的所有对象已经被回收
2.该类对应的class对象不存在任何引用,或者已经被回收
3.该类对应的类加载器对象可以被回收,或者已经被回收

满足以上三个条件后,那么该类可以被卸载。那么我们是否可以做到手动去卸载一个类呢?从我所查阅的资料以及自己
的理解,几乎不可能也是不切实际的。一个类被类加载器加载成功以后,该类对应的class对象会保存在类加载器对应的
缓存空间中,一个类只能被加载一次。当试图加载一个具有相同名字的类时,会报类重复的异常。而当你需要卸载一个类时,
意味着需要销毁其对应的类加载器,所以该类加载器所加载的其他所有的类都会无法使用。如果由于业务需要更细一个类的信息,
可以使用缓存的方法,将类加载器加载过的类以及字节码信息放到缓存当中,然后新建新的类加载器重新装载所有的类。
一些服务器,像现在我们常用的tomcat,支持热部署。由于在tomcat中,每一个web项目,都会有自己对应的唯一一个
WebAppClassLoader,所以使得热部署成为可能。热部署的原理其实就是给对应的项目新建一个新的类加载器,重新装载
所有的类。

五、tomcat类加载机制简析:
不同的tomcat版本,其类加载机制有所差别,本文所讲的都是基于tomcat7中的类加载机制。tomcat 7中的类加载机制同样
遵循java的双亲委派模型,tomcat中存在以下几种类加载器:

上一层类加载器是下一层的父亲。如java一样,不同的类加载器加载不同路径下的class。图解:

图 4


图 5
Bootstrap :该类加载器加载的启动jvm所需的类以及标准扩展类(位于jre/lib/ext/*)
System:根据$CATALINA_HOME/bin/catalina.sh or %CATALINA_HOME%\bin\catalina.bat中的配置去加载不同的jar,例如:
$CATALINA_HOME/bin/bootstrap.jar,$CATALINA_HOME/bin/tomcat-juli.jar
$CATALINA_HOME/bin/bootstrap.jar:主要包含启动tomcat服务器所需要的类以及所需要的一些加载器实现类
$CATALINA_HOME/bin/tomcat-juli.jar:主要加载的是跟日志相关的类
CommonClassLoader:根据$CATALINA_BASE/conf/catalina.properties中的配置去加载对应的class,例如以下路径中的类:
  • unpacked classes and resources in $CATALINA_BASE/lib
  • JAR files in $CATALINA_BASE/lib
  • unpacked classes and resources in $CATALINA_HOME/lib
  • JAR files in $CATALINA_HOME/lib
WebAppClassLoader:tomcat会给每一应用分配一个唯一的该类加载器对象,用于加载对应应用下的所有类和资源,也就是:
WEB-INF/classes和WEB-INF/lib下的类和资源。

当一个tomcat应用需要加载某个类时,默认会依次从以下路径中加载类:
  • Bootstrap classes of your JVM
  • /WEB-INF/classes of your web application
  • /WEB-INF/lib/*.jar of your web application
  • System class loader classes (described above)
  • Common class loader classes (described above)
一个tomcat应用中 WebAppClassLoader配置为 <Loader delegate="true"/> 时,会按以下顺序加载类:
  • Bootstrap classes of your JVM
  • System class loader classes (described above)
  • Common class loader classes (described above)
  • /WEB-INF/classes of your web application
  • /WEB-INF/lib/*.jar of your web application

欲查阅更多关于java类加载器资料,请参考:
http://blog.csdn.net/xiangbq/article/details/49678869
http://blog.csdn.net/zhoudaxia/article/details/35824249
http://tomcat.apache.org/tomcat-7.0-doc/class-loader-howto.html





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值