资讯专栏INFORMATION COLUMN

JVM类加载过程分析及验证

zhangyucha0 / 3380人阅读

摘要:类加载过程共分为加载验证准备解析初始化使用和卸载七个阶段这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

JVM类加载过程共分为加载、验证、准备、解析、初始化、使用和卸载七个阶段

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

加载

加载过程是JVM类加载的第一步,如果JVM配置中打开-XX:+TraceClassLoading,我们可以在控制台观察到类似

[Loaded chapter7.SubClass from file:/E:/EclipseData-Mine/Jvm/build/classes/]

的输出,这就是类加载过程的日志。
加载过程是作为程序猿最可控的一个阶段,因为你可以随意指定类加载器,甚至可以重写loadClass方法,当然,在jdk1.2及以后的版本中,loadClass方法是包含双亲委派模型的逻辑代码的,所以不建议重写这个方法,而是鼓励重写findClass方法。
类加载的二进制字节码文件可以来自jar包、网络、数据库以及各种语言的编译器编译而来的.class文件等各种来源。
加载过程主要完成如下三件工作:
1>通过类的全限定名(包名+类名)来获取定义此类的二进制字节流
2>将字节流所代表的静态存储结构转化为运行时数据结构存储在方法区
3>为类生成java.lang.Class对象,并作为该类的唯一入口

这里涉及到一个概念就是类的唯一性,书上对该概念的解释是:在类的加载过程中,一个类由类加载器和类本身唯一确定。也就是说,如果一个JVM虚拟机中有多个不同加载器,即使他们加载同一个类文件,那得到的java.lang.Class对象也是不同的。因此,只有在同一个加载器中,一个类才能被唯一标识,这叫做类加载器隔离。

验证

验证过程相对来说就有复杂一点了,不过验证过程对JVM的安全还是至关重要的,毕竟你不知道比人的代码究竟能干出些什么。
验证过程主要包含四个验证过程:
1>文件格式验证
四个验证过程中,只有格式验证是建立在二进制字节流的基础上的。格式验证就是对文件是否是0xCAFEBABE开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。
2>元数据验证
元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。
3>字节码验证
既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验,例如:字节码是否执行到了方法体以外、类型转换是否合理等。
当然,这很复杂。
所以,即使是到了如今jdk1.8,也还是无法完全保证字节码验证准确无遗漏的。而且,如果在字节码验证浪费了大量的资源,似乎也有些得不偿失。
4>符号引用验证
符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。
因为都是验证,所以一并在这讲。符号引用验证做的工作主要是验证字段、类方法以及接口方法的访问权限、根据类的全限定名是否能定位到该类等。具体过程会在接下来的解析阶段进行分析。
好了,验证阶段的工作基本就是以上四类,下面我们来看下一个阶段。

准备

相信经历过艰辛的验证阶段的磨练,JVM和我们都倍感疲惫。所以,接下来的准备阶段给我们提供了一个相对轻松的休息阶段。
准备阶段要做的工作很简单,他瞄准了类变量这个元数据,把他放进了方法区并进行了初始化,这里的初始化并不是或者操作,准备阶段只是将这些可爱的类变量置零。

解析

这一部分我画了几个图,内容有些多,放在另一篇文章里:解析

初始化

初始化阶段是我们可以大搞实验的一块实验田。首先,初始化阶段做什么?这个阶段就是执行方法。而方法是由编译器按照源码顺序依次扫描类变量的赋值动作和static代码块得到的。
那么问题来了,啥时候才会触发一个类的初始化的操作呢?答案有且只有五个:
1>在类没有进行过初始化的前提下,当执行newgetStaticsetStaticinvokeStatic字节码指令时,类会立即初始化。对应的java操作就是new一个对象、读取/写入一个类变量(非final类型)或者执行静态方法。
2>在类没有进行过初始化的前提下,当一个类的子类被初始化之前,该父类会立即初始化。
3>在类没有进行过初始化的前提下,当包含main方法时,该类会第一个初始化。
4>在类没有进行过初始化的前提下,当使用java.lang.reflect包的方法对类进行反射调用时,该类会立即初始化。
5>在类没有进行过初始化的前提下,当使用JDK1.5支持时,如果一个java.langl.incoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上五种情况被称作类的五种主动引用,除此之外的任何情况都被相应地叫做被动引用。以下是集中常见的且容易迷惑人心智的被动引用的示例:

/**
    通过子类引用父类的类变量不会触发子类的初始化操作
*/
public class SuperClass {
    
    public static String value = "superClass value";
    
    static {
        System.out.println("SuperClass init!");
    }
}

public class SubClass extends SuperClass implements SuperInter{

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

public class InitTest {
    
    static {
        System.out.println("InitTest init!");//main第一个初始化
    }

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

/**
output:
InitTest init!
SuperClass init!
superClass value
*/
/**
    通过定义对象数组的方式是不能触发对象初始化的
*/
    public static void main(String[] args) {
        SubClass[] superArr = new SubClass[10];
    }
/**
output:
InitTest init!
*/   
/**
    引用类的final类型的类变量无法触发类的初始化操作
*/  
public class SuperClass {
    public static final String CONSTANT_STRING = "constant";
    
    static {
        System.out.println("SuperClass init!");
    }
}

public class InitTest {
    static {
        System.out.println("InitTest init!");//main
    }

