虚拟机类加载机制

Java类加载机制详解
本文详细介绍了Java类加载过程,包括加载、验证、准备、解析和初始化五个阶段,并阐述了类加载器的工作原理及双亲委派机制。

相关文章

java虚拟机之初探

java虚拟机之垃圾回收算法

当我们编写完代码后,通过编译器编译为Class文件才能被虚拟机识别而执行,而在执行这些Class文件的时候,需要把这些字节码加载到内存当中去,把Class文件加载到内存就称为类的加载机制

概念

虚拟机把类的数据从Class文件中加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型

类的生命周期

类从开始加载到内存到卸载出内存,它包括以下几个阶段:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading),其中,验证,准备,解析这三部分统称为连接(Linking)。加载,验证,准备,初始化和卸载的顺序是固定的,而解析则不一定(动态绑定)。

类的加载过程

1.加载

加载的步骤:

1.通过一个类的全限定名来获取该类的二进制流

2.将这个二进制流所代表的静态存储结构转换为方法区的运行时数据结构

3.在内存中创建一个Class对象,该Class对象就代表该类,这个Class对象将作为程序访问方法区中类型数据的外部接口

2.验证

验证是为了确保Class文件中的字节流信息符合当前java虚拟机的要求,且不会危害虚拟机的安全

验证大致分为四个阶段:文件格式校验,元数据校验,字节码校验,符号引用校验

1.文件格式检验

主要验证文件格式是否符合规范,如是否以0xCAFFBABE开头,版本号是否在当前虚拟机范围内,常量池中是否有不被支持的常量类型等。

该阶段主要是保证字节流能正确的解析并存入到方法区中,是基于字节流进行校验,

2.元数据校验

对字节码描述的信息进行语义分析,是基于方法区中的存储结构进行校验,不会操作字节流。如验证是否有父类,是否实现的抽象类或者接口的方法,类的字段,方法是否与父类冲突等。

3.字节码校验

主要是通过数据流和控制流确定程序语义是合法的,符合逻辑的;也是基于方法区中的存储结构进行校验,不会操作字节流。

4.符号引用校验

该阶段发生在虚拟机将符号引用转换为直接引用的时候,这个转换将发生解析阶段;如校验符号引用中通过字符串描述的全限定名是否可以找到对应的类,符号引用中的类,字段,方法的访问性是否能被当前类访问等。

3.准备

准备阶段正式为类变量分配内存,并为类变量设置初始值,这些变量使用的内存在方法区中进行分配,这个初始值是数据类型的初始值而不是在程序中设置的初始值,如下代码在准备阶段后的初始值是0,而不是100,把100 赋给类变量是在编译之后进行的。

public static int a = 100;

4.解析

该阶段是将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行。

类或接口的解析

public class Main {
    public Hello hello = new Hello();
}

如上代码,如果要把从未解析过的符号引用 hello 解析为类 Hello的直接引用,则会按照下列顺序执行:

(a)首先判断 hello 是不是数组,如果不是数组,则会把代表 hello 的全限定名传递给Main类的类加载器器加载 Hello 类。

(b)如果hello 是数组,数组中是hello类型,则按照(a)进行执行。

(c)对符号引用进行校验,确认 Main 类内是否具有对 Hello的访问权限。

5.初始化时机

类的初始化只能由下列这几种情况进行触发:

1.遇到new实例化对象,set/get类的静态属性,调用静态方法的时候,如果类没有进行初始化,则首先需要进行初始化。

2.使用 java.lang.reflect 反射的方式对类进行调用的时候,如果类没有进行初始化,则首先需要进行初始化

3.在初始化一个类的时候,如果其父类还没有进行初始化,则首先对父类进行初始化

4.当虚拟机在启动的时候,用户指定要执行的主类,虚拟机会首先初始化这个主类

只有以上几种情况才会触发一个类的初始化过程,而以下这些情况则不会触发类的初始化:

