JAVA类加载器详解

一.为什么需要类加载:

  Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性。例如:

  • 编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类;
  • 用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分;(如Applet,JSP,OSGI技术都是以其为基础).

二.类加载过程:

JAVA类加载器详解_第1张图片

这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用,激活另一个阶段。

Ⅰ.加载:

在加载阶段,JVM通常要完成以下三个任务:

1). 通过类的全限定名来获取类的二进制字节流。
2). 将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构
3). 在内存中生成该类所对应的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(并没有明确规定是在JAVA堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区内)。

对于1而言,由于这里并没有规定其获取字节流的具体方式,因此可以通过各种方式获取。JAVA的许多技术便是基于其之上:

  • 从ZIP包中获取,后面发展为从JAR和WAR中获取。
  • 从网络中获取,如Applet的应用。
  • 计算机运行时获取,如动态代理的使用。
  • 由其他文件生成,如JSP技术,即由JSP文件生成相对应的类。
  • 从数据库中获取,相对使用较少。

要特别注意的是,数组类的加载情况和非数组类的加载情况有些不同。数组类本身不是通过类加载器创建,而是由JVM直接创建的。其具体创建过程遵循以下规则:

1). 如果数组的组件类型是引用类型,则用加载普通类的方法加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
2). 如果数组的组件类型不是引用类型(如int数组),JAVA虚拟机将会把数组C标记为与引导类加载器相关。
3). 数组类型的可见性与它的组件一致,如果组件不是引用类型,那数组类的可见性将默认为public。

相对于类加载过程的其他阶段而言,一个非数组类的加载阶段是开发人员可控性最高的阶段。开发人员可以通过重写loadClass()方法来控制获取字节流的方式。

加载阶段与连接阶段的部分内容是交叉进行的,加载阶段(验证,准备,解析)尚未完成,连接阶段可能已经开始。

Ⅱ.验证:

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上来看,验证阶段大概分为下面四个阶段的检验动作:

1)文件格式验证
2)元数据验证
3)字节码验证
4)符号引用验证

  1. 文件格式验证:
    第一阶段要验证字节流是否符合Class文件流规范以及该版本是否能被虚拟机处理。可能包含:

    • 是否以魔数开头
    • 主,次版本是否在当前虚拟机处理范围之内
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合数据类型的常量。

      该阶段的主要目的是保证输入的字节流能正确地解析并储存于方法区之内,格式上符合描述一个Java类型信息的要求。
      只有通过了这一阶段的验证后,字节流才会进入内存的方法区中进行存储。后面的三个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流。

  2. 元数据验证:
    第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。其可能包括:

    • 这个类是否有父类(除了Object类以外,其余类都应当有父类)
    • 这个类的父类是否是不允许被继承的类(是否由final修饰)
    • 如果这个类不是抽象类,是否实现了其父类和接口之中要求实现的方法
    • 类中的字段,方法是否于父类矛盾(例如覆盖了父类的final字段,或者有不符合规则的方法重载等等)。
  3. 字节码验证
    该过程是验证过程中最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。例如:

    • 保证操作数栈的数据类型与指令代码序列都能配合工作。
    • 保证跳转指令不会跳转到方法体以外的字节码上。
    • 保证方法体中的类型转换是有效的。

    为了避免过多的时间消耗在字节码验证阶段,在JDK1.6之后的javac编译器和JAVA虚拟机中进行了一项优化,给方法体的Code属性中新加了一项名为”StackMapTable”的属性,只需要检查StackMapTable属性中的记录是否合法即可。这样将字节码验证的类型推导转变为类型检查从而节省一些时间。
    在JDK1.7之后,版本号大于50的Class文件,使用类型检查来完成数据流分析成为唯一选择。在其之前可以通过虚拟机选项对其进行开关。

  4. 符号引用验证
    最后一个验证阶段和解析阶段同时发生,发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常包括:

    • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
    • 符号引用中的类,字段方法的访问性是否可以被当前类访问。

    符号引用验证的目的是确保解析动作能正常的执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。