    public static void main(String[] args) {
        System.out.println(SuperClass.CONSTANT_STRING);//getStatic
    }
}
/**
output:
InitTest init!
constant
*/ 

了解了什么时候出发初始化操作后,那么初始化操作的执行顺序是什么样的?并发初始化情况下的运行机制又如何?
JVM虚拟机规定了几条标准:

先父类后子类,(源码中)先出现先执行

向前引用:一个类变量在定义前可以赋值,但是不能访问。

非必须:如果一个类或接口没有类变量的赋值动作和static代码块,那就不生成方法.

执行接口的方法不需要先执行父接口的方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的方法。

同步性:方法的执行具有同步性,并且只执行一次。但当一个线程执行该类的方法时,其他的初始化线程需阻塞等待。

我们通过一个实例来验证线程的阻塞问题:

public class SuperClass {    
    static {
        System.out.println("SuperClass init!");
        System.out.println("Thread.currentThread(): " + Thread.currentThread() + " excuting...");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class InitTest {
    
    static {
        System.out.println("InitTest init!");//main
    }

    public static void main(String[] args) throws ClassNotFoundException, InterruptedException {
        currentInitTest();
    }
    
    public static void currentInitTest() throws InterruptedException {
        Runnable run = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread.currentThread(): " + Thread.currentThread() + " start");
                new SuperClass();
                System.out.println("Thread.currentThread(): " + Thread.currentThread() + " end");
            }
        };
        
        Thread[] threadArr = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArr[i] = new Thread(run);
        }
        
        for (Thread thread : threadArr) {
            thread.start();
        }    
    }
}

/**
output:
InitTest init!
Thread.currentThread(): Thread[Thread-0,5,main] start
Thread.currentThread(): Thread[Thread-1,5,main] start
Thread.currentThread(): Thread[Thread-2,5,main] start
Thread.currentThread(): Thread[Thread-7,5,main] start
Thread.currentThread(): Thread[Thread-6,5,main] start
Thread.currentThread(): Thread[Thread-3,5,main] start
Thread.currentThread(): Thread[Thread-5,5,main] start
Thread.currentThread(): Thread[Thread-9,5,main] start
Thread.currentThread(): Thread[Thread-4,5,main] start
Thread.currentThread(): Thread[Thread-8,5,main] start
SuperClass init!
Thread.currentThread(): Thread[Thread-0,5,main] excuting...
Thread.currentThread(): Thread[Thread-9,5,main] end
Thread.currentThread(): Thread[Thread-3,5,main] end
Thread.currentThread(): Thread[Thread-6,5,main] end
Thread.currentThread(): Thread[Thread-7,5,main] end
Thread.currentThread(): Thread[Thread-0,5,main] end
Thread.currentThread(): Thread[Thread-5,5,main] end
Thread.currentThread(): Thread[Thread-4,5,main] end
Thread.currentThread(): Thread[Thread-8,5,main] end
Thread.currentThread(): Thread[Thread-1,5,main] end
Thread.currentThread(): Thread[Thread-2,5,main] end
*/ 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70881.html

相关文章

  • JVM加载思维导图

    摘要:用一张思维导图尽可能囊括一下的类加载过程的全流程。本文参考自来自周志明深入理解虚拟机第版,拓展内容建议读者可以阅读下这本书。 用一张思维导图尽可能囊括一下JVM的类加载过程的全流程。 本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。 showImg(http://ocxhn1mzz.bkt.clouddn.com/class%20loadin...

    Crazy_Coder 评论0 收藏0
  • 你所不知道的HelloWorld背后的原理

    摘要:今日最佳对于程序员而言,所谓的二八定律指的是花百分之八十的时间去学习日常研发中不常见的那百分之二十的原理。 【今日最佳】对于程序员而言,所谓的二八定律指的是 花百分之八十的时间去学习日常研发中不常见的那百分之二十的原理。 据说阿里某程序员对书法十分感兴趣,退休后决定在这方面有所建树。于是花重金购买了上等的文房四宝。 一日,饭后突生雅兴,一番磨墨拟纸,并点上了上好的檀香,颇有王羲之风范,...

    lavor 评论0 收藏0
  • Java的加载机制

    摘要:如果需要支持类的动态加载或需要对编译后的字节码文件进行解密操作等,就需要与类加载器打交道了。双亲委派模型,双亲委派模型,约定类加载器的加载机制。任何之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由调用。 jvm系列 垃圾回收基础 JVM的编译策略 GC的三大基础算法 GC的三大高级算法 GC策略的评价指标 JVM信息查看 GC通用日志解读 jvm的card table数据...

    aervon 评论0 收藏0
  • JVM实战---加载过程

    任何程序都需要加载到内存才能与CPU进行交流 同理, 字节码.class文件同样需要加载到内存中,才可以实例化类 ClassLoader的使命就是提前加载.class 类文件到内存中 在加载类时,使用的是Parents Delegation Model(溯源委派加载模型) Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化 showImg(https://s...

    bladefury 评论0 收藏0
  • 加载机制,双亲委派模型,搞定大厂高频面试题

    摘要:验证验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。字节码验证通过数据流和控制流分析,确定程序语义是合法的符合逻辑的。 看过这篇文章,大厂面试你「双亲委派模型」,硬气的说一句,你怕啥? 读该文章姿势 打开手头的 IDE,按照文章内容及思路进行代码跟踪与思考 手头没有 IDE,先收藏,回头看 (万一哪次面试问...

    Object 评论0 收藏0

发表评论

0条评论

zhangyucha0

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<