likes
comments
collection
share

jvm 类加载过程

作者站长头像
站长
· 阅读数 2

github.com/ccnio/Wareh…

Class对象

Class类也是一个实实在在的类,而Class的创建的实例就是Class对象。Class对象则保存了类相关的类型信息。

- Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载 - Class类的对象的作用是运行时提供或获得某个对象的描述信息,这点对于反射技术很重要。

类型信息 类型信息是一个java类的描述信息,classloader加载一个类时从class文件中提取出来并存储在方法区。它包括以下信息:

- 类型的完整有效名,类型的修饰符(public,abstract, final的某个子集),类型直接接口的一个有序列表及继承的父类。在java源代码中,类Object的的完整名称为java.lang.Object,但在类文件里,所有的”.”都被斜杠“/”代替,就成为java/lang/Object。 - 类型的常量池。常量池就是这个类型用到的常量的一个有序集合,包括实际的常量(integer, 和floating point常量)和对类型,域和方法的符号引用。因为常量池存储了一个类型所使用到的所有类型,域和方法的符号引用,所以它在java程序的动态链接中(动态绑定)起了核心的作用。 - 域(Field)信息:域名、域类型、域修饰符(public, private, protected,static,final volatile, transient的某个子集) - 方法(Method)信息:方法名、方法的返回类型(或 void)、方法参数的数量和类型(有序的)、方法的修饰符(public, private, protected, static, final, synchronized, native, abstract的一个子集) - classloader的引用,下面类的引用关系里会介绍到

Classloader加载一个类并把类型信息保存到方法区后,会创建一个Class对象,存放在堆区的,不是方法区,它为程序提供了访问类型信息的方法。JVM在运行时要大量使用存储在方法区中的类型信息。 方法区是被所有线程共享的,所以必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类还没有被加载的情况下,只应该有一个线程去加载,而另一个线程等待(单例的一种实现方式)。

类的加载过程

概述

看下下面的两份代码的执行结果是什么?

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

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

jvm 类加载过程 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

加载

查找并加载类的二进制数据,加载是类加载过程中的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

- 类的加载指的是将类的.class文件中的二进制数据读取到内存中 - 将其放在运行时数据区的方法区内, - 然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 jvm 类加载过程 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

类加载器

不同的类加载器负责的组件不同,可分为2种类型

- 自定义类加载器(java.lang.ClassLoader的子类) - java虚拟机自带类加载器

java虚拟机自带类加载器按类型又可分为三种类型(从上到下是父子关系):

- 启动类加载器:Bootstrap ClassLoader。C/C++实现的  负责加载存放在JDK\jre\lib下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 - 扩展类加载器:Extension ClassLoader    使用java代码实现 该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器:Application ClassLoader    使用java代码实现 该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,只有当父加载器在它的搜索范围中没有找到所需的类时,子加载器才会尝试自己去加载该类。 双亲委派机制:

1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。 2. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

双亲委派模型意义:

- 系统类防止内存中出现多份同样的字节码 - 保证Java程序安全稳定运行

层次关系如下图: jvm 类加载过程

自定义类加载器

所有用户自定义类加载器都应该继承ClassLoader类。 在自定义ClassLoader的子类是,我们通常有两种做法:

- 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐) - 重写findClass方法 (推荐)

Android 提供了两类自定义的 ClassLoader 来加载 dex 中的 .class 文件。

- PathClassLoader 只能加载已安装(data/app/目录下)的apk - DexClassLoader 可以加载外部(sd卡上)的apk

验证

类被加载后,就进入连接阶段。连接就是将已经读取到内存的类的二进制数据合并到虚拟机的运行时环境中去 jvm 类加载过程

准备

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值,这些内存在java7之前把静态变量存放于方法区,静态变量从7开始就在堆中。对于该阶段有以下几点需要注意:

- 这时候进行内存分配的仅包括类变量(static),按照声明的顺序依次声明并设置为该类型的默认值,但不赋值为初始化的值。而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 - 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

例如Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存控件,并赋予默认值0,为long类型的静态变量b分配8个字节的内存控件,并赋予默认值0;

public class Sample {  
    private static int a = 1;  
    private static int b;  
  
    static {  
       b = 2
    }  
}  

解析

在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

