深入理解JAVA虚拟机读书笔记

JVM 的组成部分

  1. 类加载器子系统:将编译好的 class 文件加载到 jvm 中
  2. 运行时数据区: 存储 jvm 运行过程中产生的数据
  3. 执行引擎: 包括即时编译器和垃圾回收器
  4. 本地接口库
  5. 本地方法库

在这里插入图片描述

JAVA内存区域与内存泄漏

内存区域

1、程序计数器

计数器记录的是正在执行的虚拟机字节码指令的地址

2、java虚拟机栈

每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接(例如多态就要动态链接以确定引用的状态)、方法出口等信息。

3、本地方法栈

java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

4、java堆

java heap存放对象实例,也被称为GC堆。java堆可以处于物理上不连续的内存空间中,但是逻辑上是连续的(类似磁盘文件存储)。多数虚拟机出于实现简单,会要求java堆处于连接空间。

5、方法区

也被叫做Non-Heap,它用于存储已被虚拟机加载的类信息、常量(final )、静态变量(static)、即时编译器编译后的代码等数据。

6、运行时常量区
直接内存

使用native函数库直接分配的堆外内存。

NIO中,ByteBuffer有个方法是allocateDirect(int capacity) ,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

JAVA对象

对象的创建

​ (1)一般都采用指针碰撞(已分配内存和未分配内存的分界线),当new一个对象时,移动指针。

​ (2)当需要new一个较大文件时,单个空闲内存不够,采用空闲列表,在空闲列表中选择多个空闲块。

具体采用那种方式需要看java堆的GC机制,当GC带有空间压缩能力(GC时移动内存块)时选择第一中方式。像CMS这种基于清除算法的GC机制,理论上只能采用空闲表(但是实际设计中,CMS设计了一种分配缓冲区,会先在空闲列表中收集较大的内存空间,也能只能采用指针碰撞的方式进行内存分配)。

​ 注意:内存分配存在线程安全问题

​ 解决办法:(1)采用CAS加上失败重试的方式解决

​ (2)每个线程在创建时预先分配一小块内存,本地线程分配缓存(Thread local allocation buffer TLAB),在缓存空间用完了,使用锁的方式分配新的内存。(开启TLAB对象在实例化时可以不赋初始值)

对象的内存布局

(1)对象头

​ 1)Markword 用于存储hashcode、GC分代年龄、锁状态码、线程持有的锁,偏向线程ID,偏向时间戳等

​ 2)类型指针,用于确定该对象时那个类的实例

(2)实例数据

(3)对齐填充

​ hotspot任何对象的大小都必须是8字节的整数倍,对象头大小为8字节的1-2倍,如果实例数据不是8字节的倍数,需要做填充

对象的定位

​ 在java栈的本地变量表中的reference数据来中,一般有2种方式

​ 1)reference指向句柄池中的句柄,句柄中包含对象实例数据的指针

​ 2)reference直接指向对象实例数据的指针。

​ 第一种方式的好处的reference指向的句柄地址是稳定的,对象移动,句柄地址不会变(改变的句柄存储的指针内容)

​ 第二种方式的好处的效率高,没有中介。

垃圾收集器与内存分配策略

GC机制

判断对象是否存活

1)引用计数法:一个地方引用计算器+1,引用失效-1,当引用计数=0时,对象就不会在被使用了(经验)

2)可达性分析算法:GCRoots作为根节点,不在引用链上的对象被回收。

GC算法
标记清除算法Mark-Sweep

先标记出可以回收的对象,标记完成后统一回收

缺点:1)大量标记和清除工作随着对象数量上升导致效率越来越慢

​ 2)会产生大量不碎片空间,导致大对象无法找到合适空间提前触发垃圾收集

标记复制算法

把内存按容量分成2个区域,当半区内存用完时,把还存活的对象复制到另一个半区,并清除当前半区内存。

(目前商用虚拟机的都采用这种算法去回收新时代,因为新生代98%对象都会在第一轮回收)

缺点:1)在对象存活率高的应用场景下,需要进行大量的复制操作。效率低。

​ 2)需要一倍的额外空间

标记整理法

