【JVM 1】类加载器 + 运行时数据区

声明:JVM系列博客为本人的JVM学习笔记,如有雷同,纯属巧合。

/**
* @startTime 2021-03-13 08:30
* @endTime 2021-03-13 14:30
* @start P26内存结构概述
* @end P35双亲委派机制
* @efficiency (P35-P25)/1 = 10 * 10.8 = 108分钟/天
* @needDays 3841/108 = 36天
* @overDay 2021-03-13 + 36天 = 2021-04-17
*/

第一章 类加载器

一、类加载器子系统的作用

 

类加载器子系统负责从文件系统或网络中加载class文件,class文件在文件开头有特定的文件标识。

ClassLoader只负责class文件的加载,至于它是否可以运行,则有执行引擎决定。

加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

二、类的加载过程

1、加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2、链接

(1)验证(Verify)

  • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
  • 主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证

(2)准备(Prepare)

  • 为类变量分配内存并且设置该类变量的默认初始值
  • 这里不包含final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到堆中

(3)解析

  • 将常量池内的符号引用转换为直接引用的过程
  • 例如静态代码块、静态变量的显示赋值
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对常量池中的CONSTANT_Filedref_info、CONSTANT_Class_info、CONSTANT_Methodref_info等。

3、初始化

  1. 初始化阶段就是执行类构造器方法<clinit>()的过程
  2. 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  3. 构造器方法中指令按语句在源文件中出现的顺序执行
  4. <clinit>()方法不同于类的构造器。构造器是虚拟机视角下的<init>()
  5. 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
  6. 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

三、代码实例

验证【虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁】的代码实例

package com.guor.jvm;
 
public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + "开始");
            DeadThread dead = new DeadThread();
            System.out.println(Thread.currentThread().getName() + "结束");
        };
 
        Thread t1 = new Thread(r,"线程1");
        Thread t2 = new Thread(r,"线程2");
        t1.start();
        t2.start();
    }
}
 
class DeadThread{
    static {
        if(true){
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            while (true){
 
            }
        }
    }
}

虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

四、类加载器的分类

JVM类加载器包括两种,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

所有派生于抽象类ClassLoader的类加载器划分为自定义类加载器。

1、启动类加载器(引导类加载器)

  1. 启动类加载器是使用C/C++语言实现的,嵌套在JVM内部
  2. Java的核心类库都是使用引导类加载器加载的,比如String。
  3. 没有父加载器
  4. 是扩展类加载器和应用程序类加载器的父类加载器
  5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类 

2、扩展类加载器

  1. java语言编写
  2. 派生于ClassLoader类
  3. 父类加载器为启动类加载器
  4. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载

3、应用程序类加载器(系统类加载器)

  1. java语言编写
  2. 派生于ClassLoader类
  3. 父类加载器为扩展类加载器
  4. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  5. 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的
  6. 通过ClassLoader.getSystemClassLoader()方法可以获得该类加载器

五、双亲委派机制

1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,二十把这个请求委托给父类的加载器去执行;

2、如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;

3、如果父类加载器可以完成加载任务,就成功返回,如果父类不能完成加载任务,子加载器才会参数自己去加载,这就是双亲委派机制; 

六、沙箱安全机制

七、对类加载器的引用

JVM必须知道一个类型是由启动类加载器加载器的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

八、类的主动使用和被动使用

主动使用的七种情况:

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandler实例的解析结果

除了以上七种情况,其它使用Java类的方式都被看做是对类的被动使用,都不会导致类的初始化。

/**
* @startTime 2021-03-14 08:30
* @endTime 2021-03-14 11:30
* @start P37沙箱安全机制 
* @end P43解决PC寄存器的两个面试问题
* @efficiency (P42-P25)/2 = 9 * 10.8 = 97分钟/天
* @needDays 3841/97 = 40天
* @overDay 2021-03-13 + 40天 = 2021-04-21
*/

第二章 运行时数据区

一、运行时数据区内部结构

二、PC寄存器

1、概念

JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能运行。

