JVM类加载机制

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,如下图:
Paste_Image.png

由于本文主要讲解的是类的 加载 部分,所以加载,验证,准备,解析,初始化仅仅作下简单的回顾,详细内容参阅《深入理解Java虚拟机》

加载

类的加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序使用任何类时,系统都会为之创建一个java.lang.Class对象。

类也是一种对象,就像概念主要用于定义和描述其他的事物(反射中class含有各种信息),但概念本身也是一种事物。

通常有下面几种来源 加载 类的二进制数据

  1. 从本地文件系统加载class文件
  2. 从jar包中加载class文件,如从F盘动态加载jdbc的mysql驱动。
  3. 通过网络加载(典型应用Applet)
  4. 把一个java源文件动态编译并加载
  5. 从zip包读取,如jar,war,ear。
  6. 运算时计算生成(动态代理技术)
  7. 数据库中读取(可以加密处理)
  8. 其他文件生成(jsp文件生成对应的class文件)
验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段开始为 类变量(即static变量) 分配内存并设置 类变量的初始值(注意初始值一般为0,null也是零值)的 阶段,这些变量所使用的内存都将在方法区进行分配。

注意
这里进行内存分配的仅包含 《类变量》即static变量,而不包括实例变量不包括实例变量不包括实例变量,重要的事说3遍,实例变量将会在 对象实例化时随着对象一起分配在Java堆中。且初始值 通常情况 下 是数据类型 的零值

Paste_Image.png

public class Animal{
    private static int age = 20;
}

那变量age在 准备阶段 过后的初始值是0而不是20,因为这时候还没有执行任何Java方法。所以把age赋值为20的动作将在 初始化 阶段执行。

再次注意
存在一种特殊情况,如果上面的类变量声明为final的,则此时(准备阶段)就会被初始化为20。
public static final int age = 20;
编译时候,JavaC将会为age生成ConstantValue属性,在准备阶段 虚拟机就会根据ConstantValue的设置将age赋值为20。

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用(内存地址)的过程。

这里简单说下常量池

常量池 
1. 字面量 比较接近Java语言层面,如String字符串,声明final的常量等
2. 符号引用 属于编译原理方面的概念:1.包括类和接口的全限定名 2.字段的名称和描述符3.方法的名称和描述符

符号引用大概是下面几种 类型

1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info

的常量。

初始化

类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)

这里需要区分下<init> 和<client>

  1. <init>指的是实例构造器,也就是构造函数
  2. <client>指的是类构造器,这个构造器是jvm自动合并生成的。
    它合并static变量的赋值操作(1. 注意是赋值操作,仅声明的不会触发<client>,毕竟前面准备阶段已经默认赋过值为0了,2. final static的也是这样哦)和static{ }语句块生成,且虚拟机保证<client>执行前,父类的<client>已经执行完毕,所以说父类如果定义static块的话,一定比子类先执行,当然了,如果一个类或接口中没有static变量的赋值操作和static{ }语句块,那么<client>也不会被JVM生成。最后还要注意一点,static变量的赋值操作和static{}语句块合并的顺序是由语句在源文件中出现的顺序所决定的。

静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,前面的静态语句块只能赋值,不能访问。

Paste_Image.png

类初始化的时机

当Java程序 首次主动通过下面6种方式使用某个类或接口时候,系统就会初始化该类或接口,假如这个类还没有被 加载和连接,则程序先加载并连接该类。类的初始化只会发生一次,再次使用new,callMethod等等都不会重复初始化。

  1. 生成类的实例,如(1)new (2)反射newInstance (3)序列化生成obj
  2. 调用static的方法,如LogUtil.i(TAG,"fucking");
  3. 访问类或接口的 static变量,或者为static变量赋值。注意有特例(一会说明)。
  4. Class.forName(name);
  5. 初始化某个类的子类,子类的所有父类都被初始化
  6. java.exe 运行Main类(public static void main),jvm会先初始化该主类。

刚才说 3 有一个特例,需要特别指出,仍然是static的变量,前面说过,如果是 static final类型的则会在准备阶段 就给赋值并加入常量池。所以仅仅访问某个类的常量并不会导致该类初始化。

class Person{ 
    public static int age = 20;
    static { 
        System.out.println("静态初始化!"); 
    }
} 
public class Test { 
    public static void main(String args[]){ 
        System.out.println(Person.age); 
    }
 }

没有final修饰的情况打印

静态初始化!
20

 

加上final修饰后打印

20

以下是不会执行类初始化的几种情况

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组,不会触发该类的初始化。
  3. 就是上面说的那种情况,类A引用类B的static final常量不会导致类B初始化。
  4. 通过类名获取Class对象,不会触发类的初始化。如
    System.out.println(Person.class);
  5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

JVM类加载机制算是结尾了,不过在参考其他文章时候发现一个非常棒的例子,可以很好的验证上面的结论。

出处是 简书:小腊月

public class Singleton{
    private static Singleton singleton = new Singleton();
    public static int counter1;
    public static int counter2 =0;
    private Singleton(){
      counter1++;
      counter2++;
  }
    public static Singleton getSingleton(){
        return singleton;
    }
}
public class Main{
    public static void main(String args[]){
        Singleton singleton = Singleton.getSingleton();
        System.out.println("counter1="+singleton.counter1);
        System.out.println("counter2="+singleton.counter2);
    }
}

根据 类初始化的时机 所作的结论

  1. 执行Main方法,根据结论6,会首先初始化Main类,Main类从(加载开始 ----> 初始化结束)
  2. 执行到Singleton.getSingleton();时候,根据结论2,直接 【先】 触发【类的初始化】初始化Singleton类,Singleton类首次初始化,所以从 加载部分开始执行,执行到 准备阶段 所有static变量都被设置为初始值。此时
    public static int counter1 = 0; 
    public static int counter2 = 0; 
    private static Singleton singleton = null;

    Singleton执行到初始化阶段,生成类构造器<client>,类构造器会合并 static变量的赋值操作和 static语句块。合并后执行

    public static int counter1 ;// 由于 counter1没被赋值,所以不会被合并进去
    public void client() {// 伪代码:<client>方法体内容
        Test singleton = new Test();//(1)
        int counter2 = 0;// (2)
    }

    4.**初始化阶段**执行client内代码,执行到(1)处,此时counter1counter2都变为15.**初始化阶段**执行client代码,执行到(2)处,counter2又被设置为06.**初始化结束**,回到Main方法的Singleton.getSingleton();继续执行main方法,最后输出结束。最后打印结果为:

counter1= 1
counter2= 0

 

## 详解类加载(第一阶段) 类加载部分是我们能够操作的部分,其他部分不需要我们管理。 
Jvm启动时候默认至少开启了3个类加载器,分别是Bootstrap ClassLoader,Extension ClassLoader,Application ClassLoader各自加载各自管辖的区域。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-3f7c2473524f8307.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 

1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 
2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3. 应用程序类加载器(Application ClassLoader)或者叫**System ClassLoader**:负责加载用户路径(classpath)上的类库。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-c86d07145d0bfadf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 

>此程序说明:Java中getClassLoader用的一般和getSystemClassLoader是一个实例。源码中ClassLoader默认的构造器也说明这点,initSystemClassLoader里面会获取sun.misc.Launcher.getLauncher().getClassLoader()作为默认的parent。 
**这里重点强调**:Android的ClassLoader类也有一个getSystemClassLoader()方法,但是又被改写了,后面再说明这个问题。 
        

 

【出处】:https://www.imooc.com/article/21512

posted on 2018-09-11 14:04  i野老i  阅读(247)  评论(0编辑  收藏  举报