您的位置:首页 > 编程语言 > Java开发

Java基础知识—虚拟机类加载机制

2015-08-20 01:22 519 查看

1.虚拟机类加载机制定义

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

2.类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、连接(验证、准备、解析)、初始化、使用、卸载这几个阶段。这几个阶段的发生顺序如

下图所示:



2.1加载

“加载”是“类加载”过程中的一个阶段,这两个看着相似的两个名词并不是一回事。类加载包含加载、连接、初始化三个阶段。那么在加载阶段,虚拟机会完成什么样的工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

常用的加载方式有两种:一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取。

对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。
加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是连接阶段。有一点需要注意,就是有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。

2.2 连接

连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
验证:它是连接阶段的第一步,这一阶段的只要目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会伤害虚拟机自身的安全。比如说:这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。
准备:准备阶段是正式为类变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存即将在方法区进行分配。这个阶段有两个容易产生混淆的概念。首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括视力变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况下”是数据类型的0值,假设一个类变量的定义为:public
static int value=123;那变量value在准备阶段的初始值为0,而不是123.
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。



2.3初始化

初始化时机:虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”:

(1)遇到new、getstatic、putstatic、或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先进行初始化。场景有:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候;
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;
(3)当初始化一个类的时候,如果发现其父类还没有进行初始化的,则需要先触发其初始化;
(4)虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会初始化那个类;
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;(有点懵)

除了以上五种情况是主动引用,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。下面来看一些例子(代码1—代码3)

代码1:

/**
* 被动使用类的静态字段,不会导致子类初始化
**/
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}

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

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


运行结果:

SuperClass init!
123

代码2:

/**
* 通过数组定义引用类,不会导致子类初始化
**/

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

public class NotInitialliazation {
public static void main(String[] args) {
SuperClass[] sca=new SuperClass[10];
}
}


代码3:

/**
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,
* 不会触发定义常量的类的初始化
**/

class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String NAME="messi";
}

public class NotInitialliazation {
public static void main(String[] args) {
System.out.println(ConstClass.NAME);
}
}
运行结果:

messi
不会在结果中出现ConstClass init!



2.4 使用

使用阶段包括主动引用和被动引用,在前面已经讲到,这里就不再赘述。



2.5卸载

类使用完之后,如果有下面的情况,类就会被卸载:
(1)该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
(2)加载该类的ClassLoader已经被回收。
(3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: