JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,如下图:
由于本文主要讲解的是类的 加载 部分,所以加载,验证,准备,解析,初始化仅仅作下简单的回顾,详细内容参阅《深入理解Java虚拟机》
加载
类的加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序使用任何类时,系统都会为之创建一个java.lang.Class对象。
类也是一种对象,就像概念主要用于定义和描述其他的事物(反射中class含有各种信息),但概念本身也是一种事物。
通常有下面几种来源 加载 类的二进制数据
- 从本地文件系统加载class文件
- 从jar包中加载class文件,如从F盘动态加载jdbc的mysql驱动。
- 通过网络加载(典型应用Applet)
- 把一个java源文件动态编译并加载
- 从zip包读取,如jar,war,ear。
- 运算时计算生成(动态代理技术)
- 数据库中读取(可以加密处理)
- 其他文件生成(jsp文件生成对应的class文件)
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段开始为 类变量(即static变量) 分配内存并设置 类变量的初始值(注意初始值一般为0,null也是零值)的 阶段,这些变量所使用的内存都将在方法区进行分配。
注意
这里进行内存分配的仅包含 《类变量》即static变量,而不包括实例变量,不包括实例变量,不包括实例变量,重要的事说3遍,实例变量将会在 对象实例化时随着对象一起分配在Java堆中。且初始值 通常情况 下 是数据类型 的零值
public class Animal{ private static int age = 20; }
那变量age在 准备阶段 过后的初始值是0而不是20,因为这时候还没有执行任何Java方法。所以把age赋值为20的动作将在 初始化 阶段执行。
再次注意
存在一种特殊情况,如果上面的类变量声明为final的,则此时(准备阶段)就会被初始化为20。
public static final int age = 20;
编译时候,JavaC将会为age生成ConstantValue属性,在准备阶段 虚拟机就会根据ConstantValue的设置将age赋值为20。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用(内存地址)的过程。
这里简单说下常量池
常量池 | |
---|---|
1. 字面量 | 比较接近Java语言层面,如String字符串,声明final的常量等 |
2. 符号引用 | 属于编译原理方面的概念:1.包括类和接口的全限定名 2.字段的名称和描述符3.方法的名称和描述符 |
符号引用大概是下面几种 类型
1. CONSTANT_Class_info 2. CONSTANT_Field_info 3. CONSTANT_Method_info
的常量。
初始化
类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)
这里需要区分下<init> 和<client>
- <init>指的是实例构造器,也就是构造函数
- <client>指的是类构造器,这个构造器是jvm自动合并生成的。
它合并static变量的赋值操作(1. 注意是赋值操作,仅声明的不会触发<client>,毕竟前面准备阶段已经默认赋过值为0了,2. final static的也是这样哦)和static{ }语句块生成,且虚拟机保证<client>执行前,父类的<client>已经执行完毕,所以说父类如果定义static块的话,一定比子类先执行,当然了,如果一个类或接口中没有static变量的赋值操作和static{ }语句块,那么<client>也不会被JVM生成。最后还要注意一点,static变量的赋值操作和static{}语句块合并的顺序是由语句在源文件中出现的顺序所决定的。
静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,前面的静态语句块只能赋值,不能访问。
类初始化的时机
当Java程序 首次主动通过下面6种方式使用某个类或接口时候,系统就会初始化该类或接口,假如这个类还没有被 加载和连接,则程序先加载并连接该类。类的初始化只会发生一次,再次使用new,callMethod等等都不会重复初始化。
- 生成类的实例,如(1)new (2)反射newInstance (3)序列化生成obj
- 调用static的方法,如LogUtil.i(TAG,"fucking");
- 访问类或接口的 static变量,或者为static变量赋值。注意有特例(一会说明)。
- Class.forName(name);
- 初始化某个类的子类,子类的所有父类都被初始化
- java.exe 运行Main类(public static void main),jvm会先初始化该主类。
刚才说 3 有一个特例,需要特别指出,仍然是static的变量,前面说过,如果是 static final类型的则会在准备阶段 就给赋值并加入常量池。所以仅仅访问某个类的常量并不会导致该类初始化。
class Person{ public static int age = 20; static { System.out.println("静态初始化!"); } } public class Test { public static void main(String args[]){ System.out.println(Person.age); } }
没有final修饰的情况打印
静态初始化!
20
加上final修饰后打印
20
以下是不会执行类初始化的几种情况
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 就是上面说的那种情况,类A引用类B的static final常量不会导致类B初始化。
- 通过类名获取Class对象,不会触发类的初始化。如
System.out.println(Person.class);
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
JVM类加载机制算是结尾了,不过在参考其他文章时候发现一个非常棒的例子,可以很好的验证上面的结论。
出处是 简书:小腊月
public class Singleton{ private static Singleton singleton = new Singleton(); public static int counter1; public static int counter2 =0; private Singleton(){ counter1++; counter2++; } public static Singleton getSingleton(){ return singleton; } } public class Main{ public static void main(String args[]){ Singleton singleton = Singleton.getSingleton(); System.out.println("counter1="+singleton.counter1); System.out.println("counter2="+singleton.counter2); } }
根据 类初始化的时机 所作的结论
- 执行Main方法,根据结论6,会首先初始化Main类,Main类从(加载开始 ----> 初始化结束)
- 执行到Singleton.getSingleton();时候,根据结论2,直接 【先】 触发【类的初始化】初始化Singleton类,Singleton类首次初始化,所以从 加载部分开始执行,执行到 准备阶段 所有static变量都被设置为初始值。此时
public static int counter1 = 0; public static int counter2 = 0; private static Singleton singleton = null;
Singleton执行到初始化阶段,生成类构造器<client>,类构造器会合并 static变量的赋值操作和 static语句块。合并后执行
public static int counter1 ;// 由于 counter1没被赋值,所以不会被合并进去
public void client() {// 伪代码:<client>方法体内容 Test singleton = new Test();//(1) int counter2 = 0;// (2) }
4.**初始化阶段**执行client内代码,执行到(1)处,此时counter1和counter2都变为1。5.**初始化阶段**执行client代码,执行到(2)处,counter2又被设置为0。6.**初始化结束**,回到Main方法的Singleton.getSingleton();继续执行main方法,最后输出结束。最后打印结果为:
counter1= 1
counter2= 0
## 详解类加载(第一阶段) 类加载部分是我们能够操作的部分,其他部分不需要我们管理。 Jvm启动时候默认至少开启了3个类加载器,分别是Bootstrap ClassLoader,Extension ClassLoader,Application ClassLoader各自加载各自管辖的区域。 ![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-3f7c2473524f8307.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。 2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOMElibext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 3. 应用程序类加载器(Application ClassLoader)或者叫**System ClassLoader**:负责加载用户路径(classpath)上的类库。 ![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-c86d07145d0bfadf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) >此程序说明:Java中getClassLoader用的一般和getSystemClassLoader是一个实例。源码中ClassLoader默认的构造器也说明这点,initSystemClassLoader里面会获取sun.misc.Launcher.getLauncher().getClassLoader()作为默认的parent。 **这里重点强调**:Android的ClassLoader类也有一个getSystemClassLoader()方法,但是又被改写了,后面再说明这个问题。