类加载机制

虚拟机加载机制

虚拟机将描述类从class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成能被java虚拟机直接使用的java类型。

在Java中类型的加载、连接、初始化都是在程序运行期间进行的,这样提高了程序的灵活性,但是类加载时会增加性能开销。

类加载的流程

加载->连接(验证、准备、解析)->初始化->使用->卸载

必须触发初始化的5种情况

1、遇到new、getstatic、putstatic或invokestatic指定时,若类还没有进行初始化,则先触发初始化。上述4条指令常见场景有:使用new关键字创建对象时;读取或者设置静态字段(final修饰、已在编译器放入常量池的静态字段除外);调用一个类的静态方法时。

2、在使用java.lang.reflect进行反射调用的时候,若类未进行初始化,则对该类进行初始化。因为反射时在运行时调用的,若需要使用类的属性及方法必须先初始化。

3、若初始化一个类,若果其父类未初始化,则需要先对其父类进行初始化。

4、当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会优先对此类进行初始化。例如SpringBoot程序。

5、当使用JDK1.7的动态语言支持时,若一个类java.lang.invoke.MethodHandle实力最后解析结果为REF_getstatic、REF_putstatic、REF_invokeStatic的方法句柄,并且这个方法没有初始化,则需要触发其初始化。

加载

加载过程主要完成以下三个步骤:
1)通过一个类的完全限定名来获取该类的二进制字节流。
2)将这个类的字节流代表的静态存储结构转换成方法区的运行时数据。
3)在内存中生成一个代表该类的Class对象,作为方法区数据访问的入口。

对于第一句,“通过一个类的完全限定名来获取该类的二进制字节流”,在虚拟机规范中并未指定从哪里去获取。这个定义非常灵活,所以后面衍生了许多中获取方式,例如:

1、从zip压缩文件中获取。经典的有ear、jar、war等;

2、从网络中获取;

3、运行时动态生成。使用最多的就时动态代理技术。例如springaop中使用的cglib动态代理技术。在jdk中使用的java.lang.reflect.Proxy在运行时动态生成所需的代理类;

4、其它文件生成,譬如java中的页面技术JSP等等。

连接

连接时java类加载的第二部,它还分为下面几个小步,包括:验证、解析、准备;

验证

验证时连接步骤的第一步,主要是为了校验类的合法性,保证当前类必须符合当前虚拟机的规范,保证不对虚拟机造成危害。

验证主要分下面几步:
文件格式验证
验证加载完成的字节流是否符合虚拟机的class文件规范,比如:文件是否是以魔数开头的;

主次版本号是否在当前虚拟机的处理范围内;

数据格式是否符合虚拟机规范等;

元素据验证

验证当前加载的类是否有父类;验证该类的定义是否合法,比如有无继承被final修饰的类;

若此类不为抽象类,是否继承了抽象类,并有无实现全部的抽象方法。

验证该类的一些方法或则属性定义是否符合多态性的规则等等;

字节码验证

保障操作数栈的数据类型任何时刻都能与指令代码序列配合工作;

保证字节码跳转指令不会跳转到方法体之外的字节码指令上;

保证方法体中的类型转换是有效的。

符号引用验证

符号引用通过类的完全限制名是否能够找到对应的类

在指定的类中是否符合方法字段的描述和简单名称所描述的方法和名称

符号引用中的类、字段、方法的访问权限是否能够被当前类访问

准备

准备阶段是正式为类变量分配内存及设置初始值的时候,这些值都将分配在方法区中。而示例变量则是在随着对象初始化分配在堆中。该阶段为类变量分配的初始值都是为该变量对应数据类型的零值。例如:

private static int i = 123;

在准备阶段时,i的值则为0。

解析

解析阶段是将虚拟机常量池中的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可
以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的
内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各
不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义
在Java虚拟机规范的Class文件格式中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是
一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引
用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目
标必定已经在内存中存在。

在虚拟机中并未规范解析的发生具体时间。
只要求了在执行anewarray、
checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、
invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic这16个用于
操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。

初始化

类初始化阶段是类加载过程的最后一步,到了这个阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

注意点:

1、编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,而定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

2、.初始化方法执行的顺序,虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕,因此在虚拟机中第一个被执行的类初始化方法一定是java.lang.Object。另外,也意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

3、clinit ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。

4、口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口与类一样都会生成clinit()方法,但与类不同的是,执行接口的初始化方法之前,不需要先执行父接口的初始化方法。只有当父接口中定义的变量使用时,才会执行父接口的初始化方法。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。

5、虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行类初始化方法完毕。

类初始化顺序

类文件加载的顺序

1、先加载执行父类的静态变量及静态初始化块(执行先后顺序按排列的先后顺序)

2、再加载执行本类的静态变量及静态初始化块

只要类没有被销毁,静态变量及静态初始化块只会执行1次,后续再对该类进行其他操作也不会再执行这两个步骤。

类实例创建过程

只有在调用new方法时才会创建类的实例

1、按照上面类文件加载的顺序(类已被加载则跳过此步)

2、父类的非静态变量及非静态初始化块

3、父类的构造方法

4、本类的非静态变量及非静态初始化块

5、本类的构造方法

4、类实例销毁时候,首先销毁子类部分,再销毁父类部分

静态方法和非静态方法都是被动调用

即系统不会自动调用执行。所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。

类加载器

启动加载器

启动类加载器主要是加载jvm自身需要的类,他是有c++编写,主要加载JAVA_HOME/lib下的类库或者使用-Xbootclasspath参数指定的jar到内存中

扩展加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器

系统加载器

它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

双亲委派模型

即:当一个类加载器在收到一个类加载请求时,它不会自己先去加载该类,而是委托给父类去执行,每个加载器都是如此。所以所有的类加载请求都出传输到顶层的启动类加载器,只有当父类无法加载时(即作用范围无法找到该类),子加载器才会自己去加载。

优点:
java类随着它的类加载器一期具备了优先级的层次关系。在这种关系的作用下,并且保证了类加载的不可重复性。当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次则是保证java api的安全性。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值