写java好多年一直对jvm只有个大概理解,这次来系统的全面的理解一下。
一、简介
JVM全称:Java Virtual Machine Java虚拟机。先通过下面这张图来理解一下JVM的作用。
一句话简介:JVM通过解释执行或者即时编译将java字节码转为可以在不同操作系统上运行的机器码。
下面图是JVM的组成:
ClassLoader:类加载器,加载类文件(编译后的.class文件),将字节码文件加载到内存中并生成响应的类对象。
RuntimeDataAreas:运行时数据区,程序运行期间需要使用到的内存区域,存放字节码信息、程序执行过程中的数据。
ExecutionEngine:执行引擎,负责协调类加载器、运行时数据区将字节码转为机器码指令的工具。
二、类加载器介绍
2.1 加载流程
加载流程有7步组成:
加载->验证->准备->解析->初始化->使用->卸载
如果是动态绑定类则先初始化再解析
2.1.1 加载
将字节码转化为二进制字节流加载到内存中,并生成一个class实例。
2.1.2 验证
对二进制字节流进行校验,只有复合规范的二进制文件才可以被正确执行,这一步应该比较好理解,相当于实际业务编码中的参数校验功能,不是随便输入点信息就能被执行。
具体验证项有:
1、文件格式验证,文件层面
是否以0xCAFEBABE开头
主、次版本号是否在当前虚拟机的处理范围之内
常量池中的常量是否有不被支持的常量类型
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
2、元数据验证,类层面
这个类是否有除了java.lang.Object之外的父类
这个类的父类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾
3、字节码验证,内部编码层面
确定程序语义合法、符合逻辑,主要是对类的方法体进行校验分析。如:函数的参数类型是否正确、对象类型转换是否合理。
4、符合引用验证,使用的类、字段、方法层面
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法的访问性是否可被当前类访问
2.1.3 准备
对类的静态变量分配内存并初始化,对应的数据类型设置默认初始值如0、false、null等,如果是final修饰的变量被称为常量不设置默认初始值,设置代码中定义的值。
private String param1 = "aaa";//初始化时分配空间和赋值aaa
private static String param2 = "bbb";//准备时分配空间和赋值null,初始化时赋值bbb
private static final String param3 = "ccc";//准备时分配空间和赋值ccc
private static final String param4="ddd";//初始化时分配空间和赋值ddd
2.1.4 解析
将class文件常量池类、接口、字段、方法的符号引用转化为直接引用。关于常量池请参考像下面文章。
https://blog.youkuaiyun.com/qq_45167987/article/details/124969172
2.1.5 初始化
执行构造方法的过程,具体步骤如下:
1、静态代码块执行和静态成员变量赋值,具体顺序由编码顺序决定,继承情况下先父类后子类。
2、父类的非静态代码块执行和非静态成员变量赋值,具体顺序由编码顺序决定,继承情况下。
3、父类的构造方法,继承情况下。
4、非静态代码块执行和非静态成员变量赋值,具体顺序由编码顺序决定。
5、执行构造方法。
2.1.6 使用
2.1.7 卸载
2.1.7.1 卸载条件
1、该类的所有实例都已被回收
2、类的class对象没有被引用
3、类加载器已被回收
2.1.7.2 卸载因素
jvm会根据垃圾回收策略和内存管理机制来决定何时卸载一个类。
加载流程参考:
https://www.php.cn/faq/595401.html
2.2 类加载时机
2.2.1 创建类对象,使用new关键字,如果这个类还没有被加载,jvm会隐式加载这个类
public class Main2 {
{
System.out.println("这里是代码块2");
}
static {
System.out.println("这里是静态代码块2");
}
public Main2() {
System.out.println("这里是构造方法2");
}
public static String param = "qqqq";
}
public class Main3 {
public static void main(String[] args) {
System.out.println("MAIN方法执行开始-------3");
System.out.println(new Main2());
System.out.println("MAIN方法执行结束-------3");
}
}
MAIN方法执行开始-------3
这里是静态代码块2
这里是代码块2
这里是构造方法2
org.example.Main2@75828a0f
MAIN方法执行结束-------3
2.2.2 访问类的静态变量和静态方法。
public class Main2 {
{
System.out.println("这里是代码块2");
}
static {
System.out.println("这里是静态代码块2");
}
public Main2() {
System.out.println("这里是构造方法2");
}
public static String param = "qqqq";
}
public class Main3 {
public static void main(String[] args) {
System.out.println("MAIN方法执行开始-------3");
// System.out.println(new Main2());
// System.out.println("--------------------------");
System.out.println(Main2.param);
System.out.println("MAIN方法执行结束-------3");
}
}
MAIN方法执行开始-------3
这里是静态代码块2
qqqq
MAIN方法执行结束-------3
2.2.3 初始化子类时,如果父类还没有被加载就先加载父类,再加载子类
public class Main2 extends Main2Parent {
{
System.out.println("这里是代码块2");
}
static {
System.out.println("这里是静态代码块2");
}
public Main2() {
System.out.println("这里是构造方法2");
}
public static String param = "qqqq";
}
public class Main2Parent {
{
System.out.println("这里是父类代码块");
}
static {
System.out.println("这里是父类静态代码块");
}
public Main2Parent() {
System.out.println("这里是父类构造方法");
}
public static String parentParam = "aaa";
}
public static void main(String[] args) {
System.out.println("MAIN方法执行开始-------3");
// System.out.println(Main2.param);
// System.out.println("----------------------");
System.out.println(new Main2());
System.out.println("MAIN方法执行结束-------3");
}
MAIN方法执行开始-------3
这里是父类静态代码块
这里是静态代码块2
这里是父类代码块
这里是父类构造方法
这里是代码块2
这里是构造方法2
org.example.Main2@3abfe836
MAIN方法执行结束-------3
2.2.4 启动项目时指定的主类,包含main方法的类
public class Main3 {
static {
System.out.println("这里是main方法所在主类静态代码块");
}
public static void main(String[] args) {
System.out.println("MAIN方法执行开始-------3");
// System.out.println(Main2.param);
// System.out.println("----------------------");
System.out.println(new Main2());
System.out.println("MAIN方法执行结束-------3");
}
}
这里是main方法所在主类静态代码块
MAIN方法执行开始-------3
这里是父类静态代码块
这里是静态代码块2
这里是父类代码块
这里是父类构造方法
这里是代码块2
这里是构造方法2
org.example.Main2@3abfe836
MAIN方法执行结束-------3
2.3 类加载器
2.3.1 类加载器类型
1、启动类加载器 BootStrap ClassLoader,负责加载jvm的核心类库,加载%JAVA_HOME%/jre/lib下包名为java、javax、sun开头的类,是所有其它类加载器的父记载器,由C++实现的。
2、扩展类加载器 Extension ClassLoader,负责加载java扩展库中的类,加载位于%JAVA_HOME%/jre/lib/ext目录下的所有jar文件或者由系统属性java.ext.dirs指定位置的类,允许扩展java核心功能,不影响系统类加载。
3、应用类加载器 Application ClassLoader,也叫系统类加载器 System ClassLoader,负责加载系统类路径classpath上指定的类库,通常是你的应用类和第三方库。
4、自定义类加载器,java允许用户创建自己的类加载器,通过继承java.lang.ClassLoader类的方式实现。这在需要动态加载资源、实现模块化框架或者特殊的类加载策略时非常有效。
System.out.println("------------------启动类加载器------------------------");
URLClassPath path = Launcher.getBootstrapClassPath();
for (URL url : path.getURLs()) {
System.out.println(url.getPath());
}
System.out.println("-----------------扩展类加载器-------------------------");
URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
for (URL url : extClassLoader.getURLs()) {
System.out.println(url.getPath());
}
System.out.println("------------------应用类加载器------------------------");
URLClassLoader appClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
for (URL url : appClassLoader.getURLs()) {
System.out.println(url.getPath());
}
2.3.2 双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法加载这个类时,子加载器才会尝试自己去加载。
使用双亲委派模式来组织类加载器质检的关系,好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是要委派给处于模型最顶端的启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的classpath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证。
2.3.3 打破双亲委派机制
1、自定义类加载器,并且重写loadClass方法
2、现成上下文类加载器
3、osgi框架的类加载器