深入理解jvm — 类加载篇

概述

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

Java语言里,类型的加载、连接和初始化都是在运行期间完成,使得java更灵活


类加载时机

类从加载进内存到卸载出内存,它整个的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Intialization)、使用(Using)、卸载(Unloading)7个阶段

其中验证、准备、解析3个部分统称为链接(Linking)

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的。为了支持java语言的运行时绑定,解析阶段可以在初始化之后再开始


初始化进行的时机:

1、遇到new、getstatic、putstatic、invokestatic这4条字节码执行时,如果类没有进行初始化,则需要先触发其初始化。
生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段、调用类的静态方法

2、使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化。

3、当初始化一个类时,如果发现其父类还没有进行初始化,则需要先触发其父类初始化。

4、当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5、当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结构REF_getStatic、REF_putStatic、REF_invokeStaice的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则先触发其初始化。

有且只有上面5种情况会触发类的初始化。这5种场景中的行为称为对一个类的主动引用。除此之外,所有引用类的方式都不会触发初始化,成为被动引用。

被动引用的例子:

public class SuperClass{
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}


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

执行:

System.out.println(SubClass.value);

输出结果:
SuperClass init!

结论:
对于静态字段,只有直接定义这个字段的类才会被初始化。
通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
至于是否哟啊触发子类的加载和验证,在虚拟机规范中并没有明确规定,这点取决于虚拟机的具体实现。

还是上面的例子执行:

SuperClass[] sca = new SuperClass[10];

没有输出语句

结论:
newarray会触发虚拟机生成一个中间类,这个类封装了数组元素的访问方法,当检数组越界时,会抛出java.lang.ArrayIndexOutOfBoundsExcepetion异常

public class ConstClass{
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world"
}

执行:

System.out.println(ConstClass.HELLOWORLD);

没有输出语句

结论:
在编译阶段已经将此常量的值”hello world”存储到了NotInitialization类的常量池中,并且NotInitialization类中并没有ConstClass符号的引用入口。

接口初始化的时机和类不同点是,前面的5种有且仅有的初始化条件中第三种,当初始化一个类时,不要求其父接口全部初始化。只有正真使用到接口时,才会初始化


类的加载过程
我们接下来看一下Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析、初始化这5个阶段

1、加载
“加载”是“类加载”过程的一个阶段,不用弄混,在加载阶段虚拟机需要完成以下3件事:
1) 通过一个类的全限定名来获取定义此类的二进制字节流
2) 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3) 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口

其中第2点,二进制字节流字节流要从一个Class文件中获取,但是虚拟机规范中并没有指明要从哪里获取,怎样获取。这使得java的扩展性更好,比如
可以从ZIP包中获取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
从网络中获取,典型例子就是applet
运行时计算生成,比如动态代理
由其他文件生成,比如jsp应用,即又jsp文件生成对应的Class类
从数据库中读取等等

相对于类加载的其他阶段,一个非数组类,获取类的二进制字节流阶段, 是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可由用户自定义的类加载器完成(即重写一个类加载器的loadClass() 方法)

数组类的加载和类不同,数组类本身不是通过类加载器创建,它是由java虚拟机直接创建的。数组类的元素类型是类加载器创建的

加载阶段和连接阶段顺序固定,但是是交叉进行的

2、验证
验证是连接阶段的第一步,是对虚拟机自身的保护。java是相对安全的语言,它不能访问到越界的数组元素,将对象转换为错误的类型,跳转到不存在的代码行。Class文件不一定是Java源码编译来的,有可能是十六进制编辑器直接编写的,等等。所以在字节码语言层面上,java代码无法做到的事情都是可以实现的。如果jvm不检查输入的字节流,可能会导致系统崩溃。

验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1) 文件格式验证
验证是否以魔数开头
主次版本号是否在虚拟机处理范围之内
常量池中是否有不被支持的类型
Class文件中各个部分及文件本身是否有被删除或附加的其他信息
……
主要保证输入的字节流能正确的解析并存储于方法区中,这个阶段的验证是基于二进制字节流,只有通过这阶段的验证,字节流才会进入内存的方法区中进行存储,后面的3个验证全都是基于方法区的存储结构进行的,不会再直接操作字节流。

