java的classLoader原理理解和分析

相信大多数java程序员都知道classLoader的存在,但大家想过java为什么要设计java 类加载体系吗? java类加载体系的是如何工作的?

我带着这些问题,翻阅了相关资料,写下了本篇博客。


一, 为什么要设计类加载体系

1. 使用场景

我们先看看有哪些场景会使用到java 类加载 体系:

1, java applet

这个东西估计很多人都没用过,比较古老. 但我们看看它的原理介绍就明白它是干什么的了.

它的原理如下:

含有Applet的网页的HTML文件代码中部带有<applet> 和</applet>这样一对标记,当支持Java的网络浏览器遇到这对标记时,就小应用程序代码并在本地计算机上执行该Applet.

也就是说,把java下载到本地,让本地的jvm来执行这段代码。这里面就涉及到类动态加载的相关接口。


2, tomcat,jboss等web服务器

因为tomcat.jboss本身就是java程序编写的web服务器,那么他们在加载相应的war包程序时,也涉及到类动态加载的相关接口.


还有一些像osgi等也需要涉及到 类动态加载的接口.

这些场景都有下面2个特点:

1, 程序在运行期间才决定是否需要加载某类,而不是在程序启动时决定. 

2, 程序在运行期间可能会替换或者移除某些类.

java的类加载体系满足上面的所有的需求,但还有一些更重要的原因促使java必须要有自己的类加载体系, 安全.这个原因我们后面了解了它的原理后会有更深刻的体会.


二, 类加载体系是如何工作的

首先我们需要了解,java类加载体系是什么.

看下面的代码:

public class ClassLoaderExample {
    public static void main(String[] args) {
        ClassLoader a = ClassLoaderExample.class.getClassLoader();
        while(a != null){
            System.out.println(a.toString());
            a = a.getParent();
        }
    }
}
上面的代码输出:

sun.misc.Launcher$AppClassLoader@47415dbf
sun.misc.Launcher$ExtClassLoader@1471cb25

就是说:

ClassLoaderExample 这个类是AppClassLoader这个类加载器加载的,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器为null.


2.1 类加载方式

1,隐式加载   new A().
看下面的代码:
public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        B b = new B();
    }
}
class B {
    static {
        System.out.println("static B");
    }
}
编译:  javac A.java
运行:  java -verbose A  
ps: -verbose 参数是把类加载的相关信息打印出来.
上面的输出如下: 
.....
[Loaded A from file:/root/]
[Loaded B from file:/root/]
hello world.
.....  

输出中去掉了其他一些加载信息,只保留了我们最关心的几行.
上面三行中一二行是加载 A,B类的相关信息,第三行是打印的语句.
上面的例子中 B b = new B();触发了B这个类的加载.

2,显示加载    Class.forname("xxxx") 或者 xxx .class.getClassLoader().loadClass("xxxx");
看下面的代码:
public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        //B b = new B();
        Class.forName("B");
        System.out.println("hello world.");
    }
}
class B {
    static {
        System.out.println("static B");
    }
}
编译:  javac A.java
运行:  java -verbose A  
ps: -verbose 参数是把类加载的相关信息打印出来.
上面的输出如下: 
.....
[Loaded A from file:/root/]
[Loaded B from file:/root/]
static B
hello world.
.....  
上面的例子, Class.forname("B"),触发了B类的加载. 
从输出可以看到, B类的static区域被加载后立即执行了.

我们再看看另外一种显示加载类的方式: 
把class.forName换成了 classLoader的loadClass: 
public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        //B b = new B();
//        Class.forName("B");
        A.class.getClassLoader().loadClass("B");
        System.out.println("hello world.");
    }
}
class B {
    static {
        System.out.println("static B");
    }
}
编译运行,输出如下:
....
[Loaded A from file:/root/]
[Loaded B from file:/root/]
hello world.
.....
可以看到A.class.getClassLoader().loadClass("B");同样触发了B类的加载,但和Class.forName("B");不同的是static区域并没有被执行.

我们再看下面的代码:
public static void main(String[] args) throws SQLException, ClassNotFoundException {
//        B b = new B();
        classForName();
        System.out.println("hello world.");
    }

    public static void classForName() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        String url  = "xxxx";
        String pwd = "xxx";
        String userNmae = "xxx";
        DriverManager.getConnection(url,userNmae,pwd);
    }