对于JVM类加载机制来说,验证阶段并不是必要的,如果你的类已经被多次验证,则可以通过JVM的 -Xverify:none 参数来关闭大部分的类验证,节约时间。


Ⅲ.准备:

  准备阶段是正式为类变量分配内存并设置类变量初始值(通常指零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。

注意:
① 这时候进行内存分配的仅包括类变量(被static修饰)。
② 这里所说的初始值通常是数据类型的零值。因为此阶段尚未执行任何java方法。存放于类构造器的 < clinit >()在初始化阶段才会执行

例:     public static int a = 123;
       //在该阶段过后a的值为0;

但在一些特殊情况下,如:
      public static final int a = 123;
     //则编译时javac将会为a生成ConstantValue属性,在准备阶段JVM就会根据ConstantValue的设置将value赋值为123。

Ⅳ.解析:

  该阶段是JVM将常量池中的符号引用替换为直接引用的过程。

  • 符号引用: 符号引用可以是任何形式的字面量,只要使用时可以无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。在Class文件中常以CONSTANT_CLASS info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等类型的常量出现。
  • 直接引用:直接引用可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经存在于内存中。

虚拟机类加载的解析阶段的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行解析为直接引用。

Ⅴ.初始化:

  主动引用会使类初始化,被动引用则不会。

1.主动引用:

  • 创建类实例。也就是new的方式
  • 调用某个类的类方法
  • 访问某个类或接口的类变量,或为该类变量赋值
  • 使用反射方式强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类,则其父类也会被初始化
  • Class.forName()方法(这个应该也属于第四个吧)

2.被动引用:

  • 通过子类引用父类的静态字段。其子类不会初始化。
  • 通过数组来引用类,该类不会初始化。
  • 常量在准备阶段会存入调用类的常量中,因此不会引发初始化。

三.类加载器类型和机制:

JAVA类加载机制全解析

四.程序运行时类在内存中的存储位置:

   Java程序运行时的内存结构分为:程序计数器、方法区、栈内存、堆内存、本地方法栈。

  • 程序计数器:
    当前线程所执行的字节码的行号指示器,每一条线程都需要有一个独立的程序计数器。称为”线程私有”。

  • 方法区:
    存放装载的类数据信息,包括:基本信息:每个类的全限定名、每个类的直接超类的全限定名、该类是类还是接口、该类型的访问修饰符、直接超接口的全限定名的有序列表。每个已装载类的详细信息:运行时常量池、字段信息、方法信息、静态变量、到类classloader的引用、到类class的引用。

  • 栈内存:
    Java栈内存由局部变量区、操作数栈、帧数据区组成,以帧的形式存放本地方法的调用状态(包括方法调用的参数、局部变量、中间结果……)。其也是”线程私有”。

  • 堆内存:
    堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

  • 本地方法栈内存:
    Java通过Java本地接口JNI(Java Native Interface)来调用其它语言编写的程序,在Java里面用native修饰符来描述一个方法是本地方法。

当程序new一个对象时,如:

ArrayList a = new ArrayList();

其具体的执行过程如下:

1、 Java虚拟机到方法区找到ArrayList类的类型信息,如果没有找到,Java虚拟机立马加载ArrayList类,把ArrayList类的类型信息存放在方法区里,并执行该类的静态代码块。
2、 Java虚拟机首先在堆区中为一个新的ArrayList实例分配内存, 并在ArrayList实例的内存中存放一个方法区中存放ArrayList类的类型信息的内存地址。
3、 JVM的进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。
4、位于“=”前的a是一个在main()方法中定义的一个变量(一个ArrayList对象的引用),因此,它被会添加到了执行main()方法的主线程的JAVA方法调用栈中。而“=”将把这个a变量指向堆区中的ArrayList实例。

如果想要了解更详细,参看:java程序运行时内存分配详解

你可能感兴趣的:(JVM)