让所有存活对象向内存的一端移动,不直接清除可回收对象,而是当移动完成时直接清除掉存活对象边界后的内存空间。(一般用于老年代回收)

缺点:进行移动的时候需要暂停全部用户进程。(Stop the world)问题

权衡方法:在老年代中先采用标记清除算法,当内存碎片化达到一定影响后再采用标记整理法。(CMS收集器)

HotSpot的GC实现

根节点枚举(OopMaps)

​ 根节点(GC Roots)枚举也需要暂停整个用户进程,如果需要去全局引用和执行上下文中去找根节点,在庞大的java应用中,无疑是非常慢的,所以HotSpot采用OopMaps来记录引用。一但类完成加载,hotspot就会在Oopmap中把对象在什么偏移量上是什么类型的数据记录下来,并且记录那些位置是引用。

安全点(SafePoint)

HotSpot不可能为每条指令都生成OopMaps(耗费大量内存),只有在特定位置才会生成OopMaps,被称为安全点。安全点的选择是根据指令序列的复用,列如方法调用、循环跳转、异常跳转等指令序列复用,具有这些功能的指令才会产生安全点。

​ 中断存在问题:多线程情况下,怎么让所有线程都跑到最近的安全点停顿下来。

​ 解决办法:1)抢断式中断:系统先中断全部线程,发现有线程不在安全点上,先让这个线程恢复,跑到安全点上再中断,最后一起响应GC。(目前没有虚拟机采用)

​ 2)主动式中断:设置一个标志位,各个线程轮询这个标志位。标志位为真是线程在最近的安全点中断,主动挂起。轮询标志和安全点的位置重合,在安全点查看标志位

​ (hotspot采用test指令产生轮询效果,在安全点后面插入一条test指令,当需要中断用户线程的时候把内存页设置为不可读,test指令执行就会产生自陷指令,然后挂起)

存在问题:安全点能解决暂停用户线程问题。但是当用户线程不执行的时候(如没有分配的时间片)就没法执行到安全点,不能响应虚拟机的中断请求。

安全区域

​ 安全区域是确保在一段代码中,引用关系不会发生变化。因此在这个区域中,任何时候发生GC都是安全的。

当线程要离开安全区域时,它要检查虚拟机是否完成了根节点的枚举,如果完成了继续执行,没有完成中断等待。

记忆集与卡集

​ 为了解决跨代引用的时,新生代扫描GC roots时需要扫描整个老年代。创建了一个新的数据结构-记忆集。

记忆集的实现:

​ 1)字节精度:每条记录对应一个字长。表示这个字长包含跨代引用。

​ 2)对象精度:每条记录对应一个对象,记录这个对象的指针。表示这个对象包含跨代引用。

​ 3)卡精度:每条记录对应一个卡表,表示这个卡表区域包含跨代引用。

​ CARD_Table [this address>>9] = 1; 来标记卡表。可以看出一个卡表包含2^9即512个字节。表示这512个字节空间中包含跨代引用。(一般都采用卡集记录,节省维护空间)

写屏障

​ 为了解决实现卡集,在何时写入卡表等问题。Hotspot采用写屏障,应用AOP思想。把维护卡表看作是java赋值操作的一个AOP切面。实现在每次赋值操作时,更新卡表。

并发的可达性分析

​ 垃圾收集器判断对象是否死亡采用可达性分析。而且可达性分析必须暂停全部用户线程,对这个操作的优化十分有必要。对于GCroot的收集由于使用了OopMaps,暂停的时间是相对固定且短暂的。根据GCroots继续遍历对象图,会随着java堆的庞大,停顿时间越来越长。所以设计收集器进程与用户进程并发执行。

​ 采用三色标记对象: 1)白色:还未被收集器扫描过

​ 2)灰色:被收集器扫描过,但至少还存在一个引用还没被收集器扫描(扫了一半)

​ 3)黑色:全部引用都被收集器扫描

存在问题:对象消失(还被引用的对象被标记为了白色)

产生原因:1)赋值器插入一条或者多条从黑色对象到白色对象的引用

​ 2)赋值器删除了全部从灰色对象到该白色对象的引用

解决办法:1)增量更新:

