虚拟机的启动
- 启动
- java虚拟机的启动时通过引导类加载器(bootstrap class loader)创建的一个初始类来完成的,这个类是由虚拟机的具体实现指定的
类加载器的分类
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载器通过 C/C++语言实现,嵌套在JVM实现中
- 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar等),用于加载JVM自身运行所需要的内容
- 并不继承java.lang.ClassLoader,没有父类加载器
- 运行时会加载
扩展类加载器
和应用程序类加载器
(系统类加载器),并成为他们的父类加载器 - 出于安全考虑,启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- java语言实现,继承自java.lang.ClassLoader,具体由sum.misc.Launcher$ExtClassLoader实现(即ExtClassLoader是Launcher的内部类)
- 父类加载器为启动类加载器
- 会加载JAVA_HOME/jre/lib/ext/ 目录下的jar包,如果用户创建jar包放在此目录下,也会自动由扩展类加载器加载
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言实现,继承自java.lang.ClassLoader,具体由sum.misc.Launcher$AppClassLoader实现
- 父类加载器为扩展类加载器
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 一般来说,我们自己写的类都是由它来完成加载的,是程序中的默认的类加载器
- 可由ClassLoader.getSystemClassLoader()获取到
双亲委派机制
- 背景:java虚拟机堆class文件才有的是按需加载的方式,即当用到该类时才会将它的class文件加载到内存中,生成Class对象。且加载类的时候,采用的是双亲委派机制,即把请求交给父类处理,它是一种任务委派机制
- 工作原理
- 如果一个类加载器收到类的加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行
- 如果父类加载器还存在其父类加载器,则进一部向上委托,以此类推,加载请求最终将到达顶部的启动类加载器
- 如果父类加载器可以完成类加载,则成功返回,如果父类加载器无法加载任务,则子类加载器尝试自己去加载
- 优势:
- 避免了类的重复加载
- 保护程序安全,避免核心API被随意篡改
类的加载过程(这里的加载是广义上的加载)
- 加载:通过ClassLoader类加载器把字节码文件中加载到内存中
- 加载过程:
- 通过一个类的全限定名获取定义此类的二进制字节流(一般从磁盘中获取,加载到内存中)
- 将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构
- 在内存(具体位置是在堆)中生产一个代表这个类的java.lang.Class对象,作为访问 方法区中这个类的各种数据 的入口
- 补充:加载.class文件的方式
- 从本地系统(磁盘)中获取
- 从网络获取
- 从压缩包中获取,如jar、war格式的压缩包
- 运行时计算生成,使用的最多的是:动态代理技术
- 从加密文件中获取,典型的防Class文件呗反编译的保护措施
- 链接:
- 验证:验证二进制字节流是否符合Java虚拟机规范
- 准备:为类变量分配内存并为类变量设置默认初始值,即零值。(注意这时候类变量没有被真正初始化)
- 解析:将常量池的符号引用转换成直接引用的过程
- 初始化
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程:该方法不需要定义,是java编译器自动收集类中的所有类变量(static修饰的变量)的赋值动作和静态代码块的语句合并来的,且方法中的指令是按照语句在源文件中出现的顺序执行的 -
<clinit>()
方法用于初始化类,类的构造器(()方法)用于初始化对象 - JVM会保证子类的
<clinit>()
方法执行前,父类的<clinit>()
方法已经执行结束 - JVM会保证在多线程情况下类也只会被加载一次(通过加锁实现)(静态代码块也只会执行一次)
读者可能就会有一个疑惑:加载阶段就会把字节码文件对应二进制字节流转换为方法区的存储结构,并在堆区生成一个Class对象了,而随后开始的链接阶段的验证阶段才会对二进制字节流进行验证,这样怎么保证加载阶段不会出错?
《深入理解Java虚拟机》书中是这样解释的:类的加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,注意,这里写的是开始,而不是进行或者完成,强调这点是因为这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行的过程中调用、激活另一个阶段。
比如加载阶段在将静态的字节流转换为方法区的数据结构之前,会进行链接阶段的验证阶段,进行文件格式的验证,保证输入的字节流能正确地解析并存储于方法区
public class CinitTest {
static int x=5;
int y = 10; //编译器会把这个赋值动作放到<init>方法中
static {
System.out.println("静态代码块执行=========");
x=7;
System.out.println(x);//编译通过,打印x=7
flag=true; //因为在链接的准备阶段已经给flag分配内存且设置初始值,所以可以进行赋值
//System.out.println(flag);//但不能引用,因为前文没有定义flag,编译器报错Illegal forward reference 非法前向引用
}
static boolean flag;
public CinitTest() {
//<init>方法
System.out.println("<init>方法执行");
}
public static void main(String[] args) {
System.out.println("mian方法执行==========");
System.out.println(CinitTest.x);
System.out.println(CinitTest.flag);
CinitTest ct = new CinitTest();
System.out.println(ct.y);
}
}
以上面的CinitTest类为例说明类的加载顺序与执行过程
- 因为这个类是我们自己写的,所以会通过AppClassLoader类加载器把CinitTest.class通过二进制流的方法加载到内存中,并在方法区中生成该类的数据结构,在堆区中生成该对应的Class对象。
- 在链接阶段,先验证CinitTest.class的二进制流是否符合规范,在为CinitTest类的类变量x和flag分配内存,并设置初始值为x=0,flag=false
- 在类的初始化阶段,编译器会收集
类变量的赋值操作
和static代码块
的内容,按照语句出现的先后顺序合并生成<clinit>方法
并执行,CinitTest类生成的<clinit>方法
如下.
x=5;
System.out.println("静态代码块执行=========");
x=7;
System.out.println(x);
flag=true;
- 完成了上面的过程,类的加载与初始化完成,然后开始执行main方法。执行new CinitTest()方法的时候,会调用类的构造器方法,即
<init>方法
,在这里,因为我定义成员变量y的时候,并给y赋值了y
=10,这个赋值动作编译器会自动加到所有的构造器方法里,在调用构造器方法时,这个赋值动作才会执行