博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java的classLoader原理理解和分析
阅读量:4180 次
发布时间:2019-05-26

本文共 8187 字,大约阅读时间需要 27 分钟。

相信大多数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比较灵活,事先并不知道要加载的类是谁.

你可能感兴趣的文章
动态规划的套路----左神
查看>>
KMP算法简解
查看>>
左神算法课进阶版总结
查看>>
左神算法基础班总结
查看>>
Linux性能优化
查看>>
进程间的通信---UNIX高级环境编程
查看>>
基于SSH开发的城市公交管理系统 JAVA MySQL
查看>>
基于SSH开发的勤工助学管理系统 JAVA MySQL
查看>>
基于SSH开发的宠物销售商城系统 JAVA MySQL
查看>>
基于springboot的宠物领养管理系统 java
查看>>
JAVA 洗衣房管理系统 宿舍洗衣机管理系统
查看>>
基于SSM的街道办安全管理系统 JAVA
查看>>
基于SSM的论文选题管理系统 JAVA
查看>>
生成器模式
查看>>
工厂方法模式
查看>>
阿里规范(一)关于CountDownLatch和ThreadLocalRandom的详解(带测试代码)
查看>>
Mysql 函数 STR_TO_DATE
查看>>
Commons CLI 使用介绍
查看>>
Mybatis 缓存实现原理——案例实践
查看>>
Mybatis 缓存实现原理
查看>>