声明:该文章内容摘自于网络及《深入理解Java虚拟机》,本人只是进行了内容合并,并非个人文章,只为知识共享,内容有不正确的地方还请大神大牛们指出。
一类加载及加载器概述
java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
JVM中用来完成上述功能的具体实现就是类加载器。类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例,每个实例用来表示一个java类,通过该实例的newInstance()方法可以创建出一个该类的对象。
二类加载过程
类加载过程详述:
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
加载(装载):查找和导入Class文件
常见类加载时机:
1、new 2、java.lang.reflect反射 3、加载父类 4、加载主类(main) 5、获取运行时常量或者调用静态方法
在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分。
String str = new String("hello");
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
链接
链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
验证:检查载入Class文件数据的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备:给类的静态变量分配存储空间
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。
其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为: Public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。
解析:将符号引用转成直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:对类的静态变量,静态代码块执行初始化操作
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源,或者说:
初始化阶段是执行类构造器()方法的过程.()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
Java保证了一个对象被初始化前其父类也必须被初始化:Java强制要求任何类的构造函数中的第一句必须是调用父类构造函数或者是类中定义的其他构造函数。如果没有构造函数,系统添加默认的无参构造函数,如果我们的构造函数中没有显示的调用父类的构造函数,那么编译器自动生成一个父类的无参构造函数。
三类加载器
Java 有以下两个重要的设计特性:
[if !supportLists]1、[endif]Java 是与平台无关的;
[if !supportLists]2、[endif]Java 是建立在分布式网络中的架构。
为了实现这两个目标,Java 必须对如何加载代码库进行独特设计。要实现与平台无关性,那么Java 就不能依靠文件系统来加载自己的类库,因为有些嵌入式系统甚至没有自己的文件系统。由于其分布式特性,Java 需要设计为从网络地址中加载各种代码类,从文件系统中加载类将不能够工作。为了解决这个问题,Java 架构引入了类加载器的概念。类加载器的作用是从网络或硬盘中加载类,不依赖于操作系统。
在JVM当中预定义了三种类型的类加载器:启动类加载器(Bootstrap类加载器),扩展类加载器(Extension类加载器),系统类加载器(System类加载器)。每个加载器其实就是个类的对象。另外还有线程上下文类加载器和用户自定义类加载器。
3.1 启动类加载器(JVM的各提供商使用本地代码来实现Bootstrap类加载器)
启动类加载器(BootstrapClassLoader)引导类装入器是用本地代码实现的类装入器,它负责将 /lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
启动类加载器是最低层的加载器,它是由C++编写的,因为加载器是一个类,也需要通过类加载器加载,启动类加载器就能完成这个功能。
3.2 扩展类加载器
扩展类加载器(ExtensionClassLoader)是由ExtClassLoader类实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
3.3 系统类加载器
系统类加载器(SystemClassLoader)是由AppClassLoader类实现。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库
加载到内存中。还用于加载入口函数类(仅含有main()方法的类)。开发者可以直接使用系统类加载器。
四双亲委派机制
4.1 定义
JVM加载类时默认采用双亲委派 机制。
通俗的讲就是,当某个类加载器收到加载类的请求时,首先会将加载任务任务委托给加载器的父类,然后父类再委托给父类的父类,以此类推。如果父类成功可以成功加载,就返回成功,如果父类加载器无法完成任务时,才会自己加载。
具体的讲,当在应用程序中加载一个类时,JVM 自动请求System 类加载器,System 类加载器又会请求Extension类加载器来加载,Extension类加载器又会请求Bootstrap类加载器来加载。如果Bootstrap类加载器在核心库中不能查找到请求的类,则返回请求给Extension类加载器,由Extension 类加载器在自己的路径中查找,如果仍然找不到,则返回请求给System 类加载器,如果System 类加载器在CLASSPATH 中不能找到该类,则返回ClassNotFoundException异常。new String() 和 new MyObject() 的区别
下图展示了双亲委派机制的运行逻辑。
4.2 实现
ExtClassLoader和AppClassLoader都是继承于java.lang.ClassLoader类,ClassLoader类中有个loadClass方法来实现双亲委派,ExtClassLoader和AppClassLoader都没有重写这个方法(JDK1.8)
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先获得当前类的类加载器,返回加载器或null
Class c = findLoadedClass(name);
if (c == null) {
// 如果当前类没有被加载,就委托给父类加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器不存在,就检查是否有启动类加载器加载的类
// 通过调用本地方法native findBootstrapClass0(String name)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果调用父类加载后,未能成功加载,就自己加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
下面我们来看看ExtClassLoader和AppClassLoader的父类加载器是谁。
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e null
说明AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是空,再结合上面loadClass()方法的源码,当父加载器为null时调用本地方法native findBootstrapClass0(String name) ,也就是调用BootstrapClassLoader加载器
五 初始化
Java在编译之后会在字节码文件中生成方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到方法中,收敛顺序(这里只讨论非静态变量和语句块)为:1. 父类变量初始化 2. 父类语句块 3. 父类构造函数 4. 子类变量初始化 5. 子类语句块 6. 子类构造函数。所谓收敛到方法中的意思就是,将这些操作放入到中去执行。
Java在编译之后会在字节码文件中生成方法,称之为类构造器,类构造器同实例构造器一样,也会将静态语句块,静态变量初始化,收敛到方法中,收敛顺序为:1. 父类静态变量初始化 2. 父类静态语句块 3. 子类静态变量初始化 4. 子类静态语句块
方法是在类加载过程中执行的,而是在对象实例化执行的,所以一定比先执行。所以整个顺序就是:1. 父类静态变量初始化 2. 父类静态语句块 3. 子类静态变量初始化 4. 子类静态语句块 5. 父类变量初始化 6. 父类语句块 7. 父类构造函数 8. 子类变量初始化 9. 子类语句块 10. 子类构造函数
六 答疑
1、初始化顺序
如下父类代码
public class Parent {
static int a = 1;
int b = 1;
static {
System.out.println("parent static block(a):" + (++a));
}
{
System.out.println("parent block(b):" + (++b));
}
public Parent() {
System.out.println("parent construction");
}
}
子类代码
public class Child extends Parent {
static int a = 1;
int b = 1;
static {
System.out.println("child static block(a):" + (++a));
}
{
System.out.println("child block(b):" + (++b));
}
public Child() {
System.out.println("child construction");
}
public static void main(String[] args) {
new Child();
}
}
最终输出结果为
parent static block(a):2
child static block(a):2
parent block(b):2
parent construction
child block(b):2
child construction
2、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。
七 Java代码编译过程简述
代码编译是由Javac编译器来完成,流程如下图1所示:
*.java文件经过javac *.java编译生成*.class文件,java *.class运行。
Javac是一种编译器,能将一种语言规范转化成另外一种语言规范,通常编译器都是将便于人理解的语言规范转化成机器容易理解的语言规范。Javac的任务就是将Java源代码编译成Java字节码语言,转化为JVM能够识别的一种语言,也就是二进制代码,然后由JVM将JVM语言再转化成当前这个机器能够识别的机器语言。
从表面看是将.java文件转化为.class文件。而实际上是将Java源代码转化成一连串二进制数字,这些二进制数字是有格式的,只有JVM能够真确的识别他们到底代表什么意思。
编译器把一种语言规范转化为另一种语言规范的这个过程如下:
1)词法分析:读取源代码,一个字节一个字节的读进来,找出这些词法中我们定义的语言关键词如:if、else、while等,识别哪些if是合法的哪些是不合法的。这个步骤就是词法分析过程。
词法分析的结果:就是从源代码中找出了一些规范化的token流,就像人类语言中,给你一句话你要分辨出哪些是一个词语,哪些是标点符号,哪些是动词,哪些是名词。
2)语法分析:就是对词法分析中得到的token流进行语法分析,这一步就是检查这些关键词组合在一起是不是符合Java语言规范。如if的后面是不是紧跟着一个布尔型判断表达式。
语法分析的结果:就是形成一个符合Java语言规定的抽象语法树,抽象语法树是一个结构化的语法表达形式,它的作用是把语言的主要词法用一个结构化的形式组织在一起。这棵语法树可以被后面按照新的规则再重新组织。
3)语义分析:语法分析完成之后也就不存在语法问题了,语义分析的主要工作就是把一些难懂的,复杂的语法转化成更简单的语法。就如难懂的文言文转化为大家都懂的白话文,或者是注释一下一些不懂的成语。
语义分析结果:就是将复杂的语法转化为简单的语法,对应到Java就是将foreach转化为for循环,还有一些注释等。最后生成一棵抽象的语法树,这棵语法树也就更接近目标语言的语法规则。
4)字节码生成:将会根据经过注释的抽象语法树生成字节码,也就是将一个数据结构转化为另外一个数据结构。就像将所有的中文词语翻译成英文单词后按照英文语法组文英文语句。代码生成器的结果就是生成符合java虚拟机规范的字节码。
常量是程序运行时恒定不变的量,许多程序设计语言都有某种方法,向编译器告知一块数据时恒定不变的,例如C++中的const和Java中的final。根据编译器的不同行为,常量又分为编译时常量和运行时常量,其实编译时常量肯定就是运行时常量,只是编译时常量在编译的时候就被计算执行计算,并带入到程序中一切可能用到它的计算式中。
以Java为例,static final int a = 1将是一个编译时常量,编译后的符号表中将找不到a,所有对a的引用都被替换成了1。而static final int b = "test".length()将是一个运行时常量。
(1)a被作为编译期全局常量,并不依赖于类,而b作为运行期的全局常量,其值还是依 赖于类的。
(2)编译时常量在编译时就可以确定值,上例中的a可以确定值,但是c在编译器是不可 能确定值的。
(3)由于编译时常量不依赖于类,所以对编译时常量的访问不会引发类的初始化。而运行时常量访问会引发类的初始化。