这里,并非广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

2、作用

PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。

3、PC寄存器介绍

在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程的生命周期保持一致。

4、使用PC寄存器存储字节码指令地址有什么用?

因为CPU需要不停的切换各个线程,这时候切换回来以后,就知道接着从哪里开始继续执行。

JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。 

5、CPU时间片

​/**
* @startTime 2021-03-16 21:00
* @endTime 2021-03-16 22:30
* @start P44虚拟机栈
* @end P52操作数栈
* @efficiency (P52-P25)/4 = 6.75 * 10.8 = 72.9分钟/天
* @needDays 3841/72.9 = 53天
* @overDay 2021-03-13 + 53天 = 2021-05-03
*/

三、虚拟机栈

1、虚拟机栈出现的背景

由于跨平台性的设计,Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

2、Java虚拟机栈是什么

Java虚拟机栈,早起也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。

生命周期和线程一样。

主管Java程序的运行,它保存方法的局部变量表、部分结果,并参与方法的调用和返回。

3、栈中可能出现的异常

(1)采用固定大小的Java虚拟机栈,如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机会抛出一个StackOverflowError异常。

(2)采用动态的栈大小,如果无法申请到足够的内存时,会抛出OutOfMemoryError异常。

4、栈的运行原理

5、栈帧的内部结构

四、局部变量表

1、局部变量表的大小

局部变量表的大小是在编译期确定下来的,在方法运行期间是不会改变局部变量表的大小的。

2、Slot

局部变量表的存储单位是Slot(变量槽)

局部变量表中存放编译期可知的各种基本数据类型,应用类型。

在局部变量表里,32位以内的类型只占用一个slot,64位的类型(long+double)占用两个slot。

如果当前栈帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照表顺序继续排序。这就是为什么构造函数和实例方法中可以调用this,而static方法中不能调用this的真正原因,因为this存放在局部变量表中。

3、补充说明

局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

五、操作数栈

1、操作数栈是用数组实现的,后进先出,按顺序存放,有索引。

2、操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈push/出站pop。

3、操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。

4、操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

5、虽然操作数栈是由数组实现的,但操作数栈并非采用访问索引的方式进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

/**
* @startTime 2021-03-23 21:30
* @endTime 2021-03-23 23:00
* @start P53涉及操作数栈的字节码指令分析
* @end P64本地方法栈的理解
* @efficiency (P64-P25)/11 = 3.5 * 10.8 = 38分钟/天
* @needDays 3841/38 = 101天
* @overDay 2021-03-13 + 101天 = 18+30+31+10 = 2021-06-22
*/

六、动态链接

1、动态链接

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:,描述一个方法调用了另外的方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

2、方法的调用:虚方法与非虚方法

(1)如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法就称为非虚方法。

(2)静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。

(3)其它方法称为虚方法。

3、动态类型语言和静态类型语言

编译器进行对类型的检查的是静态类型语言;

运行期进行类型的检查的是动态类型语言;

Java属于静态类型语言,但由于invokedynamic指令,Java兼备一些动态语言的特性。

静态类型语言判断变量自身的类型信息,动态类型语言判断变量值的类型信息;

比如Java语言:String name = "素小暖";

动态类型语言:

JavaScript:var name = "素小暖";var info = 素小暖;

Python:info = 130.5;

4、方法重写的本质

  1. 找到操作数栈的第一个元素所执行的对象的实际类型,记作C;
  2. 如果在类型C中找到与常量中的描述符合简单名称都符合的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

5、IllegalAccessError介绍

程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

七、方法返回地址

1、方法返回地址存的是调用该方法的PC寄存器的值。

八、一些附加信息

九、本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

在Hotspot虚拟机中,直接将本地方法栈和虚拟机栈合二为一。

 

往期精彩内容:

Java知识体系总结(2021版)

Java多线程基础知识总结(绝对经典)

超详细的springBoot学习笔记

常见数据结构与算法整理总结

Java设计模式:23种设计模式全面解析(超级详细)

Java面试题总结(附答案)

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值