java类加载详解

JAVA源码编译由三个过程组成:1. 源码编译机制 2. 类加载机制 3. 类执行机制

我们这里主要介绍类加载机制。

一、源码编译

代码编译由JAVA源码编译器来完成。主要是将源码编译成字节码文件(class文件)。字节码文件格式主要分为两部分:常量池方法字节码

二、类加载

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
《java类加载详解》

系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行的java程序处于两个不同的JVM进程中,两个jvm之间并不会共享数据。

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

类加载机制的步骤

JVM将类加载过程分为三个步骤:装载(load),链接(link)和初始化(initialize),其中链接又分为三个步骤:

  • 验证(varification)
  • 准备(Preparation)
  • 解析(Resolution);

装载阶段

这个流程中的加载(装载)是类加载机制中的一个阶段,这两个概念不要混淆,这个阶段需要完成的事情有:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区运行时数据结构

3)在java堆中生成一个代表这个类的Class对象,作为访问方法区中这些数据的入口

由于第一点没有指明从哪里获取以及怎样获取类的二进制字节流,所以这一块区域留给我开发者很大的发挥空间。这个我在后面的类加载器中在进行介绍。

加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

JVM在内存 中的 表现形式:
《java类加载详解》

验证阶段

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

Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,

编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,

包括用十六进制编辑器(如UltraEdit)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。

不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:
《java类加载详解》

准备阶段

这个阶段正式为类变量(被static修饰的变量)分配内存并设置类变量初始值,这个内存分配是发生在方法区中。

  1. 注意这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。

  2. 这里设置的初始值,通常是指数据类型的零值。

private static int a = 3;

这个类变量a在准备阶段后的值是0,将3赋值给变量a是发生在初始化阶段。

解析阶段

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

初始化阶段

初始化是类加载机制的最后一步,这个时候才正真开始执行类中定义的JAVA程序代码。在前面准备阶段,类变量已经赋过一次系统要求的初始值,在初始化阶段最重要的事情就是对类变量进行初始化,关注的重点是父子类之间各类资源初始化的顺序。

java类中对类变量指定初始值有两种方式:
1. 声明类变量时指定初始值;
2. 使用静态初始化块为类变量指定初始值。

初始化的步骤

1、如果该类还没有加载和连接,则程序先加载该类并连接。

2、如果该类的直接父类没有加载,则先初始化其直接父类。

3、如果类中有初始化语句,则系统依次执行这些初始化语句。

在第二个步骤中,如果直接父类又有直接父类,则系统会再次重复这三个步骤来初始化这个父类,依次类推,JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。

以下是java中静态语句块和非静态语句块以及构造方法的执行顺序
调用顺序:父类静态,子类静态,父类非静态,父类构造,子类非静态,子类构造

什么时候才能触发初始化?

** 1)创建类实例的时候,分别有:1、使用new关键字创建实例;2、通过反射创建实例;3、通过反序列化方式创建实例。**

  new Test();

  Class.forName(“com.mengdd.Test”);

2)调用某个类的类方法(静态方法)

Test.doSomething();

**3)访问某个类或接口的类变量,或为该类变量赋值。 **

int b=Test.a;
Test.a=b;

4)初始化某个类的子类。当初始化子类的时候,该子类的所有父类都会被初始化。

5)直接使用java.exe命令来运行某个主类。

不会初始化的被动引用

1、子类引用父类的静态变量,不会导致子类初始化。

public class SupClass
{
    public static int a = 123;

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

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

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

执行结果

supclass init
123

可以看到,子类调用了父类的静态变量,但是只初始化了父类而没有初始化子类


2、通过数组定义引用类,不会触发该类的初始化

public class SupClass
{
    public static int a = 123;

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

public class Test
{
    public static void main(String[] args)
    {
        SupClass[] spc = new SupClass[10];
    }
}

执行结果没有输出

3、引用常量时,不会触发该类的初始化

public class ConstClass
{
    public static final String A=  "MIGU";

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

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

执行结果

MIGU

用final修饰某个类变量时,它的值在编译时就已经确定好放入常量池了,所以在访问该类变量时,等于直接从常量池中获取,并没有初始化该类。

类加载器详解

下面这篇文章介绍的很详细,我就不多费笔墨了
深入理解Java类加载器(ClassLoader) https://blog.csdn.net/javazejian/article/details/73413292

参考

JAVA类加载机制详解 https://www.cnblogs.com/dongguacai/p/5860241.html
java类加载详解 https://www.cnblogs.com/zhangyu0217—-/p/7625536.html

点赞

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注