Fari

Jvm

《深入理解java虚拟机》第三版笔记

- 第一款商用虚拟机ClassicVM使用的是基于句柄的对象查找方式。这样做的目的是当对象移动时不需要修改对象引用的位置。缺点是需要两次定位对象。

- GraalVM,是一个再HotSpot的基础上增强而来的跨语言全栈虚拟机,可以运行java、Scala、Kotlin等基于jvm的语言,还有C、C++、Rust等基于LLVM的语言,同时还支持JS、Ruby、Python等。GraalVM可以无额外开销地混用这些语言

- 自JDK10起,HotSpot加入了全新的Graal编译器,顾名思义,它源于GraalVM。它本身就是用java编写的,目的是替换C2编译器

- NIO引入基于Channel与Buffer的IO方式,直接分配堆外内存,避免java堆与Native堆来回复制数据

- 对象分配内存使用指针碰撞或者空闲列表的方式,具体使用哪个取决于垃圾回收器的实现。不管哪种方式都可能出现并发问题,jvm通过CAS和TLAB解决

- new对象过程:检查类有没有被加载,如果没有则先执行 加载链接初始化的过程 -> 堆内存中分配地址并赋0值 -> 设置对象头信息包括GC分代、锁状态等 -> 执行对象构造器方法

- 对象的内容包括 对象头、实例数据、对齐填充

- 对象头信息中包含markword、类型指针(非必须)、数组长度(数组类型才有)

- 实例数据中保存着对象中的各个字段内容,包括从父类继承下来的

- 不停添加栈帧是报栈溢出还是内存溢出异常?

当栈内存固定时报栈溢出(hotspot),如果是动态扩展的(classic),则是内存溢出

- 基于分区的垃圾回收器(G1、CMS等)可能出现待清理的分区中含有别的分区所引用的对象,为了避免整堆扫描,会给每个区分配一个 Remember Set 用于记录哪些对象被其他区引用,它也将加入GCroot中进行扫描。记忆集只是一个抽象接口,它的一个实现是卡表,卡表记录了分区之间是否存在对象依赖关系(只记录两个分区的依赖,并不记录分区内部具体哪个对象的依赖),卡表本质上只是一个标记数组

- 卡表是如何维护的?

如果其他区的对象引用了本区中的对象,则那个区的卡表就应该被修改,形成脏表。问题是如何实现这一过程,HotSpot是使用写屏障(和内存屏障不同)实现的,所谓写屏障,可以理解为是aop的过程,即在对象引用这个动作前后加上一段维护卡表的代码

- 进行GCroot扫描时,由于栈空间也很大,如果直接扫也很浪费时间,所以会将对象的引用专门存储到一个名为 OopMap 的数据结构中,扫描时只扫它就够了

- ParNew是Serial多线程版本,Parallel Scavenge与ParNew差不多,区别在于PS更关注吞吐量(用户线程时间/总时间),而CMS、ParNew等关注缩短单次STW的时间,两者是不一样的,缩短单次的时间意味着收集的次数变多,而每次收集都会有额外开销,所以反而使得吞吐量降低了。

- G1垃圾收集器中,为每一个region都设计了两个名为TAMS的指针,在并发标记过程中,如果有新的对象需要分配内存,就会分配到这两个指针内,这两个指针内区域的对象默认都是存活的,不会被垃圾回收

- 三色标记法如何解决并发标记过程中的动态引用变化?

CMS使用增量更新算法实现:已经标记为黑色(所有的引用它的对象都已经扫描过)对象再引用其他对象时就会被修改为灰色

G1使用原始快照算法实现:有新的引用关系建立时,就记录下这个关系,后续重新扫描这些记录

- Shenandoah是一款只有OpenJDK才有的垃圾收集器,它能实现再任何大小的堆内存中都将STW时间控制在十毫秒内,对比于G1和CMS,它不仅能进行并发标记,还能进行并发内存整理,并且其没有年轻代和老年代的区别,并将G1的每个region维护的记忆集修改为一个“连接矩阵”,便于统一管理维护。其他基本和G1差不多