​ 在收集器执行过程中,把赋值器插入黑色对象到白色对象的引用记录下来,在这个图扫描结 束时,把黑色对象当作根节点再扫描一次。

​ 2)原始快照:

​ 在收集器执行过程中,把赋值器删除灰色对象到白色对象的引用记录下来,在这个图扫描结 束时,把灰色对象当作根节点再扫描一次。

收集器发展历程

Serial/Serial Old

新生代采用标记复制算法,老年代采用标记整理算法,执行过程暂停整个用户进程

PerNew/Pernew Old

Serial的多线程版,新生代或者老年代执行算法采用多线程。

Parallel Scavenge

通过动态调整新生代空间(空间越小会导致收集频率变得频繁)来调整收集器的吞吐量=用户运行代码的时间/(用户运行代码的时间+垃圾收集的时间)

Parallel Old

Parallel Scavenge的老年代版本,支持多线程并行收集,采用标记整理算法

CMS(Concurrent Mark Sweep)

​ CMS采用标记清除算法,可以实现垃圾收集与用户进程并发执行,主要包括4个阶段:

1)初始标记:只标记与GCroots直接关联的对象

2)并发标记:从初始标记标记的对象遍历整个对象图

3)重新标记:解决并发标记发生的对象消失问题,采用增量更新

4)并发清除:清理需要删除的空间,由于不需要移动,可以并发执行

(初始标记和重新标记仍然需要暂停整个用户进程)

Garbage First(G1)

​ G1收集器的回收范围不再是整个java、新生代或者老年代。而是先把java堆分成一个个大小相等的独立区域(Region),每个Region可以是新生代或者老年代,还有专门存放大对象的Humongous Region。G1收集器建立可以可预测的停顿时间模型,根据统计学给每个回收区域一个优先级,优先回收有价值的区域(能回收垃圾多的)来保证暂停时间的稳定。

低延迟垃圾收集器

​ 4TB以下的堆容量暂停时间不超过10ms

Shenandoah收集器

Shenandoah堆的内存分布和G1类似,也是基于Region。但shenandoah不采用记忆集去解决跨代引用,而是采用全局连接矩阵。

1)初始标记:与G1一样只标记与GCroots直接关联的对象,需要暂停整个用户进程

2)并发标记:从初始标记标记的对象遍历整个对象图,并发进行

3)最终标记:与G1一样,处理剩余的SATB扫描,并统计回收价值最高的region,将这些Region统计成一组回收集。会有一小段时间的暂停。

4)并发清除:清理那些没有一个存活对象的Region

5)并发回收:把回收集Region里面还存活的对象复制到还未分配的region里面,需要与用户进程同步进行。采取读屏障和转发指针来解决并发会引发的错误。

6)初始引用更新:并发回收阶段复制到新区域的对象,需要把旧的引用修正到新的引用地址。初始引用更新不进行修改,只是建立一个线程集合点,确保并发回收的线程都已经完成。

  1. 并发引用更新:更新引用

8)最终引用更新:修正GCroots中的引用问题,需要短暂停用户进程。

9)并发清理:并发清理回收集中的Region。

​ ZGC

​ 并发整理阶段,ZGC采用染色指针,直接把值存在在引用指针的高4位地址中,不需要额外的存储空间。

JAVA与线程

线程和进程

进程有独立代码和数据空间,进程的切换会有比较大的开销。

线程共享进程的数据空间,有独立的运行栈和程序计数器,是最小的cpu调度单位。

内核线程的实现

1:1实现,内核线程直接由操作系统支持的线程。内核通过操作调度器对线程进行调度,调度器负责将线程的任务映射到各个处理器上。程序一般不会直接使用内核线程,而是使用内核线程的高级接口-轻量级进程。每个轻量级进程对应一个内核线程。

用户线程的实现

不需要内核支持的,需要用户自己实现线程的创建、销毁、切换和调度。

java线程的实现

一般直接直接映射操作系统线程。

协程

内核线程的局限,线程调度时候用户态到核心态的状态切换,需要响应中断、保护和恢复执行现场,需要高额的成本。所以内核线程的数目很有限。而高并发的情况下,可能由百万计的线程灌入线程池。于是引入协程。

