JVM系列——字节码

第8章 虚拟机字节码执行引擎

1、运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元, “栈帧”(Stack Frame) 则是用于支持虚拟机进行方法用和方法执行背后的数据结构, 它也是虚拟机运行时数据区中的虚拟机栈(Virtual MachineStack)的栈元素。 栈帧存储了方法的局部变量表、 操作数栈、 动态连接和方法返回地址等信息 。

栈帧的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yzhBlnlc-1646402102747)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20210913204236587.png)]

局部变量表

局部变量表(Local Variables Table) 是一组变量值的存储空间, 用于存放方法参数方法内部定义的局部变量。

局部变量表的容量以变量槽(Variable Slot) 为最小单位, 其大小随操作系统改变。每个变量槽都应该能存放一个boolean、
byte、 char、 short、 int、 float、 reference或returnAddress类型的数据 。

局部变量表中的变量槽是可以重用的, 方法体中定义的变量, 其作用域并不一定会覆盖整个方法体, 如果当前字节码PC计数器的值已经超出了某个变量的作用域, 那这个变量对应的变量槽就可以交给其他变量来重用。

操作数栈

操作数栈(Operand Stack) 也常被称为操作栈, 它是一个后入先出(Last In First Out, LIFO)栈。 最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配, 在编译程序代码的时候, 编译器必须要严格保证这一点, 在类校验阶段的数据流分析中还要再次验证这一点。

栈帧中的数据共享

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LmuOwwLi-1646402102748)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20210913204703323.png)]

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用, 持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking) 。

字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用, 这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用, 这部分就称为动态连接

方法返回地址

正常调用完成:返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定

异常调用完成:法执行的过程中遇到了异常, 并且这个异常没有在方法体内得到妥善处理

在方法退出之后, 都必须返回到最初方法被调用时的位置, 程序才能继续执行, 方法返回时可能需要在栈帧中保存一些信息, 用来帮助恢复它的上层主调方法的执行状态

2、方法调用

方法调用并不等同于方法中的代码被执行, 方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法) , 暂时还未涉及方法内部的具体运行过程。

解析

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用

方法调用字节码:

invokestatic。 用于调用静态方法。
invokespecial。 用于调用实例构造器()方法、 私有方法和父类中的方法
·nvokevirtual。 用于调用所有的虚方法。
·nvokeinterface。 用于调用接口方法, 会在运行时再确定一个实现该接口的对象。
invokedynamic。 先在运行时动态解析出调用点限定符所引用的方法, 然后再执行该方法。 前面4条调用指令, 分派逻辑都固化在Java虚拟机内部, 而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。

分派

1.静态分派

// 实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 静态类型变化
sr.sayHello((Man) human)
sr.sayHello((Woman) human)  

所有依赖静态类型决定方法执行版本的分派动作, 都称为静态分派。 静态分派的最典型应用表现就是方法重载。 静态分派发生在编译阶段, 因此确定静态分派的动作实际上不是由虚拟机来执行的

解析与分派这两者之间的关系并不是二选一的排他关系, 它们是在不同层次上去筛选、 确定目标方法的过程

2.动态分派

它与Java语言多态性的另外一个重要体现——重写(Override) 有着很密切的关联。

要弄清楚它是如何确定调用方法版本、 如何实现多态查找 。 invokevirtual指令的运行时解析过程大致分为以下几步:
1) 找到操作数栈顶的第一个元素所指向的对象的实际类型, 记作C。
2) 如果在类型C中找到与常量中的描述符简单名称都相符的方法, 则进行访问权限校验, 如果通过则返回这个方法的直接引用, 查找过程结束; 不通过则返回java.lang.IllegalAccessError异常。
3) 否则, 按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
4) 如果始终没有找到合适的方法, 则抛出java.lang.AbstractMethodError异常。

3.单分派与多分派

根据分派基于多少种宗量, 可以将分派划分为单分派多分派两种。 单分派是根据一个宗量对目标方法进行选择, 多分派则是根据多于一个宗量对目标方法进行选择。

静态分派属于多分派类型 ,动态分派属于单分派类型。

4.虚拟机动态分派的实现

动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法, 真正运行时一般不会如此频繁地去反复搜索类型元数据。

建立一个虚方法表(Virtual Method Table, 也称为vtable, 与此对应的, 在invokeinterface执行时也会用到接口方法表——Interface Method Table, 简称itable) , 使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。 如果某个方法在子类中没有被重写, 那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的, 都指向父类的实现入口。 如果子类中重写了这个方法, 子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址

