JVM全文
类的生命周期
概述
- 引用数据类型需要进行类的加载
- 基本数据类型虚拟机预先定义好的,不需要加载
- 生命周期
- 使用过程
加载
- 将java类的字节码文件加载到机器内存中,在内存中构建java类的原型
类模板对象
类模板,类在内存中的快照,将字节码解析出的
常量池/类字段/类方法存储到类模板,通过反射进行获取
- 查找并加载类的二进制数据,生成class实例
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区数据接口
- 创建
java.lang.Class类的实例(堆中),作为方法区这个类的各种数据的访问入口
获取二进制流
- 通过文件系统,读入class后缀的文件
- 读入
jar/zip等归档数据包,提取类文件 - 事先存放在数据库的类的二进制数据
- 类似
http协议进行网络加载 - 运行时生成的一段class二进制信息
- 如果输入的数据不是
classFile结构,抛出ClassFormatError
类模板和class实例
- 类模板存储在方法区
class文件完全加载进元空间后,在堆中创建Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每一个类都有对应的Class类型的对象
Class类用来表示类的类,记录类的成员、方法等信息,类的实际表征,指向方法区的类模板instanceKlass -> mirror: Class实例
数组类的加载
- 数组类本身并不是由类加载器负责创建,而是JVM在运行时根据需要直接创建,但是数组中的元素仍然需要类加载器去创建
- 元素类型如果是引用类型,遵循定义的加载过程递归加载和创建数据
- JVM使用指定的元素类型和数组维度来创建新的数组
- 数组类型是引用类型,数组的访问权限由元素类型的访问权限决定
链接
验证
- 保证加载的字节码是否符合规范
- 格式验证
CAFEBABE会和加载阶段一起执行,验证通过后,才会将类的二进制数据加载进方法区 - 格式验证之外的验证操作,都将在方法区进行
- 语义检查,是否存在不兼容的方法
- 字节码验证,函数传参是否正确,变量的赋值是否正确
栈映射帧,StackMapTable,用于检测在特定的字节码处,其局部变量表和操作数栈是否有正确的数据类型,仅用于有跳转的字节指令中- 跳转语句有块的概念。块的起点是跳转字节指令的目的地址,每个基本块是以栈映射帧的形式存在的
- 字节码偏移量,和栈映射帧的偏移量不是一样的,字节码偏移量+1=帧偏移量
![]()
- 符号引用在解析的时候才会执行
准备
- 对
非final修饰的基本数据类型,为类的静态变量分配内存,初始化为默认值 - 这里不包括基本数据类型的字段用
static final修饰的情况,final在编译的时候就会分配,在准备阶段显式赋值
如果使用
static final修饰String,字面量的形式赋值,也是在准备环节直接显式赋值
- 在准备环节不会像初始化阶段,会有初始化或者代码被执行
解析
- 将类、接口、字段、方法的符号引用转换为直接引用(实际的地址),实际使用中需要知道这个方法在方法表中的偏移量,通过解析可以转变为目标方法在类中方法表的位置,从而被成功调用
- 通常在初始化之后进行
- 字符串常量池部分在常量池中的体现,带有utf8的为字符串常量池中的字符串,在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例
- jvm内部运行的常量池,维护一张字符串拘留表(intern),保存出现过的字符串常量,没有重复项
初始化
- 类装载的最后一个阶段,执行
<clinit>()方法,由类静态成员变量的赋值语句和static语句块组成 - 为类的静态变量赋于正确的初始值
- 这个阶段,真正执行类中定义的java程序代码
- 由父及子,静态先行,先加载父类,再加载子类
- 不生成
<clinit>()
- 只有实例变量,只有非静态字段
- 静态变量没有被显式赋值
final static修饰的基本数据类型(普通赋值情况,直接赋常量)和String(字面量形式赋值),在准备环节被赋值
- 除
字面量赋值形式的String外的引用类型,或者通过方法(new)的形式为数据进行赋值,在初始化阶段赋值 - 使用
final static修饰,且显式赋值中不涉及方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行的
clinit的线程安全
- 虚拟机会确保多线程环境下的安全,保证一个类的
clinit在多线程环境中被正确地加锁、同步 - 带的是隐式的锁,如果一个类的clinit耗时很长,有可能造成多线程阻塞,死锁,但是很难发现
- 如果之前的线程成功加载类,在队列中等待的线程没有机会再次执行clinit,使用这个类时直接返回已经准备好的信息
主动使用和被动使用
- 使用链接过程准备好的资源,不会进行初始化
- 主动使用会执行初始化阶段
- 创建一个类的实例,new关键字、反射、克隆、反序列化
- 调用类的静态方法,使用了字节码
invokestatic指令- 使用类、接口的静态字段,使用
getstatic/pustatic指令;final static修饰的基本数据类型或字面量赋值的String不会主动调用初始化- 使用
java.lang.reflect包中反射类的方法,Class.forName("全类名"),主动加载类- 初始化子类,发现父类还没有被初始化,需要先初始化父类,
-XX:+TraceClassLoading追踪类加载信息;初始化类的时候,并不会先初始化所实现的接口- 一个接口定义了
default方法,直接实现或间接实现该接口的类的初始化- 虚拟机启动,用户需要指定一个要执行的主类,包括
main方法的那个类- 初次调用
MethodHandle实例(反射中),初始化该MethodHandle指向的方法所在的类
- 被动使用不会引起类的初始化
- 当访问一个静态字段时,静态字段属于谁,初始化谁
- 通过数组定义类引用,不会触发
- 引用常量不会触发,常量在链接阶段显式赋值
ClassLoader类的loadClass()方法加载一个类,并不是类的主动使用,不会导致类的初始化
使用
- 在加载并初始化成功之后,可以访问和调用类中的静态类成员变量,使用new关键字为其创建对象实例
类的卸载
- 类加载器对象加载类,并用一个集合存放所加载类的引用;
Class对象总是指向加载该类的加载器,调用getClassLoader()获取加载器 - 类的实例引用这个类的Class对象,
getClass()方法,返回Class对象的引用 - 它的Class对象生命周期决定类的生命周期
回收
- 方法区主要回收的部分
常量池中废弃的常量/不再使用 - 类型回收条件
- 该类的所有实例被回收,不存在任何派生子类的实例
- 加载该类的类的回收器被回收
- 对应的Class对象在任何地方没有被引用
类的加载器
概述
- classLoader负责将class信息的二进制流数据读入jvm,转换为一个与目标对应的
Class对象实例,然后交给jvm虚拟机进行链接、初始化 显式加载,通过调用ClassLoader加载class对象隐式加载,调用对象时会加载class对象- 命名空间
- 类的唯一性,类的加载器(实例)和类本身确定类的唯一
- 命名空间,由该类的加载器以及所有的父加载器所加载的类组成;同一个命名空间下,不会出现类完整名称相同的两个类
- 加载机制的特征
- 双亲委派模型
- 可见性,子加载器加载的类可以访问父加载器加载的类型
- 单一性,父加载器的类型对于子加载器是可见的,父加载器加载过的类型就不会在子加载器中重复加载,但是同级加载器,可以重复加载同一类型多次
类加载器的分类
- 主要分为
引导类加载器Bootstrap ClassLoader(C++实现)和自定义类加载器User-Defined ClassLoader(Java实现) - 下层加载器包含上层加载器的引用
- 引导类加载器
BootStrap ClassLoader,C/C++实现,嵌套在jvm内部,用来加载java的核心库,包名为java/javax/sun...;并不继承自ClassLoader;加载扩展类和应用程序类加载器,并指定为他们的父类加载器- 自定义类加载器
User-Defined ClassLoader,所有派生于抽象类ClassLoader的类加载器,java实现- 扩展类加载器,
Extension Classloader,父类为启动类加载器,从java.ext.dirs系统属性所指定的目录加载类库,或从JDK安装目录的jre/lib/ext子目录下加载类库,用户创建的jar放在此目录下,也会由扩展类加载器加载- 应用程序类加载器,,父类加载器为扩展类加载器,
AppClassLoader,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;程序中默认的类加载器
- 获取加载器
//引导类 null
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
//当前线程获取上下文,系统类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
- 自定义加载器,实现类库动态加载
- 数据类型的加载,与元素类型有关,与元素类型的类加载器一致;基本数据类型不需要加载,虚拟机已经预先定义好
String[] arrStr = new String[10];
Person[] persons = new Person[10];
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(persons.getClass().getClassLoader());
//null
System.out.println(arrStr.getClass().getClassLoader());
- 继承关系
源码
ClassLoader
- 引导类加载器为空
Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.
- 基本数据类型的加载器为空
If this object represents a primitive type or void, null is returned.
- 数组
The class loader for an array class, as returned by {@link Class#getClassLoader()} is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
- 双亲委派模型
When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself.
Launcher
- 扩展类的父类加载器(上层类加载器)为null
- 创建系统类加载器,传入扩展类
- 设置当前线程上下文加载器
ClassLoader内部方法
- 获取父类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader.getParent());
- loadClass,内部实现逻辑为
双亲委派原则
// resolve 如果为true,加载class的同时,进行解析操作
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) { //同步操纵只能加载一次
// First, check if the class has already been loaded
// 首先在缓存中判断是否加载同名的类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //当前类加载器的父类加载器
c = parent.loadClass(name, false); //先让父类尝试加载
} else { //parent==null,则委托引导类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) { //当前类的父类加载器未加载此类
// If still not found, then invoke findClass in order
// to find the class.
//调用当前类的加载器的findClass方法加载类
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;
}
}
- findClass,在
URLClassLoader中进行了重写,负责加载类成Class实例
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//类的路径
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//根据给的数据,返回对应类的Class实例,真正加载二进制流数据
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
SecureClassLoader与URLClassLoader
- SecureClassLoader,对代码源的位置及其证书验证,权限定义类验证
- URLClassLoader,真正加载class文件,将二进制流封装成Class实例
Class.forName()与ClassLoader.loadClass()
- Class.forName(),静态方法,根据传入的类的全限定名返回Class对象,将Class加载到内存后,进行类的初始化
- ClassLoader.loadClass(),实例方法,需要一个对象调用该方法,将class文件加载进内存时,不会执行类的初始化,直到这个类第一次被使用
双亲委派模型
- jdk1.2开始
- 一个类加载器接到加载器的请求时,首先不会尝试加载这个类,而是将这个类的加载任务委托给父类,当父类加载器无法完成加载任务时,自己去尝试加载
引导类先加载 -> 扩展类 -> 系统类 -> 自定义类
- 优势
- 避免类的重复加载,确保类的全局唯一性,优先级的层次关系
- 保护程序安全,防止核心API被随意篡改
- 代码体现在
loadClass()
- 现在当前加载器的缓存中查找是否有此类,如果有,直接返回
- 判断当前加载器的父类加载器是否为空,不为空,调用父类的
loadClass()进行加载;为空,调用findBootstrapClassOrNull(),用引导类加载器进行加载- 如果都不行,则用当前类加载器进行加载,最终调用
defineClass加载目标类
- 核心类库保护机制,类加载最终都会调用
defineClass(),进一步执行preDefineClass(),对核心类库进行了保护 - 弊端
- 委托过程是单向的,顶层的ClassLoader无法访问底层的ClassLoader所加载的类
- 情景,系统类接口绑定一个工厂方法,用于创建该接口的实例,但是该接口的实现由应用类加载器进行加载,而接口和工厂方法都由启动类加载器加载,那么该工厂方法无法创建由应用类加载器加载的接口实例
- JVM没有明确要求类的加载机制一定使用双亲委派机制
破坏双亲委派机制
- 1.2之前没有双亲委派机制,1.2之后需要兼容已有的代码,引导用户编写的类加载逻辑时,重写
findClass()方法 - 模型的缺陷,基础类无法调用用户的代码,由应有类加载器进行加载
- SPI,Service Provider Interface,JNDI服务提供者接口,可由应用层自行实现的基础类中的接口,
- 当顶层加载器需要加载应用层的类,委托给线程上下文加载器去调用
- 用户对于程序动态性的追求,每一个程序的模块都有自己的类加载器,当需要更换一个部分时,就直接换掉类加载器以及相关模块内容,以实现代码热替换,类的加载机制则为更复杂的网状结构
- 代码热替换、模块热部署,可以不用重启服务,就能进行代码修改
- 用两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为是两个不同的类
沙箱安全机制
- sandbox,限制程序运行的环境
- 保证程序安全,保护java原生的jdk代码
- 限定java代码在jvm特定的运行范围中,并且严格限制代码对本地系统资源的访问,CPU、内存、文件系统、网络
- 1.0 本地代码无条件授权;远程代码有限制
- 1.1 对受信任的远程代码,允许用户指定代码对本地资源的访问权限
- 1.2 代码签名,增加权限组,按照用户设定的权限进行访问
- 1.6 会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,应用域通过系统域的部分代理来对各种资源进行访问
自定义类加载器
- 隔离加载类,实现不同中间件类的隔离
类仲裁机制,如果全限定类名一样,会导致类冲突,但是同一个框架中,不同的服务可能依赖同一个类的不同版本
- 修改类的加载方式,在某个时间点按需加载
- 扩展加载源,数据库、网络、电视机顶盒
- 防止源码泄漏,可以在自定类加载器中进行编译加密
- 不同类加载器加载同一个类,其实是两个命名空间下的,被认为是两个类
实现自定义类加载器
- 重写
findClass,保存loadClass中的双亲委派模型 - 父类加载器为系统类加载器
public class MyClassLoader extends ClassLoader {
//路径
private String byteCodePath;
public MyClassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}
public MyClassLoader(ClassLoader parent, String byteCodePath) {
super(parent);
this.byteCodePath = byteCodePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filename = byteCodePath + name + ".class";
//获取对应class文件的二进制数据流
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
//获取一个输入流
bis = new BufferedInputStream(new FileInputStream(filename));
//获取一个输出流,输出内存中的byteArr的数组中
baos = new ByteArrayOutputStream();
//写数据
int len;
byte[] buff = new byte[1024];
while ((len = bis.read(buff)) != -1) {
baos.write(buff, 0,len);
}
//获取完整的字节数组数据
byte[] bytes = baos.toByteArray();
//将字节数组的数据转换为Class的实例
Class<?> aClass = defineClass(null, bytes, 0, bytes.length);
return aClass;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.close();
}
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
- 调用
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("/Users/apple/Desktop/java/");
try {
Class<?> aClass = loader.loadClass("ADDTest");
//com.java.test.MyClassLoader
System.out.println(aClass.getClassLoader().getClass().getName());
//sun.misc.Launcher$AppClassLoader
System.out.println(aClass.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
JDK9新特性
- 模块化构建
- 没有改变三层类加载器架构和双亲委派模型
//jdk.internal.loader.ClassLoaders$AppClassLoader@7ad041f3
System.out.println(ClassLoaderTest.class.getClassLoader());
//jdk.internal.loader.ClassLoaders$PlatformClassLoader@5acf9800
System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
//null
System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());
- 扩展机制被移除,向后兼容性被保留,但是被重命名为平台类加载器
ClassLoader platformClassLoader = ClassLoader.getPlatformClassLoader();
System.out.println(platformClassLoader);
- 平台类加载器和应用类加载器不在继承于
URLClassLoader - 启动类加载器,在jvm内部与java类库共同协作实现的类加载器,具体获取依然是null值
- 获取加载器的名称
String name = platformClassLoader.getName();
System.out.println(name);
- 双亲委派模型的变动,先判断该类是否能够归属到某一个系统模块中,如果可以,优先委派给负责那个模块的加载器完成加载