《深入理解Java虚拟机》读书笔记----虚拟机类加载机制

类的生命周期

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历七个阶段

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序如下图。

图中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类加载过程必须按照这种顺序。但是解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始(为了支持Java语言的动态绑定特性)。

关于在什么时候开始第一个阶段加载,Java虚拟机规范并没有强制约束,但是对于初始化这个阶段严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之 前开始):

1、实例化对象调用静态方法、静态变量

2、使用反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。

3、初始化类的时候父类未初始化,需要先触发父类初始化

4、当虚拟机启动时,初始化主类(包含main()方法的类)

5、使用JDK 7新加入的动态语言支持时如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

6、当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载的过程

加载

“加载”阶段是整个“类加载”过程中的一个阶段,二者不能混淆。

在加载阶段Java虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流。

2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段大致上会完成下面四个阶段的检验动作:

1、文件格式验证

2、元数据验证

3、字节码验证

4、符号引用验证

准备

准备阶段是正式为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段。

解析

1、解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

2、问:解析阶段中所说的直接引用符号引用有什么关系?

答:

1、符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

2、符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。直接引用是和虚拟机实现的内存布局直接相关的,如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

3、解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7 类符号引用进行。

初始化

在准备阶段,变量已经赋过一次系统要求的初始值(零值);而在初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,也可以一句话概括,初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法时由编译器自动收集类中的所有类变量的赋值动作静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

<clinit>()方法与类的构造函数不同,不需要显式调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前执行完父类的<clinit>()方法。

与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。

<clinit>()方法对于类或者接口不是必须的,如果类中没有静态语句块,也没有对类变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。

这是类的初始化,不是类实例的初始化,因此类的构造函数是不会在这里调用的。

类加载器

类和类加载器的关系

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间。通俗点说就是,判断两个类是不是相等,前提是这两个类是一个类加载器加载出来的。

双亲委派模型

JVM 中内置了三个重要的 ClassLoader,启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分,其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

  • 启动类加载器(Bootstrap Class Loader): 这个类加载器负责加载存放在 \lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
  • 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载 \lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  • 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。

双亲委派模型: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

问:为什么要使用双亲委派模型来组织类加载器之间的关系?

答:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类并放在程序的 ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

破坏双亲委派模型

双亲委派模型不是一个具有强制性约束的模型,只是Java设计者推荐的类加载器实现方式。大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模被破坏的情况。

1、第一次破坏:向前兼容

JDK1.2发布之前,兼容之前的代码。

2、第二次破坏:加载SPI接口实现类

第二次被破坏是因为双亲委派模型存在缺陷。双亲委派模型虽然解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但如果基础类调用会用户的代码,情况就不同了。例如,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等。

3、第三次破坏:热部署

双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的。为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

如果我们自己想定义一个类加载器,破坏双亲委派模型,只需要重写重写其中的loadClass方法,使其不进行双亲委派即可。

Tomcat类加载器架构

Tomcat是主流的Java Web服务器之一,为了实现一些特殊的功能需求,自定义了一些类加载器。

Tomcat类加载器如下:

Tomcat实际上也是破坏了双亲委派模型的。

Tomact是web容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。

所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。

Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值