JVM原理篇

有什么用?

1.实现高效编程

学习java虚拟机是如何让代码执行的,并了解其优化的手段,在程序员编码过程中更容易地写出高效的代码

2.更精准的调优

通过学习JIT、垃圾回收器的原理,让调优变得更准确,也更容易理解HotSpot中的垃圾回收器的参数

3.不仅仅是学习了java的原理

学习java虚拟机的原理=学习了语言的通用性

运行在Jvm上的Groovy、Kotlin、Go语言的垃圾回收

栈上的数据存储

Java中的八大类型在虚拟机中的实现

boolean、byte、char、short在栈上是不是存在空间浪费?

是的,java虚拟机用了空间换时间的方案。

 案例:验证boolean在栈上的存储方式

栈中的数据要保存到堆上或者从堆中加载到栈上时怎么处理?

案例:验证boolean从栈保存到堆上只取最后一位

对象在堆上时如何存储的

对象在堆中的内存布局

指的是对象在堆中存放时的各个组成部分,主要分为以下几个部分:

标记字段

标记字段相对比较复杂,在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的,同时在64位(又分为是否开启指针压缩)、32位虚拟机中的布局都不同。以64位开启指针压缩为例:

元数据的指针

Klass pointer元数据的指针指向方法区中保存的InstanceKlass对象:

指针压缩

在64位的Java虚拟机中,Klass Pointer以及对象数据中的对象引用都需要占用8字节,为了减少这部分的内存使用量,64位Java虚拟机使用指针压缩技术,将堆中原本8个字节的指针压缩成4个字节,此功能默认开启,可以使用-XX:-UseConpressedOops关闭

指针压缩的思想是将寻址的单位放大,比如原来按1字节去寻址,现在可以按8字节寻址。如图。

现实的应用场景

这样将编号当做地址,就可以用更小的内存访问更多的数据。但是存在问题:

1.需要进行内存对齐,指的是将对象的内存占用填充至8字节的倍数。存在空间浪费(对于Hotspot来说不存在,即便不开启指针压缩,也需要进行内存对齐)

2.寻址大小仅仅能支持2的35次方个字节(32GB,如果超过了32GB指针压缩会自动关闭)。不同压缩指针,应该是2的64次方=16EB,用了压缩指针就变成了8字节=2的3次方*2的32次方=2的35次方

案例:在HSDB工具验证Klass pointer正确性

内存对齐

内存对齐主要目的是为了解决并发情况下CPU缓存失效的问题:

内存对齐之后,同一个缓存行中不会出现不同对象的属性。在并发情况下,如果让A对象一个缓存行失效,是不会影响到B对象的缓存行的

内存对齐---字段重排列

在Hotspot中,要求每个属性的偏移量Offset(字段地址-起始地址)必须是字段长度的N倍。

如果不这样的话,CPU的缓存行就会出现问题

如果不满足要求,会尝试使用内存对齐,通常在属性之间插入一块对齐区域达到目的。

如下图中:

案例:子类和父类的偏移量

就是先将父类的先放到一块,再把自己的放到下一块

总结:

方法调用的原理

方法调用的本质就是通过字节码指令的执行,能在栈上创建栈帧,并执行调用方法中的字节码执行

以invoke开头的字节码指令的作用是执行方法的调用

在JVM中,一共有五个字节码指令可以执行方法调用:

Invoke方法的核心作用就是找到字节码执行并执行

静态绑定

1.编译期间,invoke指令会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名+方法名+返回值+参数

2.在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定

静态绑定适用于处理静态方法、私有方法、或者使用final修饰的方法,因为这些方法不能被继承之后重写

动态绑定

对于非static、非private、非final的方法,有可能存在子类重写方法,那么就需要通过动态绑定来完成方法地址绑定的工作。比如在这段代码中,调用的其实是Cat类对象的eat方法,但是编译完之后虚拟机指令中调用的是Animal类的eat方法,这就需要在运行过程中通过动态绑定找到Cat类的eat方法,这样就实现了多态

动态绑定石基于方发表来完成的。

总结:

异常捕获的原理

在java中,程序遇到异常时会向外抛出,此时可以使用try-catch捕获异常的方式将异常捕获并继续让程序按程序员设计好的方式运行。比如:

异常表在编译期生成,存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生之后跳转到的字节码指令位置

起始/结束PC:此条异常捕获生效的字节码起始/结束位置

跳转PC:异常捕获之后,跳转到的字节码位置

程序运行中触发异常时,java虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码索引值在某个异常表条目的监控范围内,java虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配

1.如果匹配,跳转到“跳转PC”对应的字节码位置

2.如果遍历完都不匹配,说明异常无法在当前方法被捕获,此方法栈帧直接弹出,在上一层的栈帧中进行异常捕获的查询

finally的处理方式就相对比较复杂一点了,分为以下几个步骤:

1.finally中的字节码指令会插入到try和catch代码块中,保证在try和catch执行之后一定会执行finally中的代码

2.如果抛出的异常范围超过了Exception,比如Error或者Throwable,此时也要执行finally,所以异常表中增加了两个条目。覆盖了try和catch两段字节码指令的范围,any代表可以捕获所有种类的异常

JIT即时编译器

在Hotspot中,有三款即时编译器,c1、c2和graal。