第9章性能调优

调优步骤:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SL0ATBEi-1646402102748)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20210923164732210.png)]

1、寻找性能瓶颈

资源主要消耗在CPU、IO文件、网络IO以及内存方面

CPU消耗分析

三个重要概念:

​ 上下文切换:Linux系统在执行线程切换,在切换时要储存目前线程的执行状态,并恢复要执行的线程的状态的过程

​ 运行队列:每个CPU都维护了一个可运行的线程队列

​ 利用率:CPU利用率为CPU在用户进程、内核、中断处理、IO等待以来及空闲五个部分使用百分比。

top指令:Linux系统中,该指令可以查看CPU的消耗情况

pidsta指令:需安装SYSSTAT,可以在console每面输出CPU消耗情况

CPUi消耗严重主要体现在us、sy值上

us:该值过高时,表示允许的应用消耗了大部分的CPU,主要原因时线程一直处于Runnable状态

**sy:**当该值过高。表示Linux系统花费了更多的时间在进行线程切换,主要原因是,线程数量过多,采用Kill-3[javapid]或者jstack-1[javapid]的方式dump出线程信息,找出等待状态或者锁竞争过多的线程

文件IO消耗分析

Linux系统中,跟踪文件IO的消耗,通过pidstat来查找,iostat可查看各个设备的IO历史状况

当文件IO消耗过高时,对于Java应用最重要的时找到造成文件IO消耗高的代码,通过pidstat直接找出IO操作多的线程

网络IO消耗分析

尤其注意网卡中断是不是均衡地分配到各CPU中。

sar:该指令可以输出网络IO的消耗情况,主要的数值为tcpsck、udpsck。查看tcp/ip,可通过tcpdump查看。

由于JVM内存带下通常是有限的,Java程序一般不会造成网络IO消耗严重

内存消耗分析

内存的消耗主要在JVM堆上,除了关注的就是内存的消耗,最为值得关注呃是swap的消耗以及物理内存的消耗

vmstat:其中的信息和内存相关的主要是memory下的swpd、free、buff、cache、以及swap下的si和so。

swpd值过高,通常是由于物理内存不够用导致的

物理内存消耗过高可能是JVM内存设置过大,创建的java线程过多或者Dierct ByteBuffer王物理内存中放置了过多的对象造成的

sar :可以查看内存的消耗情况

top:可以查看进程所消耗的内存量,不过top可以看到java进程的消耗内存是包括了JVM已分配的内存加上java应用所耗费的JVM以外的物理内存。

pidstat:可以查看进程所消耗的内存量,可查看进程所炸药的物理内存和虚拟内存的大小

程序执行慢的原因分析

​ 1、锁竞争激烈:线程池提供较少的线程,而存在较多的线程

​ 2、为充分使用硬件资源

​ 3、数据量增长

2、调优

代大小调优

​ 最关键参数:

-Xms , -Xmx通常设置为相同的值,避免运行时不断地扩展JVM内存空间,该值决定了最大地堆空间

-Xmn 决定了新生代空间大小,-XX:SurvivorRatio可以设置Eden、s0和s1三个区域地比率

-XX:MaxTenuringThreshold 控制对象经历多少次Minor GC后转入旧生代

1、避免新生代带大小设置过小

2、避免新生代带大小设置过大

3、避免Survivor去过小或者过大

4、合理设置新生代存货周期

GC策略调优

串行GC太差、实际场景一般选用并发GC。

对Web应用而言,G1还不够成熟地情况下,CMS GC为最佳选择

CPU消耗严重的解决办法

us高的解决办法

1、对这种线程动作增加Thread.sleep,以释放CPU的执行权,降低CPU功耗。降低了程序的性能

2、通过JVM调优或者程序调优,降低GC执行次数

sy高的解决办法

1、减少线程数

2、降低线程间的锁竞争

3、采用协程

文件IO消耗严重解决方法

1、异步写文件

2、批量读写

3、限流

4、限制文件大小

内存消耗严重情况

1、释放不必要的引用

2、使用对象缓存池

3、采用合理的缓存失效算法

4、合理使用弱引用和虚引用

锁竞争激烈

1、使用并发中的类

2、使用Treiber算法

3、使用Michael-Scott非阻塞队列算法

4、尽可能减少使用锁

5、拆分锁

6、去除读写操作的互斥锁

为充分使用硬件资源

1、未充分使用CPU

2、未充分使用内存

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值