java 不是最强大语言, 但是 JVM 一定是最伟大的虚拟机。
JVM概述
jvm 是跨语言的平台
- 每个语言都需要转换成字节码文件,最后转换的字节码文件都能通过Java虚拟机进行运行和处理。
- 随着Java7的正式发布,Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。
- Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心“字节码”文件
虚拟机
虚拟机(Virtual Machine), 就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
-
系统虚拟机
-
程序虚拟机 java虚拟机
大名鼎鼎的Visual Box, VMware就属 于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
JVM
- 作用
Java虛拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台,上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
-
特点
➢一次编译,到处运行 ➢自动内存管理
➢自动垃圾回收功能
➢即时编译JIT
java代码执行例程
汇编语言 汇编 高级语言
jvm 架构模型
jvm 的生命周期
-
虚拟机的启动
- 通过引导类加载器bootstrap class loader创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。
-
虚拟机的执行
- 执行一个所谓的Java程序的时候,真正执行的是一个叫Java虚拟机的进程
-
虚拟机的退出
- 程序正常执行结束
- 执行过程遇到异常或错误而异常终止
- 操作系统错误导致Java虚拟机进程终止
- Runtime类或System类的exit方法、runtime类的halt方法,并且Java安全管理器允许这次exit或halt操作
- halt停止、停下、阻止
- exit方法源码:static native void halt0(int status)
- JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机退出的情况
类加载器
什么是类加载器
通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
类加载器有哪些
-
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。JVM的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
-
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
-
启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用
-
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lSJgaqZq-1617899154720)(jvm/image-20210301215743875.png)]
注意
对于用户来说定义器来说,默认使用系统类加载器进行加载
Java的核心类库,使用引导类加载器进行加载
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("systemClassLoader = " + systemClassLoader);// sun.misc.Launcher$AppClassLoader@18b4aac2
//
ClassLoader parent = systemClassLoader.getParent();
System.out.println("parent = " + parent); // sun.misc.Launcher$ExtClassLoader@1540e19d
//
ClassLoader parentParent = parent.getParent();
System.out.println("parentParent = " + parentParent);// null
//用户自定义类
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();// sun.misc.Launcher$ExtClassLoader@1540e19d 默认使用系统系统类加载器
System.out.println("classLoader = " + classLoader);
System.out.println("String.class.getClassLoader() = " + String.class.getClassLoader());// null String(java核心类库) 使用引导类加载器加载 JAVA_HOME/jre/lib/ rt. jar
类装载的执行过程
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 验证:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标识,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。
类加载器详解
ClassLoader抽象类
API
启动类加载器
c/c++ 语言编写,嵌套 JVM 内部
不继承于 Java.lang.ClassLoader ,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java\javax\sun等开头的类
负责加载的内容
用来加载Java核心类库,rt.jar,resources.jar,sun.boot.class.path路径下的内容
` URL[] urLs1 = Launcher.getBootstrapClassPath().getURLs();
for(URL url : urLs1){
System.out.println(url);
}
扩展类加载器
java 语言编写
父类加载器为启动类加载器
负责加载的内容
从java.ext.dirs系统属性所指定的目录中加载类库,或从jre/lib/ext子目录下加载类库
String property = System.getProperty("java.ext.dirs");
String[] split = property.split(";");
for (String s : split) {
System.out.println("s = " + s);
}
应用程序类加载器(系统类加载器)
Java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader#getSystemClassLoader()方法可以后去到改类加载器
用户自定义类加载器
出现的意义?
- 隔离加载类 例如使中间件的Jar包与应用程序Jar包不冲突
- 修改类加载的方式 启动类加载器必须使用,其他可以根据需要自定义加载
- 扩展加载源
- 防止源码泄露 对字节码进行加密,自定义类加载器实现解密
实现
继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器
1.2之前,继承并重写loadClass方法,1.2之后,建议把自定义的类加载逻辑写在findClass()方法中
如果没有太过复杂的需求,可以直接继承URLClassLoader类,可以避免自己编写findClass()方法,及其获取字节码流的方式,使自定义类加载器编写更加简洁
双亲委派机制
原理
Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式。
过程
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
- 如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
其他
沙箱安全机制
保证对Java核心源代码的保护
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改
补充
-
JVM必须知道一个类型是由启动类加载器加载的,还是由用户类加载器加载的。如果是用户类加载器加载的,JVM会将这个类加载器的一个引用作为类型信息的一部分,保存到方法区中。
-
JVM中表示两个class对象是否为同一个类的两个必要条件。
-
类的完整类名必须一致,包括包名
-
加载这个类的ClassLoader必须相同
-
类加载过程
加载
通过一个类的全限定名获取定义此类的二进制字节流
- 本地系统获取
- 网络获取,Web Applet
- zip压缩包获取,jar,war
- 运行时计算生成,动态代理
- 有其他文件生成,jsp
- 专有数据库提取.class文件,比较少见
- 加密文件中获取,防止Class文件被反编译的保护措施
将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
链接
验证
目的
确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
四种验证
文件格式验证
- 开头:CA FE BA BE(魔数,Java虚拟机识别)
- 主次版本号
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
元数据验证
- 对字节码描述的信息进行语义分析,保证描述符合Java规范
- 类是否有父类,除了Object之外,所有的类都应该有父类
- 类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是 抽象类,是否实现了其父类或接口中要求实现的所有方法。
- 类的字段,方法是否与父类的产生矛盾。例如方法参数都一样,返回值不同
字节码验证
- 通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的。
- 对类的方法体,进行校验分析,保证在运行时不会做出危害虚拟机的行为
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现类似于在操作数栈放了一个int类型的数据,使用时却按照long类型加载到本地变量表中的情况。
- 保障任何跳转指令都不会跳转到方法体之外的字节码指令上。
符号引用验证
- 通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类、字段、方法的可访问性是否可被当前类访问
准备
目的
为**类变量(静态变量)**分配内存,并且设置该类变量的初始值,即零值
注意
不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到Java堆中
解析
目的
将常量池内的符号引用转换为直接引用的过程(虚拟机栈,动态链接,解析)
-
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
-
符号引用就是一组符号来描述引用的目标。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
-
直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄
-
解析动作主要针对类,或接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info
初始化
初始化阶段是执行类构造器方法< clinit >()的过程
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
非法的前向引用问题
如果没有类变量和静态代码块,也不会有clinit
注意
构造器方法中指令按照语句在源文中出现的顺序执行
< clinit >()不同于类的构造器(关联:构造器是虚拟机视角下的< init >() )
若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕
虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁
注意
类初始化的充分必要条件
1、遇到new,getstatic,putstatic或invokestatic这四条字节码指令时。
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段(final修饰已在编译期将结果放入常量池的静态字段除外)
- 调用一个类型的静态方法的时候
2、对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化
3、初始化类的时候,发现父类没有初始化,则先触发父类初始化
4、虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
5、只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化
6、当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化
父类没有初始化,则先触发父类初始化
4、虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类
5、只用JDK7中新加入的动态语言支持,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法对应的类没有进行初始化,则先触发其初始化
6、当一个接口中定了JDK8新加入的默认方法时,如果这个接口的实现类发生了初始化,要先将接口进行初始化