_StriveG Blog

jvm类加载机制

前言

深入理解java虚拟机

类加载机制是jvm中,非常重要的一部分,也是后面可能说道的分包、插件化、热修复等技术的基础。

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。

类加载机制的时机

类加载机制的整个生命周期如下:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

其中,卸载的条件比较难达到,在方法区gc的时候提到过。

虚拟机规范中,严格规定了有且只有5种情况必须对类进行”初始化”(而加载、验证、准备、初始化需要在此之前)

  1. 遇到new、getstatic、putstatic或者invokestatic这四条字节码指令时,如果没有进行过初始化,则要先出发初始化,对应的操作为new对象、读取或设置static字段(被final修饰或编译器把结果放入到常量池的静态字段),以及屌用一个类的静态方法的时候,PS:因为被final修饰的字段,在编译期会被优化,放入到类的常量池中。
  2. 使用java.lang.reflect包的方法对类进行反射调用
  3. 当初始化一个类时,如果其父类没有进行过初始化,则先初始化父类
  4. 当虚拟机启动时,需要指定一个执行的主类(入口),虚拟机会先初始化这个类
  5. 使用动态语言支持时,动态代理,遇到REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄

上面的几种为主动引用,而被动引用就不会触发初始化,被动引用的例子如:

  • 子类引用父类的静态字段
  • 通过数组定义引用类
  • 常量在编译时会被放入常量池,本质上并没有引用到类

加载阶段

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

  1. 通过类的全限定名来获取定义此类的二进制流(全限定名,用/替换.)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
  3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口

加载二进制流的手段如下:

  • 从zip包中获取,如jar
  • 从网络中获取,Applet
  • 运行时计算生成,动态代理
  • 由其他文件生成,jsp
  • 从数据库中读取,较少

对我们Android程序员来说,最常见的就是zip包和动态代理这种。

对于数组类,不一样了,数组类由不通过类加载器创建,而由虚拟机直接创建,

数组类的加载过程遵循以下规则:

  1. 如果数组的组件类型(每一个值)是引用类型,则递归采用类的家在过程去加载
  2. 如果不是引用类型,而是int[]中,将数组标记为与引导类加载器关联
  3. 如果不是引用类型,则数组的可见类型默认为public

验证阶段

这一阶段是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致分为四个检验工作,如下:

  1. 文件格式验证,验证字节流是否符合Class文件格式的规范,并且是否能被虚拟机处理(版本号是否在处理范围)
    • 是否以魔数0xCAFEBABE开头
    • 主次版本号是否在虚拟机处理范围内
    • 常量池中的常量是否有不被支持的类型
    • 指向常量的各种引用值中是否有不存在的常量或者不支持的类型
    • 等等
      后面会说道class文件结构
  2. 元数据验证 进行语义分析
    • 是否有父类(Object类除外)
    • 是否继承了不允许被继承的类
    • 如果不是抽象类,要求实现父类或者接口中要求实现的类
    • 类中的字段、方法是否与父类产生矛盾
  3. 字节码验证 确定程序语义是否合法、符合逻辑
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
    • 保证跳转指令不会跳转到方法体以外的字节码指令上
    • 保证方法体中类型转换是有效的
  4. 符号引用验证 发生在虚拟机将符号引用转化为直接引用的时候
    • 符号引用中能否通过字符串描述的全限定名找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段、方法的访问性是否可被当前类引用

准备阶段

准备阶段正式为类变量分配内存并设置类变量的初始值,这些变量所使用的内存都在方法区进行分配。这里只包括类变量,不包括实例变量,
如果被final修饰,则进行赋值操作,就不是零值了,千万注意

解析阶段

解析阶段是讲常量池内的符号引用解析成直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行。

初始化阶段

初始化阶段,根据程序的主观计划去初始化一个类变量和其他资源,也就是执行类构造器()方法的过程

  • (),由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合而成。顺序,有语句在源文件出现的顺序决定
  • () 和()不同,不需要显示的调用父类的这个方法,会保证子类的()执行之前,父类的()已经执行完毕。
  • 父类的静态语句块优与子类
  • () 对类和接口来说并不是必须的,如果没有静态语句块和赋值操作,就不生成
  • 执行接口的()之前,不需要先执行父类的
  • 保证类的()在多线程环境中正确使用

类与类加载器

比较两个类是否相等,只有这两个类是由同一个类加载器加载,并且来自同一个Class文件。

双亲委派模型

  • BootStrap ClassLoader:启动类加载器,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
  • Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • Application ClassLoader:应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。

加载过程如下:

  1. 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
  2. 每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器
  3. 如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载

双亲委派 模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。

参考资料

来自经典好书 深入理解java虚拟机。建议入手一本,

最近访客