public void gotoWork() {  
   car.run(); //这段代码在Worker类的二进制数据中表示为符号引用  
}  

在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法区内的内存位置,这个指针就是直接引用。

初始化

初始化阶段才真正开始执行类中定义的java程序代码。Java虚拟机执行类的初始化语句(注意这里与准备阶段初始化数据的不同),为类的静态变量赋予正确的初始值。在程序中,静态变量的初始化有两种途径:

- 在静态变量的声明处进行初始化 - 在静态代码库中进行初始化

注意,按声明的顺序依次设置为初始化的值,如果没有初始化的值就跳过。

开头的例子中,准备阶段:singleton初始化前,counter1/counter2为0;singleton初始化后,counter1/counter2为1;如果再有一个 singleton2 初始化,counter1/counter2则变为2; 然而在初始化阶断,counter1未赋值则忽略执行,counter2赋值为1,覆盖了原来的初始化。 接下来继续2个例子

public class Test2 {  
    public static void main(String[] args) {  
        System.out.println(FinalTest2.x); // 3  
    }  
}  
  
class FinalTest2 {  
    public static final int x=6/2;  
    static {  
        System.out.println("I am a final x"); // 不会执行  
    }  
}  
class FinalTest2 {  
    public static final int x=new Random().nextInt();  
    static {  
        System.out.println("I am a final x");  
    }  
}  

为什么第一个demo里面的静态块没有执行呢? public static final int x=6/2; java虚拟机在编译期就可以知道x的值是什么,因此在编译期就已经把3放到了常量池,所以在main方法中调用的时候不会触发类的初始化  即此时的x为编译期的常量 public static final int x=new Random().nextInt();  这行代码中的x在编译期不能确定具体的值,需要等到运行的时候才能确定x的值,所以在运行时会触发类的初始化,即此时的x为编译器的变量

只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。这句话是什么意思呢?下面让我们看下这个Demo

public class Test3 {  
    public static void main(String[] args) {  
        System.out.println(Child.x);  
    }  
}  
  
class Parent{  
    public static int x=3;  
    static {  
        System.out.println("this is a parent");  
    }  
}  
  
class Child extends Parent{  
    static {  
        System.out.println("this is a Child");  
    }  
}  

jvm 类加载过程

class Parent {  
    public static int x=3;  
    static {  
        System.out.println("this is a parent");  
    }  
}  
  
class Child extends Parent{  
    public static int x=3;  
    static {  
        System.out.println("this is a Child");  
    }  
}  

jvm 类加载过程

上面两个例子同样是相差一行代码,结果却差别很大,一个Child发生了初始化,另一个没有。

总结上述类的初始化时机。

初始化时机场景

类的初始化时机就是在"在首次主动使用时",那么,哪些情形下才符合首次主动使用的要求呢?首次主动使用的情:

- 创建某个类的新实例时--new、反射、克隆或反序列化; - 调用某个类的静态方法时; - 使用某个类或接口的静态字段或对该字段赋值时(final字段除外); - 调用Java的某些反射方法时 - 初始化某个类的子类时 - 在虚拟机启动时某个含有main()方法的那个启动类。

除了以上几种情形以外,所有其它使用JAVA类型的方式都是被动使用的,他们不会导致类的初始化。

被动引用的几种经典场景

1. 通过子类引用父类的静态字段,不会导致子类初始化 2. 通过数组定义来引用类,不会触发此类的初始化:newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化 3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

类的卸载

卸载时机

当满足以下条件时,类可以被卸载

- 该类所有的实例已经被回收 - 该类对应的java.lang.Class对象没有任何对方被引用

由jvm自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的。为什么哪? java虚拟机自带的类加载器包含根类加载器、扩展类加载器、应用程序类加载器,Jvm本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载类的Class对象,因此这些Class对象始终是可触及的。而由用户自定义的类加载器所加载的类才可以被卸载。那这里说的引用关系是如何的哪?

类的引用关系

一个Class对象总是会引用他的类加载器,调用Class对象的getClassLoader方法就可以获得它的类加载器。类加载器则也会始终引用他们所加载类的Class对象。由此可见,Class实例和加载它的加载器之间是双相关联关系。

Class objClass = loader1.loadClass("Sample");   
Object obj = clazz.newInstance();  

jvm 类加载过程 loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。