2) 元数据验证
验证类是否有父类(除了java.lang.Object之外,所有类都有父类
父类是否继承了不允许继承的类(final 修饰的类)
如果这个类不是抽象类,是否实现了父类或接口中要求实现的所有方法
……
主要是对字节码描述的信息进行语义分析,保证其符合java语言规范要求

3) 字节码验证
保证跳转指令不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换是有效的
……
主要是对类的方法体进行校验,保证类的方法在运行时不会做出危害虚拟机安全的事情

4) 符号引用验证
验证符号引用中通过字符串描述的全限定名是否能找到对应的类
符号引用中的类、字段、方法的访问性,是否可以被当前类访问
……
主要是确保解析动作的正常进行。

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段。如果运行的全部代码已经被反复使用和验证过,那么在实施阶段就可以考虑关闭大部分的类验证,以缩短虚拟机类加载的时间

3、准备
准备阶段是正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(实例变量会在对象实例化时随着对象一起分配在java堆中)

这里说的初始值,通常是数据类型的零值,例如:
public static int value = 123;
那变量values在准备阶段过后的初始值为0,而不是123,把value赋值为123的动作在初始化阶段才会进行,因为putstatic指令是程序编译后,存放在类构造器()方法中。

特殊情况:如果类字段的字段属性存在ConstantValue属性,那么在准备阶段变量value就会被初始化为ConstantValues属性所指定的值,例如
public static final int value = 123;
编译时javac将会value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValues的设置将value赋值为123

4、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(symbolic References):用一组符号来描述所引用的目标,符号可以说任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用(Direct References): 可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。如果有了直接引用,那引用目标必定已经在内存中存在。

1) 类或接口的解析
假设当前代码所处的类为D,如果把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么解析过程为:
如果C不是一个数组类型,那虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C,包括会触发相关类的加载。
如果C是一个数组类型,并且数组元素类型为对象,那将会按照第1点的规则去加载数组元素类型。接着有虚拟机生成一个代表次数组维度和元素的数组对象
如果上面的步骤没有出现任何异常,那么C在虚拟机中已经成为一个有效的类或接口了,但是在解析完成之前嗨哟奥惊醒符号引用验证,确认D是否具备对C的访问全限。如果没有就会抛出java.lang.IllegalAccessError异常

2) 字段解析
3) 类方法解析
4) 接口方法解析

5、初始化
类初始化时是类加载过程的最后一步,这步才开始正式执行类中定义的java程序代码。
初始化是执行类构造器() 方法的过程。() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。


类加载器
类加载器,通过一个类的全限定名来获取描述此类的二进制字节流,的代码模块

1、类与类加载器
两个类“相等”的条件是,同一个列并且由同一个类加载器加载。
这里所说的相等时指,类对象的equlas()方法、isAssignableFrom()方法、对象所属关系的isInstance()方法的返回结果。

2、双亲委派模型
3种系统提供的类加载器:
1) 启动类加载器(Bootstrap ClassLoader):负责将JAVA_HOME\lib 目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar)类库加载到虚拟机内存中。用户在编写自定义的类加载器时,如果需要把加载请求委派为引导类加载器,那就直接用null代替。

2) 扩展类加载器(Extension ClassLoader): 这个加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载JAVA_HOME\lib\ext目录中的,或者被java.ext.dirs系统变量所指定路径中所有类,开发者可以直接使用扩展类加载器。

3)应用程序加载器(Application ClassLoader): 这个加载器由sun.misc.Launcher$AppExtClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader() 方法的返回值,所以一般也成它为系统类加载器。可以自定义。

应用程序都是有这3中类加载器互相配合加载,加载器之间的这种组合(不是继承)的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)

原理:
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终应该传送到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求时,子类才会尝试加载。

优点:
对于同一类,会交个用一个类加载器加载,不会造成类的混乱。

实现:
实现非常简单,所有代码都集中在java.lang.ClassLoader的loadClass()方法中

protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException
    {
            // 首先,检查请求的类是否已经被加载过了
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器抛出ClassNotFoundException
                    // 说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 当父类加载器无法加载时
                    // 再调用本身的findClass方法进行类加载
                    c = findClass(name);
                }
            }

            if (resolve) {
                resolveClass(c);
            }
            return c;
    }

注意:
即使自定义了自己的类加载器,强行用defineClass()去加载一个以“java.lang”开头的类也不会成功。如果这样尝试的话,虚拟机会抛出一个“java.lang.SecurityException:Prohibited package name:java.lang”异常

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值