1.子类直接调用父类的静态属性,只有直接定义这个属性的类才会初始化,所以子类不会被初始化

2.通过数组来引用类,不会触发该类的初始化,仅仅是创建该类的一个数组,并没有使用该类,所以不会初始化,这个很好理解,

/**
 * Created by Lenovo on 2018/6/25.
 */
public class Hello
{
    static
    {
        System.out.println("hello init....");
    }
}

public class Main
{
    public static void main(String[] args)
    {
        Hello[] hellos = new Hello[2];
        // 不会输出 hello init....
    }
}

3.调用常量也不会触发类的初始化操作,因为常量在编译的时候就已经放入到常量池中去了,调用的时候,并没有直接引用到该类,所以也不会触发类的初始化操作。

/**
 * Created by Lenovo on 2018/6/25.
 */
public class Hello
{
    static
    {
        System.out.println("hello init....");
    }
    public static final String CONST_NAME = "Hello";
}
public class Main
{
    public static void main(String[] args)
    {
        System.out.println(Hello.CONST_NAME);
        // 只会输出 Hello, 
    }
}

    上述代码只会输出 Hello,也就是说Hello类并没有进行初始化,这是因为在编译该代码的时候,就已经将值 Hello 存入到 Main 类中的常量池中了,以后 Main 再对  Hello.CONST_NAME 进行操作,实际上就是对自身常量池进行操作,也就不会再有对 Hello 类的引用了,编译过后,这两个类不会再有任何的联系了。

在初始化阶段,才开始真正的执行java代码,该阶段就是执行类构造器<clinit>()方法的过程。

类构造器<clinit>()

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值操作和静态块中的语句合并而成,收集顺序和代码中的顺序一致,如下代码:

public class Main {

    public static int i = 1;
    static {
        i = 100;
        System.out.println(i);
    }
}

编译后的class文件通过 jclassbin 查看如下:

0c0d42f0326c265ad6373311d0349d8e97a.jpg

1.通过 iconst 指令把 1 压入栈:iconst 指令-->当取值为0-5时,通过iconst指令压入栈中,iconst_<i>  : Push int constant

2.通过putstatic指令为属性 i 设置值,此时值为1,putstatic指令--> Set static field in class

3.通过bipush指令把 100 压入栈中:bipush指令-->当int取值-128~127时,JVM采用bipush指令将常量压入栈中

4.再通过putstatic指令为属性 i 设置值,此时值为100

5.通过getstatic指令获取System.out,getstatic指令--> Get static field from class

5.通过getstatic指令获取 i

6.调用invokevirtual执行,invokevirtual指令-->Invoke instance method; dispatch based on class

静态块中只能访问到定义在静态块之前的变量,定义在之后的变量,只能在静态块中复赋值,不能访问:

 

7c614c68fe0a3f22791509ffcce24c2d234.jpg

如果一个类或接口没有静态属性或静态块,<clinit>()可以没有。

查看 JVM 相关指令:java 虚拟机指令

类加载器

java中共有四种类加载器,

1.启动类加载器,负责加载 lib 目录下的类,如rt.jar

2.扩展类加载器,负责加载 lib\ext目录下的类

3应用程序加载器,负责加载classpath上的类,也是程序中默认的类加载器

4.用户自定义加载器

java加载类使用的是双亲委派机制,也就是如果一个类加载器收到了类加载的请求,它首先不会去加载这个类,而是把这个加载请求委托给父类进行加载,每一层的类加载器都是如此,最终所有的加载请求都会被委托到顶层动类加载器进行加载,只有当父类的加载器不能加载的时候,子加载器才会自己去加载,模型图如下:

480e0f4eb6b8bb31b9dba9ac060a3fbb12a.jpg

优点就是java类随着它的加载器一起具备了带有优先级的层次关系,如Object类,都会被委托到启动类加载器进行加载,所以,在各种加载器环境中Object都是同一个类。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

 

 

转载于:https://my.oschina.net/mengyuankan/blog/1838416

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值