- Shenandoah如何实现并发整理?

在原本的对象头前面增加一个指向该对象的指针,正常情况下,该指针指向本身这个对象,但在并发整理时,会将该对象复制到其他地方,该指针就会指向新的那个对象地址。缺点是每次对象访问都是两次定位,类似于句柄访问对象的方式。Shenandoah使用CAS的方式保证这一过程的安全

- ZGC的目标和Shenandoah相似,都是要实现不影响吞吐量的情况下,将任意堆的收集时间控制在10ms。它也是基于Region并且不设分代。区别在于ZGC的region可以动态变化,并且ZGC的并发整理要优秀很多(见下)

- ZGC的并发整理的原理?

实际上,GC过程中,很多步骤不需要知道对象的具体内容,而只需要知道对象的状态,例如在三色标记过程中,只需要知道对象之间的依赖关系,而不需要知道对象的内容。传统的做法是将一些与对象本身无关的信息记录在对象头,例如GC分代年龄、锁信息等,但如此一来,就是你只需要对象头中的某些信息,你也必须找到该对象的具体位置(因为对象头也是对象的一部分),这样会造成多余的消耗。而ZGC就是直接将一部分信息存放到对象的指针上,这样只要知道对象的指针就够了,而不用真正去访问对象的内容。这种方法称之为“染色指针”

为什么可以这样做?

因为实际上,在64位系统中,一个指针的范围非常大,而实际上现在的cpu架构也只会用到部分长度(AMD64只支持到52位长度),所以很大一分部都浪费掉了,ZGC就是利用指针的这部分空间存储相关数据

- 向 HashMap<Long, Long> 添加一个kv会额外占多大内存?

long类型占8字节,而包装称Long类型,则需要添加8字节的Markword,8字节的Klass指针,需要24字节,两个Long则需要48字节,然后这两个Long组成一个Map.Entry对象后,又需要16字节的对象头(markword和klass指针),并且Entry需要一个8字节的next字段和一个4字节的hash值(另外需要4字节填充),这一共又是32字节,然后HashMap需要有一个指向该Entry的8字节指针,所以一共占了 48 + 32 + 8 = 88 字节

java中的动态链接

以下为我个人的理解:

在java中,有两个地方会提到动态链接,一不小心弄混了就会非常困惑

一个过程

假设我写了两个方法:

method2的字节码指令为:

其中 invokevirtual 表示执行的是一个虚方法,所谓虚方法,就是只有代码在运行期间才知道真正调用的是哪个方法(final 修饰的方法除外),而不是在编译期间就确定的。

后面的 #7 指的是常量池中字符串常量的引用,将引用具体化可以得到 Method method1:()I,后面得到的这一串字符就是符号引用

为什么这里执行 method1 是一个虚方法?因为你不确定子类或者父类是否重写了该方法,所以编译期间你根本不知道它调用的到底是哪个方法。只有真正运行后,知道是哪个对象调用的该方法后才能确定应该调用谁的方法

所以在真正调用该方法前,这个被调用的方法仅仅是一个符号引用,每次只有真正调用该方法时,才会将该引用转换为所调用方法的直接引用,这个运行时才确定直接引用的过程,就称之为动态链接。

注意:这里的动态链接是一个过程,或者说是一种思想

一个引用

JVM会在调用每一个方法前,都在方法栈头创建一个栈帧,该栈帧是一种能够支持函数调用所有信息的数据结构,它是一个具体的存在。

而栈帧中会保存一个指向常量池的指针,该指针实际指向常量池中该方法的实际引用。这个指针就是动态链接。

注意:这里的动态链接是一个指针,它是一个有形的实体