c1的编译效率比c2快,但是优化效果不如c2。所以c2适合优化一些执行时间较短的代码,c2适合优化服务端程序中长期执行的代码

c1和c2是协同工作的

JDK7之后采用了分层编译的方式,在JVM中c1和c2会一同发挥作用,分层编译将整个优化级分为了5个等级

c1和c2都有独立的线程去进行处理,内部会保存一个队列,队列中存放需要编译的任务。

一般即时编译器是针对方法级别来进行优化的,当然也有对循环进行优化的设计

详细看一下c1和c2是如何工作的

案例:测试JIT即时编译器的优化效果

常见的JIT即时编译器优化手段

主要优化手段是方法内联逃逸分析

案例:使用JIT Watch工具查看方法内联的优化结果

但是并不是所有方法都可以内联:

案例:String的toUpperCase方法性能优化

逃逸分析

逃逸分析指的是如果JIT发现在方法内创建的对象不会被外部引用,那么就可以采用锁消除,标量替换等方式进行优化。

锁消除

锁消除指的是如果对象被判断不会逃逸出去,那么在对象就不存在并发访问问题,对象上的锁处理就不会执行,从而提升性能

标量替换

逃逸分析真正对性能优化比较大的方式是标量替换,在java虚拟机中,对象中基本数据类型成为标量,引用的其他对象称为聚合量。标量替换指的是如果方法中的对象不会逃逸,那么其中的标量就可以直接在栈上分配

案例:逃逸分析的优化测试

JIT优化的几点建议

垃圾回收器原理

G1垃圾回收器原理

G1垃圾回收有两种方式:

1.年轻代回收

2.混合回收

年轻代回收

1.新创建的对象会存放在Eden区,当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行YoungGc

2.标记处Eden和Survivor区域中的存活对象

3.根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域

4.后续的YoungGC是与之前相同的,只不过Survivor区中存活对象会被搬运到另外一个Survivor区

5.当某个存活对象的年龄到达了阈值(默认15),将被放入老年代

6.部分对象如果超过了Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M,就被放入Humongous区,如果对象过大会横跨多个Region

7.多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时,会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成

年轻代回收纸扫描年轻代对象(Eden+Survivor),所以GC Root到年轻代的对象或者年轻代对象引用了其他年轻代的对象都很容易被扫描出来

但是这样做有缺点,如果对象太多这张表会占用很大的内存空间,存在错标(A---->F)的情况

优化

卡表

每一个Region都拥有一个自己的卡表,如果产生了跨代引用(老年代引用年轻代),此时这个Region对应的卡表上就会将字节内容进行修改,JDK8源码中0代表被引用了称为脏卡。这样就可以标记处当前Region被老年代中的哪些部分引用了。那么要生成记忆集就比较简单了,只需要遍历整个卡表,找到所有脏卡、

写屏障

JVM使用了些屏障技术,在执行引用关系建立的代码是,可以在代码前和代码后插入一段指令,从而维护卡表。

记忆集中不会记录新生代到新生代的引用,同一个Region中的引用也不会记录

年轻代回收

混合回收

初始标记

并发标记

可能会出现一些比较严重的问题:

为了解决这种问题使用了SATB技术

最终标记

转移

总结

ZGC原理

ZGC是一种可扩展的低延迟的垃圾回收器。ZGC在垃圾回收过程中,STW的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到16TB的堆大小,堆大小对STW的时间基本没影响

在G1垃圾回收器中,STW时间的主要来源是在转移阶段:

ZGC的执行流程

G1转移时需要停顿的主要原因

ZGC的解决方案

在ZGC中,使用了读屏障的技术,来实现转移后对象的获取。当获取一个对象引用时,会触发读后的屏障指令,如果对象指向的不是转移后的对象,用户线程就会将引用指向转移后的对象

着色指针

访问对象引用时,使用的是对象的地址,在64位虚拟机中,是8个字节可以表示接近无限的内存空间。所以一般内存中对象,高几位都是0没有使用。着色指针就是利用了这多余几位,存储了状态信息。

真正的应用程序使用8个字节去进行对象的访问,现在只使用了44位,不会出现问题吗?

应用程序使用的对象地址,只是虚拟内存,操作系统会将虚拟内存转换成物理内存。而ZGC通过操作系统更改了这层逻辑。所以不管颜色位变成多少,指针都指向同一个对象。

ZGC的内存划分

初始标记

标记GC Roots引用的对象为存活对象,数量不多,所以停顿时间很短

并发标记

遍历所有对象,标记可以到达的每一个对象是否存活,用户线程使用读屏障,如果发现对象没有完成标记也会帮忙进行标记

并发处理

选择需要转移的Zpage,并创建转移表,用于记录转移前对象和转以后对象地址

 

转移开始

转移GC Root直接关联的对象,不转移的对象remapped值设置为1,避免重复进行判断

并发转移

转移完成后把Zpage清理一下

最终的执行流程图

第二次垃圾回收的初始标记阶段

第二次垃圾回收的并发处理阶段

将转移映射表删除,释放内存空间

并发转移阶段---并发问题

分代ZGC的设计

核心技术

ShenandoahGC的原理

设计:ShenandoahGC很多是使用了G1源代码改造而成的,所以很多算法、数据结构的定义上,与G1十分相似,而ZGC是完全重新开发的一套内容

执行流程

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值