深刻理解jvm类加载机制

本文将以四个问题展开:java

  1. 什么是类加载?
  2. 什么是双亲委任模型?
  3. 如何破坏双亲委任模型?
  4. Tomcat 的类加载器是怎么设计的?

1.什么是类加载?

类加载机制一个很大的体系,包括类加载的时机,类加载器,类加载时机。web

1.1 类加载过程


加载器加载到jvm中,接下来其实又分了好几个步骤缓存

  • 加载,查找并加载类的二进制数据,在Java堆中也建立一个java.lang.Class类的对象
  • 链接,链接又包含三块内容:验证、准备、初始化。

 1)验证,文件格式、元数据、字节码、符号引用验证;
 2)准备,为类的静态变量分配内存,并将其初始化为默认值;
 3)解析,把类中的符号引用转换为直接引用tomcat

  • 初始化,为类的静态变量赋予正确的初始值。

1.2 类加载时机

如今咱们例子中生成的两个.class文件都会直接被加载到JVM中吗??安全

虚拟机规范则是严格规定了有且只有5种状况必须当即对类进行“初始化”(class文件加载到JVM中):服务器

  • 建立类的实例(new 的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法
  • 反射的方式
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类(包含main方法的那个类)
  • 当使用JDK1.7的动态语言支持时(....)

因此说:markdown

  • Java类的加载是动态的,它并不会一次性将全部类所有加载后再运行,而是保证程序运行的基础类(像是基类)彻底加载到jvm中,至于其余类,则在须要的时候才加载。这固然就是为了节省内存开销

1.3 类加载器

                     

各个加载器的工做责任:app

  • 1)Bootstrap ClassLoader:负责加载$JAVA_HOME中jre/lib/rt.jar里全部的class,由C++实现,不是ClassLoader子类
  • 2)Extension ClassLoader:负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包
  • 3)App ClassLoader:负责记载classpath中指定的jar包及目录中class

工做过程:jvm

  • 一、当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 二、当ExtClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 三、若是BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 四、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  • 五、若是AppClassLoader也加载失败,则会报出异常ClassNotFoundException

2.什么是双亲委任模型

1.3的回答其实这就是所谓的双亲委派模型。简单来讲:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上jsp

好处:

  • 防止内存中出现多份一样的字节码(安全性角度)

特别说明:

  • 类加载器在成功加载某个类以后,会把获得的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加

3. 如何破坏双亲委任模型?

第一种:引入线程上下文类加载器

咱们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之因此称为“基础”,是由于它们老是做为被用户代码调用的API, 但没有绝对,若是基础类调用会用户的代码怎么办呢? 这不是没有可能的。

一个典型的例子就是JNDI服务,JNDI如今已是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它须要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。由于这些类不在rt.jar中,可是启动类加载器又须要加载。怎么办呢?  

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoader方法进行设置。若是建立线程时还未设置,它将会从父线程中继承一个,若是在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即便应用程序类加载器。

有了线程上下文加载器,JNDI服务使用这个线程上下文加载器去加载所须要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动做,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的通常性原则。但这迫不得已,Java中全部涉及SPI的加载动做基本胜都采用这种方式。例如JNDI,JDBC,JCE,JAXB,JBI等。 

第二种:自定义类加载器

自定义类加载器,而且重写ClassLoader类的loadClass()

扩展:Tomcat 的类加载器是怎么设计的?

首先,咱们来问个问题: Tomcat 若是使用默认的类加载机制行不行? 咱们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  1.  一个web容器可能须要部署两个应用程序,不一样的应用程序可能会依赖同一个第三方类库的不一样版本,不能要求同一个类库在同一个服务器只有一份,所以要保证每一个应用程序的类库都是独立的,保证相互隔离。
  2.  部署在同一个web容器中相同的类库相同的版本能够共享。不然,若是服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。
  3.  web容器也有本身依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。  
  4. web容器要支持jsp的修改,咱们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已是司空见惯的事情,不然要你何用? 因此,web容器须要支持 jsp 修改后不用重启。 

再看看咱们的问题:Tomcat 若是使用默认的类加载机制行不行? 答案是不行的。为何?咱们看,第一个问题,若是使用默认的类加载器机制,那么是没法加载两个相同类库的不一样版本的,默认的累加器是无论你是什么版本的,只在意你的全限定类名,而且只有一份。第二个问题,默认的类加载器是可以实现的,由于他的职责就是保证惟一性。第三个问题和第一个问题同样。咱们再看第四个问题,咱们想咱们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么若是修改了,但类名仍是同样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会从新加载的。那么怎么办呢?咱们能够直接卸载掉这jsp文件的类加载器,因此你应该想到了,每一个jsp文件对应一个惟一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。从新建立类加载器,从新加载jsp文件。 Tomcat 如何实现本身独特的类加载机制? 因此,Tomcat 是怎么实现的呢?牛逼的Tomcat团队已经设计好了。咱们看看他们的设计图:

                  

咱们看到,前面3个类加载和默认的一致,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebappClassLoader则是Tomcat本身定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6以后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。

其中WebApp类加载器和Jsp类加载器一般会存在多个实例,每个Web应用程序对应一个WebApp类加载器,

commonLoader:Tomcat最基本的类加载器,加载路径中的class能够被Tomcat容器自己以及各个Webapp访问; 

catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见; 

sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于全部Webapp可见,可是对于Tomcat容器不可见;

WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见

JasperLoader:每个JSP文件对应一个Jsp类加载器

相关文章
相关标签/搜索