com.mysql.jdbc.Driver
这段代码很熟悉吧. 刚学编程时,基本上所有的同学都会接触这段代码.这句Class.forName("com.mysql.jdbc.Driver"); 到底是干什么的?
我们来看看com.mysql.jdbc.Driver的实现:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }

非常简单,只有一个无参构造函数 和 static区域. 
Class.forName的作用就是告诉JVM把com.mysql.jdbc.Driver这个类加载进来,并注册到DriverManger上,以便后面的DriverManager.getConnection(url,userNmae,pwd)使用.

2.2 委派模式

现在我们知道了jvm加载类的几种方式,加载的实现是什么呢? 相信大家都听说过,叫 双亲委派 或 代理模式.
我们直接看classLoader的实现源码里,加载类的方式(ClassLoader.loadClass(String name,boolean resolve)方法):
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    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;
    }
}
重要的代码是这段:
if (parent != null) {
        c = parent.loadClass(name, false);
} else {
        c = findBootstrapClassOrNull(name);
}
就是说如果, parent不为空,把类的加载一直委派给parent加载,如果父亲为空,则在 c = findBootstrapClassOrNull(name); 
如果依然找不到,则:
c = findClass(name); 由自己的类加载器加载.
他们的委派关系总结下如图:


程序员编写的程序首先由appclassLoader加载,appclassLoader委派给它的父loader extcalssLoader,extclassLoader再在bootstrapClasssLoader里找,找不到则依次下沉返回,找到为止。
这样的委派模式有什么好处?
安全! 试想用户自己写了一个java.lang.String的类,如果没有委派模式,jvm加载了这个类,那么其它类中所有引用String的类的地方都从jre自带的java.lang.String被替换成了用户的java.lang.String类。那么很多功能和性能可能都不如jre自带的String.而且会造成严重的安全问题。

注意:ExtClassLoader 到 bootstrapClasssLoader之间是条虚线,也就是说,ExtClassLoader 的父加载器并不是bootstrapClasssLoader,只是在extcalssLoader里找不到时,去bootstrapClasssLoader里找。
从上面classLoader的loadclass方法里用的是 
if (parent != null) {
xxxx
}else{
xxxx
} 可以看出来.


2.3 类加载时相关的异常

相信大多数人都遇到过下面这2个异常:
1).   classNotFoundException 
看下面的代码: 
public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("C");
    }
}
class B {
    static {
        System.out.println("static B");
    }
}
编译运行上面的代码: 
Exception in thread "main" java.lang.ClassNotFoundException: C
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:171)
        at A.main(A.java:9)
在执行上面的委派模式后,appclassLoader在classpath中并没有找到C这个类,直接抛出ClassNotFoundException异常.
2).  NoClassDefFoundError  
看下面的代码:
public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        B b = new B();
    }
}
class B{
    C c = new C();
    static{
        System.out.println("static B");
    }
}
class C{}

编译:  javac A.java
运行:  java  A  
输出:  static B 
如果这时,我们把C.class删掉.
再次运行: java A,输出如下:
static B
Exception in thread "main" java.lang.NoClassDefFoundError: C
        at B.<init>(A.java:14)
        at A.main(A.java:9)
Caused by: java.lang.ClassNotFoundException: C
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
        ... 2 more
出现了 NoClassDefFoundError错误。
注意: 我们后面的代码从 Class.forName("B") 改成了 B b = new B(),如果用Class.forName("B"),并不会抛出错误,因为static区域并没有触发加载C的动作,new B() 触发 new C(),new C()触发C的加载,出现了C的 NoClassDefFoundError错误.

下次再出现这2种错误了,我们应该知道它的原理了。

一,  总结

经过上面的讨论,总结如下:

1, 类的加载是双亲委派模式.

2,每个类加载器只加载一次同一个类.

3,类的加载是延迟的,按需加载的.

4,Class.forName 和 classloader.loadclass,new 都可以触发类的加载.  new 是强依赖,Class.forName比较灵活,事先并不知道要加载的类是谁.




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值