前言
本次带来JVM的另一块重要内容,类加载机制,不废话,直接开怼。
正文
、类加载的过程。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析个部分统称为连接。
)加载
“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下件事情:
通过一个类的全限定名来获取定义此类的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2)验证
连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
)准备
该阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里所说的初始值“通常情况”下是数据类型的零值,下表列出了Java中所有基本数据类型的零值。
4)解析
该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
5)初始化
到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
我们也可以从另外一种更直接的形式来表达:初始化阶段是执行类构造器clinit()方法的过程。clinit()不是程序员在Java代码中直接编写的方法,而是由Javac编译器自动生成的。
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
我之前还写过一篇关于初始化的面试题:一道有意思的“初始化”面试题,有兴趣的同学可以看一看。
2、Java虚拟机中有哪些类加载器?
从Java虚拟机的角度来讲,只存在两种不同的类加载器:
一种是启动类加载器(BootstrapClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,绝大部分Java程序都会使用到以下种系统提供的类加载器。
)启动类加载器(BootstrapClassLoader):
这个类加载器负责将存放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
2)扩展类加载器(ExtensionClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
)应用程序类加载器(ApplicationClassLoader):
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图所示。
、什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
类加载的源码如下:
protectedClass?loadClass(Stringname,booleansolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){//、检查请求的类是否已经被加载过了Class?c=findLoadedClass(name);if(c==null){longt0=System.nanoTime();try{//2、将类加载请求先委托给父类加载器if(pant!=null){//父类加载器不为空时,委托给父类加载进行加载c=pant.loadClass(name,false);}else{//父类加载器为空,则代表当前是Bootstrap,从Bootstrap中加载类c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){//如果父类加载器抛出ClassNotFoundException//说明父类加载器无法完成加载请求}if(c==null){//、在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载longt=System.nanoTime();c=findClass(name);//thisisthedefiningclassloader;cordthestatssun.misc.PerfCounter.getPantDelegationTime().addTime(t-t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t);sun.misc.PerfCounter.getFindClasses().incment();}}if(solve){solveClass(c);}turnc;}}
4、为什么使用双亲委派模式?
)使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
2)如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
5、有哪些场景破坏了双亲委派模型?
目前比较常见的场景主要有:
)线程上下文类加载器,典型的:JDBC使用线程上下文类加载器加载Driver实现类
2)Tomcat的多Web应用程序
)OSGI实现模块化热部署
6、为什么要破坏双亲委派模型?
原因其实很简单,就是使用双亲委派模型无法满足需求了,因此只能破坏它,这边以面试常问的Tomcat为例。
我们知道Tomcat容器可以同时部署多个Web应用程序,多个Web应用程序很容易存在依赖同一个jar包,但是版本不一样的情况。例如应用和应用2都依赖了spring,应用使用的.2.*版本,而应用2使用的是4..*版本。
如果遵循双亲委派模型,这个时候使用哪个版本了?
其实使用哪个版本都不行,很容易出现兼容性问题。因此,Tomcat只能选择破坏双亲委派模型。
7、如何破坏双亲委派模型?
破坏双亲委派模型的思路都比较类似,这边以面试中常问到的Tomcat为例。
其实原理非常简单,我们可以看到上面的类加载方法源码(loadClass)的方法修饰符是protected,因此我们只需以下几步就能破坏双亲委派模型。
)继承ClassLoader,Tomcat中的WebappClassLoader继承ClassLoader的子类URLClassLoader。
2)重写loadClass方法,实现自己的逻辑,不要每次都先委托给父类加载,例如可以先在本地加载,这样就破坏了双亲委派模型了。
8、Tomcat的类加载器?
Tomcat的类加载器如下图所示:
)BootstrapClassLoader:可以看到上图中缺少了ExtensionClassLoader,在Tomcat中ExtensionClassLoader被集成到了BootstrapClassLoader里面。
2)SystemClassLoader就是ApplicationClassLoader:Tomcat中的系统类加载器不会加载CLASSPATH环境变量的内容,而是从以下资源库构建System类加载器。
$CATALINA_HOME/bin/bootstrap.jar,包含用于初始化Tomcat服务器的main()方法,以及它所依赖的类加载器实现类。$CATALINA_BASE/bin/tomcat-juli.jar或$CATALINA_HOME/bin/tomcat-juli.jar,日志实现类。如果$CATALINA_BASE/bin中存在tomcat-juli.jar,则使用它来代替$CATALINA_HOME/bin中的那个。$CATALINA_HOME/bin/