JVM类加载机制


  • 在了解类加载机制前先看下java程序执行流程
    java程序执行流程

    1. 类加载的过程

  • 类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)
    类生命周期
  • 加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。
  • 加载、验证、准备、解析、初始化这五个步骤组成了一个完整的类加载过程。使用没什么好说的,卸载属于GC的工作

1. 加载(Loading)

  • what loading do
    1. 获取.class文件的二进制流(可以从jar,era,war,applet,jsp获取或者运行时计算生产(动态代理技术))
    2. 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
    3. 在内存中生成一个代表这个.class文件的java.lang.Class对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
  • 加载阶段与链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持这固定的先后顺序

2. 验证(Verification)

  • what verification do
    1. 文件格式验证
    2. 元数据验证
    3. 字节码验证
    4. 符号引用验证
  • 这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,.class文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

3. 准备(Preparation)

  • what preparation do

    为类变量分配内存并设置其初始值

  • 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
  • static变量赋值为0,正常赋值在初始化阶段才进行,final static常量正常赋予用户指定的值

    4. 解析(Resoluting)

  • what resolution do

    将常量池内的符号引用替换为直接引用

  • 符号引用

    符号引用是以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标(类、变量、、方法)即可.符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中.各种虚拟机实现的内存布局可以各不相同,但他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中

  • 直接引用

    直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。

    5. 初始化(Initiallization)

  • what initiallization do
    1. 给static变量赋予用户指定的值
    2. 执行静态代码块
    3. 若该类具有超类,则对其进行初始化

5.1 <clinit>()

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
    public class ClassLoader {
      static{
          i = 0;//给变量赋值可以正常通过
          System.out.println(i);//这句话编译器提示"Illegal forward reference"(非法向前引用)
      }
      static int i = 1;
    }
    
  • <clinit>()方法与类的构造函数不同,它不需要显式的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前父类的<clinit>()方法已经执行完毕.因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object.由于父类的<clinit>()方法先执行,也就意味着父类定义的静态语句块要优先于子类的变量赋值操作
    `java
    public class Parent {
    public static int a = 1;
    static {
      a = 2;
    
    }
    }

public class Child extends Parent{
public static int b = a;
}

public class ClassLoader {
public static void main(String[] args) {
System.out.print(Child.b);//result is 2
}
}

* <clinit\>()方法对类或接口来说不是必需的,如果一个类中没有静态语句块也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit\>()方法
* 虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步,即如果多个线程同时去初始化一个类,那么只会有一个类去执行这个类的<clinit\>()方法,其他线程都要阻塞等待,直至活动线程执行<clinit\>()方法完毕。因此如果在一个类的<clinit\>()方法中有耗时很长的操作,就可能造成多个进程阻塞。不过其他线程虽然会阻塞,但是执行<clinit\>()方法的那条线程退出<clinit\>()方法后,其他线程不会再次进入<clinit\>()方法了,因为同一个类加载器下,一个类只会初始化一次。

#### 5.2 when do initiallization
* Java虚拟机规范严格规定了有且只有5种场景必须立即对类进行初始化(而加载,验证,准备,解析自然也要在此之前开始),这5种场景也称为对一个类进行主动引用
    >1. 使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法的时候
    >2. 使用java.lang.reflect包中的方法对类进行反射调用的时候
    >3. 初始化一个类,发现其父类还没有初始化过的时候,则需要先触发父类的初始化
    >4. 虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类
    >5. 看不懂不写了打字很累
* 对于这5种会触发类进行初始化的场景,虚拟机在规范中使用了一个很强烈的限定语:有且只有.除此之外,所有引用类的方式都不会触发类的初始化,称为被动引用,接下来看下被动引用的几个例子:
```java
1.子类引用父类静态字段,不会导致子类初始化。至于子类是否被加载、验证了,前者可以通过”-XX:+TraceClassLoading”来查看

public class SuperClass
{
    public static int value = 123;

    static
    {
        System.out.println("SuperClass init");
    }
}

public class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}

运行结果为:
SuperClass init
123
2.通过数组定义引用类,不会触发此类的初始化
public class SuperClass
{
    public static int value = 123;

    static
    {
        System.out.println("SuperClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        SuperClass[] scs = new SuperClass[10];
    }
}

运行结果为:(没结果哦)
3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类
public class ConstClass
{
    public static final String HELLOWORLD =  "Hello World";

    static
    {
        System.out.println("ConstCLass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        System.out.println(ConstClass.HELLOWORLD);
    }
}
运行结果为:
Hello World

2. 类加载器(ClassLoader)

2.1 类加载器划分

  • 启动类加载器(Bootstrap ClassLoader)

    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

  • 扩展类加载器(Extension ClassLoader)

    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

  • 应用程序加载类/系统加载类(Application/System ClassLoader)

    也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

    2.2 双亲委派模型

    双亲委派模型

  • 双亲委派模式(Parents Delegation Model)要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码
  • 双亲委派模式是在Java 1.2后引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器的实现方式。
  • 其工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
    //双亲委派模型的实现源码
    protected synchronized Class<?> loadClass(String name, boolean resolve)  
    throws ClassNotFoundException  
    {  
      //首先, 检查请求的类是否已经被加载过了  
      Class c=findLoadedClass(name);  
      if( c== null ){  
          try{  
              if( parent != null ){  
                  c = parent.loadClass(name,false);  
              } else {  
                  c = findBootstrapClassOrNull(name);  
              }  
          } catch (ClassNotFoundException e) {  
          //如果父类加载器抛出ClassNotFoundException  
          //说明父类加载器无法完成加载请求  
          }  
          if( c == null ) {  
              //在父类加载器无法加载的时候  
              //再调用本身的findClass方法来进行类加载  
              c = findClass(name);  
          }  
      }   
      if(resolve){  
          resolveClass(c);  
      }  
      return c;  
    }  
    

2.3 类加载器工作原理

类加载器的工作原理基于三个机制:委托、可见性和单一性。

  • 委托机制(与双亲委派模型工作原理一样)
  • 可行性机制

    子类加载器可以看到父类加载器加载的类,而反之则不行

  • 单一性机制

    父加载器加载过的类不能被子加载器加载第二次

2.4 如何显示的加载类

  • Java提供了显式加载类的API:Class.forName(classname)和Class.forName(classname, initialized, classloader),你可以指定类加载器的名称以及要加载的类的名称。类的加载是通过调用java.lang.ClassLoader的loadClass()方法,而loadClass()方法则调用了findClass()方法来定位相应类的字节码。
  • Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象
  • 所谓class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式,显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

文章作者: kangshifu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kangshifu !
 上一篇
JVM垃圾回收机制 JVM垃圾回收机制
概述 Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存,而且这两个问题针对的内存区域就是Java内存模型中的堆区和方法区,但主要是堆(栈中的栈帧随着方法的进入和退出有条
2018-05-28
下一篇 
JVM内存区域 JVM内存区域
1. 线程独占区1.1 程序计数器(Program Counter Register) 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行
2018-05-23
  目录