基于《深入理解JVM》输出目录
JVM-01 概述
JVM-02 类加载子系统
JVM-03 运行时数据区- [程序计数器+虚拟机栈+本地方法栈+本地方法+堆+方法区]
JVM-04 执行引擎+字符串常量池StringTable
JVM-05 垃圾回收(器)
本文目录
前言
提示:基于《深入理解JVM》-第二版-周志明著的个人输出文章,文中会提及相关内容在该书页码。B站大学相关视频 链接地址。 参考大佬博客链接地址
1. 类加载子系统 / JVM类加载机制
书P209: 虚拟机类加载机制
1.1 JVM架构图
1.2 类加载子系统作用
- 负责从文件系统或者网络中加载Class文件,class文件在文件开头要有特定的文件标识
验证阶段-文件格式验证
- ClassLoader只负责加载class文件,至于是否能运行由执行引擎(Execution Engine)决定
- 加载的类信息存放在一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字量(这部分常量信息是class文件中常量池部分的内存映射)
1.3 类加载过程
1.3.1 加载(Loading)
- 通过一个类的全限定名称来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
数组类的加载过程有点特殊,建议看书
1.3.2 链接(Linking)
I. 验证
- 目的在于确保Class文件的字节流中的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全
-
主要有四种验证:
-
- 文件格式验证:是否符合Class文件格式的规范
是否以魔数0xCAFEBABE
开头
主次版本号是否在当前虚拟机处理范围内)
…
- 文件格式验证:是否符合Class文件格式的规范
-
- 元数据验证:对字节码描述的信息进行语义分析,确保信息都符合Java规范
-这个类是否有父类(除了java.lang.Object之外的类都应当有父类)
-这个类的父类是否继承了不允许被继承的类(final修饰的类)
-如果不是抽象类,是否实现了其父类或接口要求实现的所有方法
…
- 元数据验证:对字节码描述的信息进行语义分析,确保信息都符合Java规范
-
- 字节码验证:在对元数据信息的数据类型做完校验之后,这阶段将对类的方法体进行校验分析,保证这个类的方法在运行是不会危害虚拟机安全
-保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
E:操作数栈中放了int类型,使用时却按long类型来加载入本地变量表,就有问题
-保证跳转指令不会跳到方法体以外的字节码指令上
…
- 字节码验证:在对元数据信息的数据类型做完校验之后,这阶段将对类的方法体进行校验分析,保证这个类的方法在运行是不会危害虚拟机安全
-
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段(解析)中发生。看作对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
-符号引用中通过字符串描述的全限定名称是都能找到对于的类
-符号引用中的类、字段、方法的访问性(private、public…)是否可以被当前类访问
-…
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段(解析)中发生。看作对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验
II. 准备
- “通常情况”下,正式为被static修饰的变量分配内存并设置该类变量的默认初始值,即零值。
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | ‘\u0000’ | reference | null |
byte | (byte)0 |
E: public static int value = 123; 在准备阶段默认初始化零值,即0
- “特殊情况”下,如果存在常量(用final修饰的static),因为final在编译的时候就会分配内存了,所以在准备阶段虚拟机将会根据常量的设置直接赋值。
E: public static final int value = 123;
编译时Javac会将value生成ConstantValue属性,在准备阶段JVM会根据ConstantValue的设置将value赋值为123,即final和static修饰的变量 在准备阶段会直接赋值
- 对于实例变量将会在对象实例化时随着对象一起分配到Java堆中
没有static的变量呢?private int i = 1;
和 private static int i = 1;
的区别?
- 被赋值的时机来解释:
static int i
是静态的类变量,当类加载的准备阶段时,i变量就会被分配内存地址和初始化为零值- 没有static的
int i
是私有的实例变量,只有在生成对象后来调用它的时候,才在类加载的初始化阶段来赋予其值:1。
III. 解析(有点难理解)
- 将常量池内的符号引用转化为直接引用的过程,解析操作往往在完成初始化阶段后再执行
- 符号引用:以一组符号来描述所应用的目标。可以是任意形式的字面量,定位得到目标即可
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池 的CONSTANT_Class_info/CONSTANT_Fieldref_info…
1.4 初始化
- 初始化阶段就是执行类构造器方法
<clinit>()
的过程 - 该方法是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语 句合并而来。 如果没有静态变量,字节码文件中就不会有clinit方法。
- 构造器方法中指令按语句在源文件中出现的顺序执行
所以输出是num = 2; number = 10;这个是clinit方法收集顺序按源文件出现顺序决定的
<clinit>()
方法与类的构造函数(<init>()方法
)不同;
若该类具有父类,JVM会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
已经执行完毕。
所以JVM中第一个被执行<clinit>()
方法的类肯定是java.lang.Object。- 父类的
<clinit>()
方法会先执行,意味父类中定义的静态语句块优先于子类的变量赋值操作 - 接口中不能使用静态语句块,但还是有变量初始化的操作。执行接口的
<clinit>()
方法不需要先执行父接口<clinit>()
方法。
只有父接口定义的变量使用时才会初始化。
接口的实现类在初始化时也一样不会执行接口的<clinit>()
方法。 - JVM必须保证一个类的
<clinit>()
方法在多线程下被同步加锁。
2. 类加载器分类
- JVM支持俩种类型的加载器:引导类(BootStrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
- 概念上来讲,自定义类的加载器一般指的是开发人员自定义的,但是JVM将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
- 无论怎么划分,最常见的类加载器始终只有三个
启动类加载器(引导类加载器,BootStrap ClassLoader)
拓展类加载器(Extension ClassLoader)
应用程序类加载器(App ClassLoader / 系统类加载器,System ClassLoader )
2.1自定义类与核心类库的加载器:
- 对于用户自定义类来说:使用系统类加载器AppClassLoader加载
- Java核心类库都是使用引导类加载器BootStrapClassLoader加载的
2.2 JVM自带的类加载器
启动类加载器(引导类加载器BootStrap ClassLoader)
- 由C++实现的,嵌套在JVM内部
- 加载java核心库(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载。是拓展类加载器和应用程序类加载器的父加载器
- 处于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类
拓展类加载器(Extension ClassLoader)
- java语言编写 ,独立于JVM外部,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类,父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,独立于JVM外部,由sun.misc.Launcher$AppClassLoader实现。
- 派生于ClassLoader类,父类加载器为拓展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
- 该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
Java日常应用程序开发中,类的加载几乎都是上述3种类加载器相互配合执行的,必要时,我们还可以自定义类加载器,来定制类的加载方式。
2.3为什么要自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 拓展加载源
- 防止源码泄漏
3. 双亲委派机制
-
工作过程:
-
如果一个类加载器收到类类加载的请求,它不会先自己去尝试加载,而是把这个请求委托给父类的加载器去执行,每一层次的类加载器都是如此。
-
所以所有的类加载请求都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(即它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。
-
E:小孩拿着梨,先问老妈吃不,老妈问奶奶吃不,奶奶不吃再给老妈,老妈不吃再给小孩。要是有人想吃就轮不到下一个人。
E: 创建一个类名为String,在包名为java.lang下
如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String。但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法,所以报错。
3.1 双亲委派机制的优势
- Java类随着类加载器一起具备了一 种带优先级的层次关系。能避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
E1: 自定义类:java.lang.String
E2: 自定义类:java.lang.MeDsh(java.lang包需要访问权限,阻止我们用包名自定义类)
3.2 反向双亲委派机制(了解)
E:程序需要用到SPI接口实现类,通过双亲委派机制去到引导类加载器,加载rt.jar包的SPI核心类。该类中存在一些接口,需要用到具体的实现类用到第三方的jar包,不属于引导类加载器,所以是反向委派到系统类加载器去加载,实际上是线程上下文类加载器去加载jdbc.jar
4. 沙箱安全机制
自定义String类,但是在加载自定义的String类的时候回率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
E:读写U盘的时候的360沙箱,防止U盘内的病毒对沙箱外的系统构成污染。
5. 其他补充
5.1 类与类加载器
- 在JVM中表示俩个class对象是否为同一个类,存在俩个必要条件
a) 类的完整类名和包名必须一致
b) 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同 - 即,在JVM中,即使俩个类对象来源于同一个Class文件,被同一个虚拟机加载,但只要加载它们的ClassLoader实例对象不同,那么这俩个类对象也是不相等的。
5.2 类的主动使用和被动使用
- Java程序对类的使用方式分为:主动使用 和 被动使用
主动使用:七种情况
- 创建类的实例
- 访问某个类或接口的静态变量,或者对静态变量赋值
- 调用类的静态方法
- 反射 比如Class.forName(com.dsh.jvm.xxx)
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始支持的动态语言:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、 REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
基于《深入理解JVM》输出目录
JVM-01 概述
JVM-02 类加载子系统
JVM-03 运行时数据区- [程序计数器+虚拟机栈+本地方法栈+本地方法+堆+方法区]
JVM-04 执行引擎+字符串常量池StringTable
JVM-05 垃圾回收(器)