jvm笔记05:虚拟机类加载机制

JVM把class文件加载的内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型的过程就是加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。


类加载时机

有且只有以下五种情况必须立即对类进行”初始化”(称为对一个类进行主动引用):

1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法)。
2)使用java.lang.reflet包的方法对类进行反射调用的时候。
3)当初始化一个类的时候,如果发现其负类没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,虚拟机会初始化主类(包含main方法的那个类。

5)当使用JDK1.7的动态语言支持时候,如果一个java.lang.invoke.MethodHandle实例最后的解释结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要触发其初始化。


类加载过程

加载 
“加载”(Loading)阶段是“类加载”(Class Loading)过程的一个阶段,在加载阶段,虚拟机需要完成以下三件事情: 
1) 通过一个类的全限定名来获取定义此类的二进制字节流 
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 
3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

验证 
验证是连接阶段的第一步,这一阶段的目的是位了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 
1)文件格式验证 
2)元数据验证 
3)字节码验证 
4)符号引用验证

准备 
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中,


解析 
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 
1)类或接口的解析 
2)字段解析 
3)类方法解析 
4)接口方法解析

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


类加载器

类与类加载器

通过一个类的全限定名来获取描述此类的二进制流,执行这个动作的代码模块成为“类加载器”。
 
两个类只有在同一个类加载器加载的前提下才有意思,否则即使两个类原子相同的Class文件,只要加载它们的加载器不同,那这两个类也是不相等的。
 
这里的相等,包括equals,isAssignableFrom(),isInstance() instanceof等情况。

双亲委派模型

只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另一种是所有其他的类加载器,使用JAVA实现,独立于JVM,并且全部继承自抽象类java.lang.ClassLoader.
 
绝大部分JAVA程序都会使用到以下三种系统提供的类加载器:
启动类加载器(Bootstrap ClassLoader),负责将存放在<JAVA+HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中的,并且是JVM识别的(仅按照文件名识别,如rt.jar,如果名字不符合,即使放在lib目录中也不会被加载),加载到虚拟机内存中,启动类加载器无法被JAVA程序直接引用。
扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader),由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。


这张图表示类加载器的双亲委派模型(Parents Delegation model). 双亲委派模型要求除了顶层的启动加载类外,其余的类加载器都应当有自己的父类加载器。,这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。
 
 
双亲委派模型并不是一个强制性的模型,仅是推荐
 
双亲委派模型的工作过程是:当一个类加载器受到类加载请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给自己的父类加载器去完成,只有当父加载器表示自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
 
这个模型的好处,就是保证某个范围的类一定是被某个类加载器所加载的,这就保证在程序中同一个类不会被不同的类加载器加载。这样做的一个主要的考量,就是从安全层面上,杜绝通过使用和JRE相同的类名冒充现有JRE的类达到替换的攻击方式。

破坏双亲委派模型

到目前为止,有三次大规模的破坏双亲委派模型的情况
 
由于双亲委派模型是在JDK1.2后引入,而在JDK1.0时就存在java.lang.ClassLoader和类加载器。为了向前兼容,JDK1.2后的java.lang.ClassLoader添加了一个新的protected方法findClass(),再次之前,用户继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),该方法唯一逻辑就是调用自己的loadClass(). JDK1.2之后就不提倡重写loadClass()方法,而应当将自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就保证新写出来的类加载符合双亲委派规则的。(默认loadClass()方法会先调用父类加载,如果失败,再调用findClass()方法,如果在自己的类加载器实现中重写loadClass()方法,不调用父类加载,就会导致双亲委派模型失效).
 
 
第二次被破坏是由这个模型自身的缺陷造成的,如果基础类调用回用户的代码,比如JNDI服务,它本身由启动类加载器加载,但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者代码,但启动类加载器并不会认识这些代码,为了解决这个问题,引入了线程上下文类加载器(Thread Context ClassLoader)。这个类可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。这个设置,实际上就是让父类可以通过指定子加载器来帮助自己加载类。
 
第三次被破坏是由于用户对程序动态性的追求而导致的,比如HotSwap, HotDeployment
 
事实上,当前业界事实上的JAVA模块坏标准OSGi也破坏了双亲委派模型,每一个程序模块(bundle)都有一个自己的类加载器,当需要替换一个bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.文件格式验证 
 验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围之内等
2.元数据验证
这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。
3.字节码验证
 进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。
4.符号引用验证
符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值