13、类加载器- 初始化和实例化

JVM主要包含三大核心部分:运行时数据区,类加载器和执行引擎。
jvm把字节码文件加载到内存,通过验证、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载过程:
1、加载loading
通过一个类的全名(包.类路径)来获取此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
[color=green]在java堆中生成一个代表这个类的java.lang.Class对象[/color],方法区保存数据的访问入口。(说明类和实例都是放在heap中)
若类没有进行过初始化,先进行初始化。
[img]http://dl2.iteye.com/upload/attachment/0106/2524/3c19debf-fe36-3890-982a-6b34e5096c3d.jpg[/img]
一个类的整个生命周期如下:
[img]http://dl2.iteye.com/upload/attachment/0106/2528/b120d574-c6c2-3422-9f6e-5437f1bd649f.png[/img]
加载阶段与连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
站在Java虚拟机的角度讲,只存在两种不同的类加载:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(只限于Hot Spot,而MPR、Maxine等虚拟机 是由Java语言实现的),是虚拟机自身的一部分;另外一种用户自定义类装载器。前者是Java虚拟机实现的一部分,后者是Java程序的一部分。由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中。

用户自定义的类装载器是普通的Java对象,它的类必须继承java.lang.ClassLoader类。ClassLoader中定义的方法为程序为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其它对象一样,用户自定义的类装载器以有Class类的实例都放在内存中的堆区,而装载的类型信息则都放在方法区。

2验证verification
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并却不会危害虚拟机自身的安全,一些在编译层面上可以控制的事情(比如超边界访问数组,跨类型进行类型对象转换存在时,编译器是拒绝工作的)可以通过直接修改class文件的方式进行破解,这就是验证阶段存在的原因。
按照虚拟机规范,如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang.VerifyError异常或其子类异常。
验证大致分成4个阶段的验证过程:文件格式验证、元数据验证、字节码验证和符号引用验证
文件格式验证:
比如是否以魔数开头,主次版本号是否在虚拟机可处理范围之内,常量池是否有不支持类型等。
class文件并不一定用java编译器生成,直接用16进制编写器也可以编写一个class文件;
经过这个阶段的验证之后,字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。

元数据验证:
这一阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求,这个阶段可能包括的验证点有:
这个类是否有父类;
父类是否继承了final类;
如果这个类不是抽象类,是否实现了父类或接口中需要实现的所有方法;
类中的字段和父类是否有矛盾(实现的方法而返回类型不同)

字节码验证:
字节码验证是验证中最复杂的一个阶段,主要工作是对 数据流和控制流 分析,对类的方法体校验分析,保证该方法在运行时不会做出危害JVM安全的行为;
通过了字节码验证并不能保证一定安全,程序效验无法通过程序去检查程序是否能在有限的时间内结束:"halt problem" 停机问题;
1.6加入StackMapTable功能对这个阶段做了优化,提高速度,但这个StackMapTable也可能被篡改,可以通过参数-XX:-UseSplitVerifier来关闭这个选项。或者-XX:+FailOverVerfier在类型效验失败时退回到旧的类型推导方式进行效验;
在jdk1.7之后对应主版本号大于50的class文件,不再允许回退;

符号引用验证:
对引用的类是否存在
对引用的类、属性、方法的访问权限验证

对于jvm的类加载机制来说,验证是一个非常重要、但是不是必须的阶段,若(包括自己写的代码)代码经过反复使用和验证,可以考虑使用-Xverify:none来关闭验证;以缩短类加载时间

3准备preparation
准备阶段是为类(static)变量在方法区内分配内存 。这个时候内存分配的仅包括类变量(static变量),不包括实例变量,[color=green]实例变量将会在对象实例化时随着对象一起分配在java堆中;静态方法和方法也是在准备阶段分配的,分配到方法区[/color];
[color=red]方法存放在方法区,运行在jvm栈或本地方法栈![/color]
注:static int a = 32;
在准备阶段a的值为零值,是没有进行赋值的,在初始化赋值或在其他实例方法中赋值。


4解析resolution(转换解析)
[color=green]解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。[/color]
符号引用在CLASS文件中它以CONSTANT_CLASS_INFO, CONSTANT_FIELDREF_INTO, CONSTANT_METHODREF_INFO等类型的常量出现
符号引用:(Symbolic References)符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,引用的目标并不一定已经加载到内存中,与虚拟机内存布局无关。
直接引用:(Direct References)直接引用可以是直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。与虚拟机内存布局相关。

虚拟机规范并未规定解析阶段发生的具体时间,只要求了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokedynamic
invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic这16个操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析。所以jvm需要判断是在类加载器link时解析还是使用时解析。

