2.1 类加载子系统
类加载子系统作用
主要作用就是从文件系统或网络中加载class文件生成描述类信息的Class类并将类信息放到方法区中
类加载子系统只负责加载class,能否执行需要根据执行引擎决定.
加载的类信息会放到方法区中,除了类信息,还会存放运行时的常量池,可能还包括字符串字面量和数字常量(这些信息是class文件中常量池的内存映射)
Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这里就是我们经常能见到的Class类。
类的生命周期
加载-> 链接-> 初始化 -> 使用 --> 卸载
类的加载过程
1.通过一个类的全限定名获取此类的二进制流
2.将这个字节流代表的静态存储结构转化为方法区中的数据结构(类模板)
3.在堆中生成一个代表这个类的java.lang.Class对象,他指向了方法区中的类模板,作为方法区这个类的各种数据的入口
4.Class类元信息存放在方法区
类的链接阶段
1.验证(Verify)
主要验证class文件的字节流中包含的信息符合规范要求确保加载的正确性,确保不会危害虚拟机.主要验证方式有 文件格式验证(和加载阶段同步进行),字节码验证,元数据验证,符号引用验证(解析环节进行,例如NoClassDefException)
class文件的特殊头部
2.准备(Prepare)
为类的静态变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
这里不包含用final修饰的static变量,因为final在编译的时候就会分配了,准备阶段会显示初始化
ps:
private static int i = 1; //变量i在准备阶只会被赋值为0,初始化时才会被赋值为1
private final static int j = 2; //这里被final修饰的变量j,直接成为常量,编译时就会被分配为2
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
3.解析(Resolve)
将常量池内的符号引用转换为直接引用(内存地址)的过程
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
类的初始化
初始化阶段就是执行类构造器方法()的过程
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作(仅定义不赋值不会收集)和静态代码块中的语句合并而来
使用static+final且显式赋值中不涉及方法的调用的基本数据类型和String的显式赋值在Prepare阶段进行,其他情况的static变量的显式赋值都是在()中完成
构造器方法中指令按语句在源文件中出现的顺序执行
()不同于类的构造器(构造器是虚拟机视角下的())
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
虚拟机必须保证一个类的()方法在多线程下被同步加锁,其他线程想加载这个类只能等待阻塞.()的锁是隐式定义的不体现在class文件中,这种情况可能会造成死锁.当出现循环引用时,两个线程都需要加载对方的class导致死锁
public class ClassInitTest{
private static int num1 = 30;
static{
num1 = 10;
num2 = 10; //num2写在定义变量之前,不报错.因为类的link过程中的prepare会将num2初始化零值.
System.out.println(num2); //這裡直接打印可以吗?报错,非法的前向引用,可以赋值,但不可调用
}
private static int num2 = 20; //num2在准备阶段就被设置了默认初始值0,初始化阶段又将10改为20
public static void main(String[] args){
System.out.println(num1); //10
System.out.println(num2); //20
}
}
何时触发初始化
在类主动使用时会触发类的初始化,而被动使用时不会.
主动使用的几个情况
- 为一个类型创建一个新的对象实例时(比如new、反射、序列化)
- 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
- 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
- 调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
- 初始化一个类的子类时(Java虚拟机规范明确要求初始化一个类时,它的父类必须提前完成初始化操作,接口例外)
- JVM启动包含main方法的启动类时。
被动使用的几个情况
- 访问一个static字段时只有定义这个static字段的类才会初始化,例如通过子类使用父类的static字段时不会初始化
- 通过数组定义类引用时
- 通过类引用其常量
- 通过ClassLoader调用loadClass()
类的卸载
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
类的加载机制
两个类相等意味着类的加载器相等和类本身也相等
命名空间
- 每个类加载器都有自己的命名空间,由该加载器与父加载器共同构成.
- 在同一命名空间下不会出现两个全限定名相等的类
- 不同命名空间可能出现限定名相同的类
类加载层级结构如图:
注意:这里的加载器之间只是上下级关系,并不是通过继承关系来实现的,而是采用组合实现的。例如ExtClassLoader和AppClassLoader之间就没有继承关系,他们都是sun.misc.Launcher的内部类之间并没有继承关系
类加载器在JVM规范中只有启动类加载器和自定义加载器
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的(由C++实现)。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。
2)动态地创建符合用户特定需要的定制化构建类。
3)从特定的场所取得java class,例如数据库中和网络中。
类的加载器信息会在类加载过程中存到Class中,而类如果是BootstrapClassLoader加载就不会存所以String.class.getClassLoader()返回null
JVM类加载机制原理
•全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入(实际不一定是他),除非显示使用另外一个类加载器来载入
•父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
•缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//同步加锁
synchronized (getClassLoadingLock(name)) {
// 检查类是否已加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//调用父加载器来加载,不能加载返回null
c = parent.loadClass(name, false);
} else {
//父类为启动类加载器,通过启动类加载器加载,不能加载返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父加载器没能加载,就自己调用findClass()来加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
使用双亲委派机制可以保证一个类在系统中的全局唯一性,同时所有的类加载动作会经过父加载器,这样可以保证核心的api的安全性.
由于双亲委派的存在还导致了一个问题,那就是通过父类加载器无法找到由子类加载器加载的类.这时候可以使用ThreadContextClassLoader来破坏双亲委派逆向传递
注意:即使使用自定义类的加载器重写了loadClass()破坏了双亲委派也不会导致核心api的替换,因为不管是什么类的加载器加载最终都需要调用java.lang.ClassLoader#preDefineClass(),这其中会调用java.lang.ClassLoader#preDefineClass()对加载的类进行验证,保护核心api
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
自定义类加载器
自定义类加载器只需要继承ClassLoader抽象类就可以了,没有复杂的需求可以直接
重写findClass可以扩展类的加载逻辑
重写loadClass可以破坏双亲委派机制
Class.forName()与ClassLoader.loadClass()
Class.forName()会加载Class文件并完成类的链接和初始化工作
ClassLoader.loadClass()只是将Class文件加载到JVM中,不会主动进行类的初始化
2.2 JVM数据
总论
和java语言一样,JVM中也只有两种数据类型,即原始数据类型(基本数据类型+返回地址类型)和引用数据类型.
JVM的几乎所有的数据类型检查都是在运行之前做的,一般都是编译器做的.
JVM杜宇不同的基本数据类型都有专门对应的指令,所以不需要对基本数据类型进行检查或者区分是不是引用类型.
原始数据类型
原始数据类型包括了数字类型、布尔类型、返回地址类型.数字类型和布尔类型都与java语言的类型一一对应,返回地址则没有对应的java语言类型
数字类型包括 整型和浮点型,这些类型的默认值和java语言的默认值一致
整型:
- int
- byte
- short
- long
- char
浮点型:
- float
- double
布尔类型和java语言一致,1:true,0:false,默认值为false
返回地址类型的值是指向jvm的操作指令的指针
引用数据类型
引用数据类型包括了三种,类类型,数组类型,接口类型.
类类型的值是对动态创建的类实例的引用
数组类型的值是数组的引用
接口类型的值是对实现接口的类实例的引用
当然也可以是null引用