上一篇博文Java类的生命周期概要性地介绍了一个类从“出生”到“凋亡”的各个生命阶段。今天,笔者就跟你们聊聊其中很是重要的一个环节:类的加载过程。Java虚拟机中类加载的过程具体包含加载、验证、准备、解析和初始化这5个阶段,各阶段涉及的细节较多,在上一篇博文中都有简短介绍,本文将主要介绍加载阶段,其中包括加载方式、加载时机、类加载器原理、实例分析等内容。java
在具体介绍类加载机制以前,咱们先来看下网上关于理解类加载机制的经典题目:spring
public class Singleton { private static Singleton singleton = new Singleton(); public static int counter1; public static int counter2 = 0; private Singleton() { counter1++; counter2++; } public static Singleton getSingleton() { return singleton; } } // 打印静态属性的值 public class TestSingleton{ public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); System.out.println("counter1=" + singleton.counter1); System.out.println("counter2=" + singleton.counter2); } } // 输出结果: >>> counter1=1 >>> counter2=0
关于为何counter2=0
,这里就不具体解释了,只是想说下它考核了那几个点:编程
- 类加载过程的5个阶段前后顺序:准备阶段在初始化以前
- 准备阶段和初始化阶段各自作的事情
- 静态初始化的细节:前后顺序
言归正传,咱们先从类加载的定义提及,一句话概述,缓存
虚拟机将class文件中的二进制数据流加载到JVM运行时数据区的方法区内,并进行验证、准备、解析和初始化等动做后,在内存中建立java.lang.class对象,做为对方法区中该类数据结构的访问入口。tomcat
这里有几点要解释下,class文件是指符合class文件格式的二进制数据流,也就是咱们常说的字节码文件,它是咱们与JVM约定的格式协议,只要是符合class文件格式的二进制流,均可被JVM加载,这也是JVM跨平台的基础;另外,java.lang.class对象只是说在内存建立,并无明确规定是否在Java堆中,对于Hotspot虚拟机,是存放在方法区的。安全
类的加载方式分为两种:隐式加载和显式加载。服务器
实际就是不用咱们代码主动声明,而是JVM在适当的时机自动加载类。好比主动引用某个类时,会自动触发类加载和初始化阶段。数据结构
则一般是指经过代码的方式显式加载指定类,常见如下几种:架构
经过
Class.forName()
加载指定类。对于forName(String className)
方法,默认会执行静态初始化,但若是使用另外一个重载函数forName(String name, boolean initialize, ClassLoader loader)
,其实是能够经过initialize
来控制是否执行静态初始化并发经过
ClassLoader.loadClass()
加载指定类,这种方式仅仅是将.class加载到JVM,并不会执行静态初始化块,这个等后面谈到类加载器的职责时会再强调这一点
关于Class.forName()
是否执行静态初始化,经过源码就能一目了然:
public static Class<?> forName(String className) // 执行初始化,由于initialize为true throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } ... public static Class<?> forName(String name, boolean initialize, ClassLoader loader) // 可控的,经过initialize来指定初始化与否 throws ClassNotFoundException { ... return forName0(name, initialize, loader, caller); }
类加载的第一个阶段——加载阶段具体何时开始,虚拟机规范并未指明,由具体的虚拟机实现决定,可分为预加载和运行时加载两种时机:
JAVA_HOME/lib
下的rt.jar,它包含了咱们最经常使用的class,如java.lang.*
、java.util.*
等,在虚拟机启动时会提早加载,这样用到时就省去了加载耗时,能加快访问速度。先上代码(JDK1.7源码):
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; } }
loadClass
是类加载机制中最核心的代码,这段代码基本阐述了如下最核心的两点:
findLoadedClass(name)
,首先第一步就检查这个name
表示的类是否已被某个类加载器实例加载过,若已加载,则直接返回已加载的c
,不然才继续下面的委派等逻辑。即JVM层会对已加载的类进行缓存,那具体是怎么缓存的的呢?在JVM实现中,有个相似于HashTable
的数据结构叫作SystemDictionary,对已加载的类信息进行缓存,缓存的key
是类加载器实例+类的全限定名,value
则是指向表示类信息的klass数据结构。这也是为何咱们常说,JVM中加载类的类加载器实例加上类的全限定名,这二者一块儿才能惟一肯定一个类。每一个类加载器实例就至关因而一个独立的类命名空间,对于两个不一样类加载器实例加载的类,即使名称相同,也是两个彻底不一样的类。
对于新加载的类,缓存没命中后走双亲委派逻辑——当parent
存在时,会先委派给parent
进行loadClass
,而后parent.loadClass
内部又会进行一样的向上委派,直至parent
为null
,委派给根加载器。也就是说委派请求会一直向上传递,直到顶层的引导类加载器,而后再统一用ClassNotFoundException异常的方式逐层向下回传,直到某一层classLoader
在其搜索范围内找到并加载该类;当parent
不存在时,即没有父类加载器,此时直接委派给顶层加载器——BootstrapClassLoader
。
从这里能够看到双亲委派结构中,类加载器之间的父子层级关系并非经过继承来实现,而是经过组合的方式即子类加载器持有parent
代理以指向父类加载器来实现的。
ucp
属性了解下~仍是给个定义吧:
经过一个类的全限定名来获取描述此类的二进制字节流的代码模块
经典的三层加载器结构:
一、启动类加载器(或称为引导类加载器):只负责加载<JAVA_HOME>/lib
目录中的,或是启动参数-Xbootclasspath
所指定路径中的特定名称类库。该加载器由C++实现,对Java程序不可见,对于自定义加载器,如果未指定parent,则会委派该加载器进行加载。
二、扩展类加载器:负责加载<JAVA_HOME>/lib/ext
目录中的,或是java.ext.dirs
系统变量所指定的路径下全部类库。该加载器由sum.misc.Launcher$ExtClassLoader
实现,可直接使用。
三、应用程序类加载器(或称为系统类加载器):负责加载用户类路径ClassPath
中全部类库。该加载器由sum.misc.Launcher$AppClassLoader
实现,可由ClassLoader.getSystemClassLoader()
方法得到。
ExtClassLoader
和AppClassLoader
都是继承自URLClassLoader
,各自负责的加载路径都是保存在ucp
属性中,这个看源码就能得知。
双亲委派并非一个强制性约束模型,毕竟它自身也有局限性。不管是历史代码层面、SPI设计问题、仍是新的热部署需求,都不可避免地会违背该原则,累计有三次“破坏”。
经过ClassLoader的源码可知,双亲委派的实现细节都在loadClass方法中,而该方法是一个protected的,意味着子类能够覆盖该方法,从而可绕过双亲委派逻辑。双亲委派模型是在JDK1.2以后才被引入,在此以前的JDK1.0,已有部分用户经过继承ClassLoader重写了loadClass逻辑,这使得后面引入的双亲委派逻辑在这些用户程序中不起做用。
为了向前兼容,ClassLoader新增了findClass方法,提倡用户将本身的类加载逻辑放入findClass中,而不要再去覆盖loadClass方法。
双亲委派的层次优先级就决定了用户代码和JDK基础类之间的不对等性,即只能用户代码调用基础类,反之不行。对于SPI之类的设计,好比已经成为Java标准服务的JNDI,其接口代码是在基础类中,而具体的实现代码则是在用户Classpath下,在双亲委派的限制下,JNDI没法调用实现层代码。
开个后门——引入线程上下文类加载器(Thread Context ClassLoader),该加载器可经过java.lang.Thread.setContextClassLoader()
进行设置,若建立线程时未设置,则从父线程继承;若应用程序的全局范围都未设置过,则默认设置为应用程序类加载器,这个可在Launcher的源码中找到答案。
有了这个,JNDI服务就可以使用该加载器去加载所需的SPI代码。其余相似的SPI设计也是这种方式,如JDBC、JCE、JAXB、JBI等。
模块化热部署,在生产环境中显得尤其有吸引力,就像咱们的计算机外设同样,不用重启,可随时更换鼠标、U盘等。 OSGi已经成为业界事实上的Java模块化标准,此时类加载器再也不是双亲委派中的树状层次,而是复杂的网状结构。
一般Web服务器须要解决几个基本问题:
为了应对以上基本问题,主流的Java Web服务器都会提供多个Classpath存放类库。对于Tomcat,其目录结构划分为如下4组:
/common
目录,存放的类库被Tomcat和全部的Web应用程序共享。/server
目录,仅被Tomcat使用,其余Web应用程序不可见。/shared
目录,可被全部Web应用程序共享,但对Tomcat不可见。/WebApp/WEB-INF
目录,仅被所属的Web应用程序使用,对Tomcat和其余Web应用程序不可见。跟以上目录对应的,是Tomcat经典的双亲委派类加载器架构:
上图中,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader分别负责/common/*
、/server/*
、/shared/*
和/WebApp/WEB-INF/*
目录下的Java类库加载,其中WebApp类加载器和Jsp类加载器一般会存在多个实例,每个Web应用程序对应一个WebAppClassLoader,每个JSP文件对应一个Jsp类加载器。
OSGi(Open Service Gateway Initiative)是OSGi联盟制定的一个基于Java语言的动态模块化规范,其最著名的应用案例就是Eclipse IDE,它是Eclipse强大插件体系的基础。
OSGi中的每一个模块称为Bundle,一个Bundle能够声明它所依赖的Java Package(经过Import-Package描述),也能够声明它容许导出发布的Java Package(经过Export-Package描述)。Bundle之间的依赖关系为平级依赖,Bundle类加载器之间只有规则,没有固定的委派关系。假设存在BundleA、BundleB和BundleC,
BundleA:声明发布了packageA,依赖了java.*的包 BundleB:声明依赖了packageA和packageC,同时也依赖了java.*的包 BundleC:声明发布了packageC,依赖了packageA
一个简单的OSGi类加载器架构示例以下:
上图的这种网状架构带来了更好的灵活性,但同时也可能产生许多新的隐患。好比Bundle之间的循环依赖,在高并发场景下致使加载死锁。
本文以一个关于类加载的编程题为切入点,阐述了类加载阶段的具体细节,包括加载方式、加载时机、加载原理,以及双亲委派的优劣点。并以具体的类加载器实例Tomcat和OSGi为例,简单分析了类加载器在实践过程当中的多种选择。
同步更新到原文。