Java虚拟机之类装载子系统

本文探讨Java类加载的五个阶段:加载、验证、准备、解析和初始化,重点讲解了类加载器、双亲委派模型及其在Tomcat中的突破。了解如何确保类的唯一性与沙箱安全,以及Tomcat如何解决多版本类库隔离和热部署问题。
摘要由CSDN通过智能技术生成

类加载机制

我们知道我们写的 java 程序,经过编译后会生成 .class文件,而Java虚拟机的类加载机制指的就是:通过类加载器把.class文件加载到内存(运行时数据区),并对数据进行效验、解析、初始化等操作,最终形成可以被虚拟机直接使用的类型。

java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先把主类加载到JVM。 主类在运行过程中如果使用到其它类,会逐步加载这些类。jar 包里的类不是一次性全部加载的,是使用到时才加载(按需加载)。

这个类加载的过程是由“类装载子系统”完成的,“类装载子系统”是 Java 虚拟机的一部分。

类加载主要包含5个阶段:

加载 => 验证 => 准备 => 解析=> 初始化

加载

“加载” 是 “类加载” 的第一个阶段。

预加载

虚拟机启动时加载,加载的是 JAVA_HOME/lib/ 下的 rt.jar 下的 .class文件,这个jar包里面的内容是程序运行时经常常用到的,像 java.lang.*、java.util.*、java.io.* 等等,因此随着虚拟机一起加载。

可以通过设置虚拟机参数 -XX:+TraceClassLoading 来验证

运行时加载

虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。

在加载阶段,虚拟机需要完成三件事

  • 通过一个类的【全限定名】获取定义此类的【二进制字节流】
  • 将这个二进制字节流所代表的【静态存储结构】转化为方法区的【运行时数据结构】
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中这个类各种数据的访问入口

验证

验证阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

验证阶段包含4个过程: 

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
  2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
  3. 字节码验证:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
  4. 符号引用验证:对类自身以外的信息进行匹配性验证

准备

准备阶段是正式为加载的类的类变量(静态变量)分配内存并设置初始值的过程,这些变量所使用的内存都将在方法区中分配。

数据类型 初始值
byte0
short0

int

0
long0L
float0.0f
double0.0d
char‘\u0000’
booleanfalse
引用类型变量null

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用

  • 符号是对于变量方法的描述。

  • 符号引用和虚拟机的内存布局没有关系,引用的目标未必已经加载到内存中了。

  • 各种虚拟机实现的内存布局各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用

  • 直接引用可以是直接指向目标的指针地址,相对偏移量或是能间接定位到目标的句柄。

  • 直接引用与虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译的直接引用一般不会相同。

  • 如果有了直接引用,那引用的目标必定是已经在内存中存在的

初始化

类初始阶段是类加载过程中最后一步,前面的类加载过程中,除了在加载阶段用户程序应用可以通过自定义类加载器参与之外,其余动作都是由虚拟机主导和控制的。到了初始化阶段才开始真正执行类中定义的Java程序代码。

准备阶段的初始化和初始化阶段的初始化的区别

  • 准备阶段的初始化是仅仅是为类的静态变量分配内存空间,并且将内存清零,即赋予初始值

  • 初始化阶段的初始化则是根据程序员的要求去初始化

类构造器<clinit>()方法

  • 类构造器<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的
  • 类构造器<clinit>()与类的构造函数(实例构造器<init>())不同,类构造器不需要显式的调用父类构造器,虚拟机会保证在子类的类构造器调用之前,父类的类构造器已经执行完毕。
  • 类构造器<clinit>()对于类或接口来说并不是必需的,如果一个类中没有静态代码块和对类变量的赋值操作,那么编译器可以不为这个类生成类构造器<clinit>()方法)。
  • 因为接口不能使用静态代码块,但任然有类变量的赋值操作,所以接口也有类构造器。但跟类不同的是,接口的类构造器不需要优先执行其父接口的构造器。只有当父接口中定义的变量被使用时,父接口才会开始初始化,调用父接口构造器。
  • 虚拟机会保证一个类的<clinit>()方法在多线程中能被正确的加锁,同步。

类加载器

类加载器定义

类加载器就是在类加载过程的加载阶段中完成获取某类的二进制字节流的一个东西(代码模块),我们通过这个东西去获取类的字节码信息,并存放到方法区和生成该类的Class对象。

它是在Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。

显式加载和隐式加载

  • 显式加载:Class.forName 或 ClassLoader.loadClass 去主动加载某个类

  • 隐式加载:如 new 了某个类,会隐式的触发该类的加载

类加载器类型

类型描述

启动类加载器

Bootstrap classloader

由C++语言实现,是Java虚拟机本身的部分,并不继承自java.lang.ClassLoader类

用来加载Java核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下)的内容

用来加载扩展类加载器,应用程序类加载器,并指定他们的父类加载器

扩展类加载器

Extensions classloader

由 sun.misc.Laucher$ExtClassLoader 实现,继承自 java.lang.ClassLoader

用来加载Java扩展库(JAVA_HOME/jre/ext/*.jar 或 java.ext.dirs路径下)的所有类库

应用程序类加载器Application classloader

sun.misc.Launcher$AppClassLoader

负责加载classpath路径下的类

自定义类加载器开发人员可以通过继承java.lang.ClassLoader类并重新它的loadClass方法的方式实现自己的类加载器,用于满足一些特殊的需求

双亲委派机制

双亲委派模型的工作流程

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给自己的父类加载器去完成
  2. 每一层的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中
  3. 只有父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到相应的类),子加载器才会一层层往下尝试让子加载器去加载

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器

类加载器之间的父子关系一般不会以继承的关系去实现,而是使用组合的方式来复用父加载器的代码

不一定所有的类加载器都需要实现双亲委派模型,因为这不是一个强制性的约束模型,只是Java设计者推荐的

双亲委派模型的好处

  1. 避免重复加载类

    当父加载器已经加载了该类时,就没有必要子加载器再加载一次了。保证被加载类的唯一性。

  2. 沙箱安全机制

    比如自己写一个 java.lang.String 类,是不会被加载的。防止了核心 API 遭到篡改!

双亲委派模型在一定程度上保证了Java程序的稳定性和安全性。

打破双亲委派机制

Tomcat 的实现中就打破了双亲委派机制。

为什么tomcat需要打破双亲委派机制

Tomcat是个 web 容器, 它需要解决以下问题:

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类 库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
  3. web容器(Tomcat)也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改 jsp 已经是很正常的事情, web容器需要支持 jsp 修改后不用重启。

Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行

  1. 第一个问题:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
  2. 第二个问题:默认的类加载器是能够实现的,因为他的职责就是保证唯一性。 
  3. 第三个问题:和第一个问题一样。 
  4. 第四个问题:我们想我们要怎么实现jsp文件的热加载,jsp文件其实也就是 .class 文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后 的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

Tomcat自定义类加载器

Tomcat委派关系:

tomcat的几个主要类加载器:

  1. CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问 
  2. CatalinaClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
  3. SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见
  4. WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对 前Webapp可见
  5. JspClassLoader:仅用于加载jsp文件,每个jsp文件对应一个jsp类加载器

从图中的委派关系中可以看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用。
  • CatalinaClassLoader和SharedClassLoader自己能加载的类与对方相互隔离。 
  • WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。 
  • JapClassLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件。当Web容器检测到JSP文件被修改时,会替换掉目前的JapClassLoader实例,并通过再建立一个新的JapClassLoader来实现JSP文件的热加载功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值