[color=green]对同一个符号引用进行多次解析请求是很常见的事情。[/color]出invokedynamic指令以外,把已经解析过的class标记为已解析状态,避免多次解析;invokedynamic指令用于动态语言支持,动态语言,必须等到程序执行invokedynamic指令时,执行解析,不管有没有标记为已解析状态。
解析动作主要针对 类和接口,字段,类方法,接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。分别对应于常量池的CONSTANT_CLASS_INFO,CONSTANT_FIELDREF_INFO,CONSTANT_METHODREF_INFO,CONSTANT_INTERFACEMETHODREF_INFO、CONSTANT_MethodType_info、CONSTANT_MethodHanle_info和CONSTANT_InvokeDynamic_info7种类型。

对 类和接口的解析:假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用(未标记为已解析状态的类或接口),虚拟机完成解析的过程需要以下三个步骤:
a、如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
b、如果C是数组类型,并且数组的元素类型是对象,则按照1的情况处理。如果元素类型不是对象,则由虚拟机生成一个代表此数组维度和元素的数组对象。
c、如果上述步骤没有异常,C在虚拟机中已经生成一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限,如果没有则抛出java.lang.IllegalAccessError异常。

对 字段的解析:先找到字段所属的类或借口,将所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:
1.如果C本身包含这个字段,就返回这个字段的直接引用,查找结束
2.否则,如果C中实现了接口,将会按 照继承关系从下往上递归搜索 各个接口和它的父接口,如果匹配,则返回这个字段的直接引用,查找结束
3.否则,如果C中继承了父类,将会按 照继承关系从上往下递归搜索父类,如果匹配,返回字段的直接引用,查找结束
4.查找失败,抛出java.lang.NoSuchFieldError异常。
5.如果查找过程成功返回应用,则还需要对该字段进行权限验证,不通过,则抛出java.lang.IllegalAccessError异常