有栈协程:完整的实现调用栈的保护、恢复工作。

无栈协程:本质是一个有限状态机,状态保存在闭包里,比有栈协程轻量,但功能有限。

java协程

纤程,有栈协程的一种特例。

线程安全

线程安全的实习方法

互斥同步(悲观锁)

java里添加synchronized。添加synchronized关键字编译后,会在代码段前后添加monitroenter和monitorexit指令,这两个指令都指向一个reference参数。monitorenter指令会对reference参数+1,monitorexit会对reference-1,当reference=0时,锁释放。

RenntrantLock时java.util.concurent.locks.Lock接口的一种实现。支持:
等待可以中断:有持有锁的线程长期不释放锁,正在等待的线程可以放弃等待,处理其他事情。

公平锁:多个线程等待同一个锁时候,需要按申请时间依次获得锁。会降低吞吐量。

锁可以绑定多个条件:

非阻塞同步(乐观锁)

使用原子操作来保证同步,如CAS

无同步方案

代码本身无共享数据,不需要同步。

锁优化

自旋锁

让线程空转,等到资源被释放,再继续执行。可以避免频繁的上下文切换。

锁消除

通过逃逸分析,判断数据是否时线程私有的,如果是私有无需加锁。

锁粗化

循环内部频繁的加锁,释放锁,大量的互斥同步导致性能消耗。虚拟机扩大锁的范围,把加锁同步扩大到整个操作序列。

轻量级锁

对象头分两部分,第一部分(mark word)储存hash码,分代年龄等。第二部分存储指向方法区对象类型的指针。mark word中有2bit的标志位,01表示未锁定,00表示轻量级锁定(并记录了指向调用栈的指针),01表示重量级锁(记录重量级锁的指针),11GC标记。每个线程的栈帧中建立了一个锁记录(Lock Record),储存锁对象目前的mark word拷贝。

锁膨胀

当标志位为01时,虚拟机可以用CAS操作把mark word更新为指向Lock Redcord的指针。成功则表示获得锁,标志位改为00。失败后会先比较markword和lock record中的记录是否相同,不同则说明别的线程占用了锁对象。这个时候就需要锁膨胀,膨胀为重量级锁,并把标志位改为10。

偏向锁

锁会偏向第一个获取锁对象的线程,如果没有其他线程请求锁,这个线程就可以一直执行。第一个获取锁对象的线程会把mark word中的1bit的偏向模式位改为1,标志位改为01,并用CAS把线程id记录到mark word中,成功则表示进入偏向模式。后续操作无需同步。一但出现另一个线程获取锁对象,偏向模式位立马变为0。

轻量级锁是把锁对象的位置放在了markword中,可以直接读取到,所以快。重量级锁markword记录的是指向重量级锁对象的指向,还需要去读取。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java虚拟(JVM)是Java程序的运行环境,它负责将Java字节码转换为可执行的器码并执行。深入理解Java虚拟涉及了解JVM的工作原理、内存管理、垃圾回收、类加载制等方面的知识。 首先,JVM的工作原理是通过解释器或即时编译器将Java字节码转换为器码并执行。解释器逐条执行字节码指令,而即时编译器将字节码转换为本地器码,以提高程序的执行效率。 其次,内存管理是JVM的重要任务之一。JVM将内存分为不同的区域,包括堆、栈、方法区等。堆用于存储对象实例,栈用于存储局部变量和方法调用信息,方法区用于存储类的信息。JVM通过垃圾回收制自动回收不再使用的对象,释放内存空间。 此外,类加载制也是深入理解JVM的关键内容之一。类加载是将类的字节码加载到内存中,并进行验证、准备、解析等操作。类加载器负责查找并加载类的字节码,而类加载器之间存在着父子关系,形成了类加载器层次结构。 还有其他一些与性能优化、调优相关的内容,如即时编译器的优化技术、垃圾回收算法的选择等,也是深入理解Java虚拟的重要方面。 总的来说,深入理解Java虚拟需要对JVM的工作原理、内存管理、垃圾回收、类加载制以及性能优化等方面有较深入的了解。掌握这些知识可以帮助开发人员编写出更高效、稳定的Java程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值