方法在调用时不是已经确定了自己的引用吗,为什么还要保存自己的引用?

  1. 便于获取栈信息:例如我们在debug或者保存栈快照时,它能告诉我们栈中到底有哪些函数

  2. 函数结束执行后可以知道它的返回值是哪个方法返回的

  3. 另一种说法是方法在调用时并不会一次性加载所有的方法信息,例如异常表等,只有真正出现异常后才去方法区加载该方法的异常表,那么这个过程肯定得知道该方法到底属于方法区的哪个方法。

参考:http://rabbit.eng.miami.edu/class/519/frames.html#:~:text=Stack%20Frames%20are%20an%20implementation,record%20is%20currently%20in%20use.

引自上文:Often, it is better to take a more abstract and high-level view of things. An Activation Record is a data structure that holds all the information needed to support one call of a function. It contains all the local variables of that function, and a reference (or pointer) to another activation record; that pointer is known as the Dynamic Link. Stack Frames are an implementation of Activation Records. The dynamic link corresponds to the “saved FP” entry; it tells you which activation record to return to when the current function is finished. The frame pointer itself is simply a way of indicating which activation record is currently in use. The dynamic links tie all the activation records for a program together in one long linked list, showing the order they would appear in a stack.

jvm学习笔记

基于栈和基于寄存器的指令集架构

Jvm前端编译器架构=都是= 基于栈 的指令集架构,与之对应的还有 基于寄存器 的指令集架构。### 基于栈的指令集架构

* 跨平台性好、指令集小、指令多、性能相较于寄存器更差
* 例:

file

基于寄存器的指令集架构

* 直接使用cpu的指令集,故执行幸能更好,但是移植性较差

Hotspot/JRocket/J9

JRocket:号称最快的虚拟机,专注于服务端,牺牲程序启动速度,因此其内部不包含解析器的实现,全部代码都即时编译器编译后执行

J9:运行IBM自己的软件时速度较快,J9最厉害的地方是它高度模块化,不但可以部署在桌面或服务器上,还可以部署到嵌入式环境中,例如CLDC级别的环境;这些环境用的是同一个J9核心VM,搭配上适用于具体环境的GC和JIT编译器。

hotspot:运用最广泛的虚拟机

类加载子系统

graph LR
加载 ---> 验证
subgraph 链接
验证 ---> 准备 ---> 解析
end
解析 ---> 初始化

类的加载过程

  1. 加载 加载二进制流并产生对应的Class对象。
  2. 链接 2.1 验证
    • 确保class文件的字节流满足当前虚拟机的要求,保证被加载类的正确性
    • 主要包含四种验证:文件格式验证(例如是否以魔数开头),元数据验证,字节码验证,符号引用验证 2.2 准备
    • 为类变量分配内存并且设置该类变量(static修饰的变量)的默认初始值,即零值。如 int i = 3,则在此阶段将i赋值为0。
    • 注:这里不包含被final修饰的类变量,它在编译的时候就已经赋零值了,在准备阶段会显示初始化,即 i 赋值为3。
    • 准备阶段不会为实例变量初始化,类变量会分配到方法区中,而示例变量则会随着对象一起分配到java堆中。 2.3 解析
    • 将常量池内的符号引用转换为直接引用的过程。
  3. 初始化
    • 初始化阶段就是执行类构造器方法()的过程。
    • 该方法不需要定义,由javac编译器自动生成(如果没有类变量或静态代码块就不会生成该方法)。不同于类的构造器,即()
    • 构造器方法中的指令语句按照源文件出现的顺序执行。
    • 若该类有父类,则一定要保证父类的()已经执行完毕了
    • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。

file

file

类加载器

file

  1. 虚拟机规范中规定类加载器只有两种,启动类加载器(bootstrap class loader)和用户自定义类加载器(凡是直接或间接继承自ClassLoader类的均为用户自定义类加载器)

  2. Bootstrap class loader使用的是c/c++编写,其他类加载器使用java编写。