对 类方法的解析:第一个步骤与字段解析一样,同样是需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用:
1.对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。
2.类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,则抛出java.lang.IncompatibleClassChangeError。
Java代码 收藏代码
interface A{
static void fun();
Demo1.java:3: 错误: 此处不允许使用修饰符static ,在ide中编写时报错,javac编译时报错,如果用其他工具生成的class文件会在运行时报错
3.在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
4.否则,在C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
5.否则,在C实现的接口列表及它们的父接口中递归的查找是否有简单名称和描述符都与目标相匹配的方法,如果有说明C是个抽象类,查找结束,抛出java.lang.AbstractMethodError异常。
否则,查找失败,抛出java.lang.NoSuchMethodError异常。
如果查找返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对这个方法的访问权限,则抛出java.lang.IllegalAccessError异常。

对 接口方法的解析 :
1.对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的接口。
2.如果在接口方法表中发现class_index中索引的C是个类,则抛出java.lang.IncompatibleClassChangeError。
3.否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
4.否则,在接口C的父接口中递归查找,直到java.lang.Object类(包括在内),看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
5.否则,查找失败,抛出java.lang.NoSuchMethodError。

5初始化
初始化是类加载过程的最后一步,初始化阶段才真正开始执行类文件中定义的JAVA程序代码,[color=cyan]准备阶段中,类变量已经得到内存分配,[/color]
[color=green]而在初始化阶段,主要执行静态块里面的内容和完成对类变量赋值;[/color]
从另一角度:[color=green]初始化阶段是执行类初始化<clinit>()方法的过程[/color](个人理解<clinit>()为初始化方法,<init>()为实例化方法);
1、 [color=green]<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的[/color],编译器收集的顺序和语句在源文件中出现的顺序一致,静态语句块中只能访问到定义在它之前的类变量,定义在它之后的变量,只能赋值,不能访问
2、 <clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证父类的<clinit>()在执行子类的<clinit>()方法之前完毕。因此,虚拟机执行的第一个<clinit>()方法肯定是java.lang.Object.
[color=green] 由于父类<clinit>()方法先执行,也就意味着父类中定义的静态语句要优先于子类的类变量赋值操作(父类的初始化方法先于子类初始化方法,初始化方法包括对类变量的赋值)。[/color]
3、<clinit>()方法不需要执行[color=red]父接口[/color]的<clinit>()方法。[color=red]只有当父几口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。[/color]
4、 [color=green]虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,[/color]如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽(这也是和实例化有明显区别的地方)。


双亲委派模型
从jvm的角度来看,只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另一种是JAVA类继承抽象类java.lang.ClassLoader的类加载器,独立于JVM;
双亲是指一个父类和一个子类,没有其他继承或实现的关系。
双亲委派的工作过程(java模块类加载器 有讲 这里复习下):当一个类加载器受到类加载请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给自己的父类(层层向上委派,直到启动类加载器),当父加载器无法完成这个加载请求时,子加载器才会尝试自己去加载(自下向上请求,自上向下加载)。父类能加载就有父类加载,父类不能加载就由子类尝试加载(可能该子类还是不能完成加载,就由他的子类继续尝试加载);
可以看出类加载器是有层次的,Bootstrap 是最先开始也是必须启动的,而java.lang.Object也是最先初始化的,所以可以把Objec放在Bootstrap启动的rt.jar中;
同时sun.misc.Launcher$ExtClassLoader 和sun.misc.Launcher$AppClassLoader都在这个rt.jar包中;这样,每个类加载时都会首先加载jre/lib里面的jar文件包括rt.jar。
扩张类加载器和系统类加载器都是继承java.lang.ClassLoader这个类:系统类加载器与扩张类加载器以及启动类加载器之间的父子关系不以继承来实现,使用组合关系来复用父加载器的代码。
查看loadClass方法,调用了findLoadedClass方法,这个findLoadedClass会调用JNI方法;

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 如果加载过则能通过类名在堆内存中找到java.lang.Class对应的实例;若没有被类加载器加载过则返回空;
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//判断该类加载器不为启动类加载器,这个parent是指类加载器之间的父子关系,而不是这里的继承关系
if (parent != null) {
//表示没有被加载的类有父类加载器去启动加载,该方法会一直向上委派到启动类加载器;
c = parent.loadClass(name, false);
} else {
//没有被加载的类由启动类加载器去执行加载,可能返回为空
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//父加载类不能实现加载,抛出异常
//c的值还是为null
}
if (c == null) {
//父类不能实现加载,由自己尝试加载
long t1 = System.nanoTime();
c = findClass(name); //这个方法在URLClassLoader中定义

双亲委派模型保证java程序稳定运行;保证某个范围的类一定是被某个类加载器所加载的,从安全层面上,杜绝通过使用JRE相同的类名冒充现有JRE的类 替换攻击。

初始化和实例化
[color=green] 类的初始化和对象实例化是 JVM 管理的类型生命周期中非常重要的两个环节,[这里说的是类初始化和对象实例化,显然这是两个不同的概念,只是他们在大多数情况下是紧密执行的];[/color]

初始化(initialization),初始化的5个启动点:
1.new关键字创建实例(对象实例化会启动初始化过程)
2.反射方法实例化对象(实例化)
3.初始化子类,先初始化父类
4.初始化主类(主类一般不会实例化)
5.其他指令或句柄如:getstatic、putstatic、invokestatic、REF_getStatic、REF_putStatic、REF_invokeStatic。
注:初始化接口并不需要初始化它的父接口。
jvm的启动不会主动进行类初始化,除了主类;

public class Demos1 {
static int a=45;
static {
System.out.println("i'm static block----"+a);
}
public static void main(String[] args) {
System.out.println("i'm main funtion----"+a);
}
}}
输出:
i'm static block----45
i'm main funtion----45

可以理解为,初始化就是为静态块、静态属性、静态方法分配空间的过程,从类的生命周期图看,他们是先于实例化(构造方法)执行的;
注:静态属性是要先于静态块执行的 储存在静态数据区,静态方法、方法也是在准备阶段存放在method area的;
所以当new一个对象实例的时候,先启动类的初始化过程(静态属性优先静态块执行),而不是先执行构造方法;


class Son{
static{
System.out.println("ok");
}
static int a = 34;
}
public class Demos1 {
public static void main(String[] args) {
System.out.println(Son.a);
}
}
输出:
ok
34

在这段代码中,调用了Son的属性,这个过程会对Son进行初始化,所以就会先执行Son的初始化过程,后返回; 所以先打印ok而不是34;
注:Son.class不会执行初始化方法;


class Father{
static {
System.out.println("父类的初始化方法");
}
static int b = 35;
}
class Son extends Father{
static int a = 34;
static{
System.out.println("ok");
}

}
public class Demos1 {
public static void main(String[] args) {
System.out.println(Son.b);
}
}
输出:
父类的初始化方法
35

[color=green]上段代码,没有输出ok,由于先要对父类进行初始化,先执行b的赋值和打印"父类的初始化方法",完成之后发现b是父类的属性,就不再需要对子类Son初始化了,等以后需要初始化Son时再初始化[/color],如: System.out.println(Son.a);执行Son的初始化方法;


class Father{
static {
System.out.println("父类的初始化方法");
}
static final int b = 35;
}
class Son extends Father{
static int a = 34;
static{
System.out.println("ok");
}
}
public class Demos1 {
public static void main(String[] args) {
System.out.println(Son.b);
}
}
输出:
35

这段代码连父类的初始化方法都没有执行, 这又是为什么呢?
[color=red]因为jvm将final声明的常量,放入到NonInitialization类的常量池中去了,它是属于NonInitialization的属性[/color],与Father和Son活生生的脱离了关系,调用Son.b时指向了NonInitialization的这个属性,而不再执行类初始化方法;加上System.out.println(Son.a);可以得到验证:这一步才执行Father的初始化方法;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值