深入理解JVM笔记

深入理解jvm
推荐书 Java性能优化权威指南
学习步骤:第三部分class和第四部分程序编译和代码优化,然后看class相关的视频;第五部分内存模型并发,然后视频内存模型和jvm指令等;第二部分内存管理全部, 然后视频的运行时数据区和调优
第一部分 虚拟机执行子系统
一、类文件结构

  1. 规范:java语言规范和java虚拟机规范,后者保证了任何语言只要编译成class这种字节码就能跑在jvm上的可能性,且其比java语言本省强大的多
    2.Class类文件结构
    (1)任何一个Class文件都对应一个类或者接口的定义,但是类或者接口定义不一定都在文件里(可以动态生成eg:cglib直接给类加载器)
    (2**)Class文件是8bit的字节为基础单位的二进制流,数据项紧密排列无缝隙,数据项>8字节则高位在前分割多个8字节单位**
    (3)无符号数是基本的数据类型 u1 u2 u4 u8分别代表 1 2 4 8字节的无符号数;表是由多个无符号数或者其他表作为数据项构成的复杂结构,_info结尾,整个class文件本质上也是表,以下class文件格式
    在这里插入图片描述

i. 魔数和Class文件版本号:头4字节被称为魔数,确定该文件是否是可被接受的class文件,固定为0XCAFEBABE;5-6字节为副版本号 jdk2-12均为未使用,7-8字节为主版本号,版本号jdk1为45,以后版本默认加1 如0x34=52即jdk8,版本号作用是高版本jdk可兼容低版本class文件,反之不行
在这里插入图片描述
ii.常量池
官网描述:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
8 9位字节为常量池u2的计数值,从1开始,如果是0则表示不引用任何常量池项目,除常量池索引外其他类型均从0开始,eg 0x0023=35,表示该常量池有35项常量,索引值是1-35,而非仅有35个字节
常量池主要存放两大:字面量和符号引用,常量池中每一项都是一个表,共以下17种类型的常量,表结构起始第一位均为u1的标志位
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

eg:第一个常量是0A=10 为方法描述常量,查看表可知,0x0006指向的是CONSTANT_Class_info的索引值,则在后面找第6个常量值,描述该方法的类的符号引用
在这里插入图片描述

javap -verbose能够展示class常量池
在这里插入图片描述

iii 访问标识符 access_flags u2 识别类或者接口层的访问信息:类还是接口? public?final?abstract?等等
一般的类就是public的,那么 值为0x0021=0x0001|0x0020
在这里插入图片描述
javap结果,ACC_SUPER只要是jdk1.2之后编译都为true

iv.访问标识符后是类索引、父类和接口索引
eg 0x0021表示类标识符,0x0008 0x0009 0x0000 表示当前类索引项8 父类索引项9 接口数为0,若接口数量不为0,则后面 接口类索引项
Image
其中 #8=Class表示该项是一个CONSTANT_Class_info,全限定性名指向27索引项 是个utf-8的字符串-- ;#9类似
Image
ps: 类全限定名 就是把类全名中的.换成/,然后为区分连续多个限定名再后缀一个; : personal/zsx/study/java8/JvmTest;
v. 字段标识
参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5-200-A.1
字段标识表中的descriptor_index,指向常量池中的索引值, 该索引值代表必须是utf_8的常量,描述符标识符如下,数组一般前置[
在这里插入图片描述

eg:
在这里插入图片描述
L表示对象类型,[I表示int[];
方法描述符:遵循的顺序是: 先参数列表后返回值的顺序描述
eg: public void test(); —> ()V
toString();---->()Ljava/lang/String;
vi.方法表集合在字段表集合后面,与字段表集合非常类似
解析一个方法表
在这里插入图片描述

第一个0x0002表示有两个方法, 第二个0x0002第一个方法的访问符为 private, 方法简单名称为:0x0008常量池索引值,javap 的结果:
在这里插入图片描述

即构造函数,0x0009为方法描述符常量池索引值,为()V,即无参构造函数,返回void, 0x0001表示有1个额外属性表,索引为10,发现是code,即存放方法体中字节码指令的属性集合
ps:继承的属性字段以及没有被重写的方法时不会在当前字节码中出现的
vii.属性表集合 attribute_info
jvm规范对属性表要求不严格,可以自定义属性信息,jvm运行时会忽略不认识的属性,只要求定义一个u4的长度属性说明属性表占的字节数就行
viii.Code属性方法体编译后的指令集保存在其中,而抽象类和接口方法没有code属性
Code属性结构参看官网:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3
第0行-第6行指令,出现对应type的异常在,则执行
行指令
lineNumberTable不是必须的,但是class文件默认会生成
在这里插入图片描述

code属性中stack表示最大栈, locals表示需要的变量槽,局部变量分配内存的最小单位是变量槽,<=32的变量占一个,而64位则需要2个变量槽,变量槽会被重用,因此变量槽的个数 max_locals=同时生存的最大局部变量数量+类型计算,args_size表示参数数量 ,任何实例方法(若是static方法则没有)默认会传入一个参数this,实例方法初始化时会给this赋值,因此args_size>=1 ,locales>=1
eg:字节码中code属性解释
在这里插入图片描述
⦁ 二、字节码指令
⦁ 1. 指令由长度为1个byte的操作码Opcode和任意多个操作数Operand组成;指令集的长度1个字节最多256个操作码;
⦁ 2.字节码指令集面向的是操作数栈,而非寄存器,因此指令只包含操作码,而指令参数放在操作数栈中
⦁ 3.字节码指令与操作并非一对一,并非每种与数据类型相关的指令都支持jvm运行时数据类型, 指令会超过1字节,对于不支持的指令和数据类型对,会转换成支持的类型和指令操作,大多数对于boolean byte short和char的操作实际都是转成响应的int类型来处理的
⦁ 4.jvm指令集参考官网:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
⦁ 5.大部分数据类型对应的操作码都有助记符
⦁ eg:fload->加载float类型, a–>reference, d–>double arraylength指令–>数组
⦁ (1)加载和存储指令,用于将数据在栈振中的局部变量表和操作数栈之间传输
⦁ 从局部变量表中加载到操作数栈: Tload T为i/f/d/a等;
⦁ 从操作数栈存储到局部变量表中 :Tstore ,istore_<2> 带尖括号的表示指令集, 表示istore_<0> istore_<1> istore_<2> 表示存储数据 0 1 2,而不需再在操作数栈中取数,指令带着操作数
⦁ 将常量加载到操作数栈:fconst_ dconst_等
⦁ (2)运算指令:iadd 、isub、imul、idiv、 加减乘除等
⦁ (3)类型转化指令
⦁ (4)对象创建与访问: new(创建)、访问实例字段 getfield 、加载和存储数组 baload
、iastore
(5)操作数栈管理指令 pop2(出两个) dup2_x1复制栈顶2个元素并将复制值重新压入栈顶等
(6)控制转移指令: goto等
(7)方法调用和返回指令 :
invokevirtual指令调用对象实例方法,·invokeinterface指令调用接口方法,·invokespecial指令一些特殊方法,如私有方法 父类方法和初始化方法,·invokedynamic指令动态解析,比如反射,lamada或者其他运行时才产生和加载的类
返回指令:ireturn等,无返回时return指令
⦁ (8)异常处理指令
⦁ athrow 显示抛异常,异常处理则是由异常表完成的而不是字节码指令
⦁ (9)同步指令
⦁ monitorenter和monitorexit 共同支持了synchronize关键字,默认会给两个指令安排异常表,保证异常抛出锁能够释放
ps:i=i++和i=++i的字节码指令原理
在这里插入图片描述
iinc指令:Increment local variable by constant,oprand stack :No change
⦁ 三、虚拟机类加载机制
⦁ 1.类的生命周期图
在这里插入图片描述

⦁ 2.加载、验证、准备、初始化和卸载顺序固定,而解析并非固定
⦁ 3.类加载的第一个阶段–加载的具体时机并没有明确规定,jvm中并没有约定类何时加载进来,一般都使用lazyLoading,只有用该类的时候,才会加载**,但是如果后续的阶段必须要进行时,类就不得不进行加载了,相当于间接的规定了加载时机**,如触发初始化(则加载验证准备必须完成)的场景

⦁ i.遇到new getstatic putstatic invokestatic指令(即new对象,调用非final的静态字段,final的字段会放常量池,因此无需初始化对应类,调用static方法等)则如果类没有初始化,则必须初始化
⦁ ii.反射调用某个类型时,需要先初始化类;
⦁ iii.虚拟机启动时调用main函数的那个类需要初始化;
⦁ iv.初始化类时,其父类还未初始化,则先初始化父类
⦁ 4.类加载过程
⦁ (1)加载:某种途径(如类全限定名,网络IO,zio压缩包中)获得class文件,将class静态数据转成运行时数据结构,在内存堆中生成一个Class对象,作为这些运行时数据的访问入口
⦁ (2).验证:验证class格式是否符合jvm规范的要求,该过程占用了整个类加载过程相当大的一部分时间和性能;包括文件格式验证魔术版本号…,当格式验证通过后字节流就会被加载到方法区,然后进行元数据验证,比如是否多继承,接口是否有方法体等java语义规范;字节码验证和符号引用验证(通过字符串描述的全限定名能否找到类,方法和字段是否存在且可访问等,如不,则抛出java.lang.NoSuchMethodError、java.lang.IllegalAccessError等)
⦁ 验证阶段不是必须的,如果代码已经反复在测试环境里验证过使用过,则可通过-Xverify:none关闭验证,节约时间
(3)准备:为类中定义的变量(static修饰)分配空间并设置初始值
(4)解析:将常量池中的符号引用替换为直接引用的过程
符号引用:一般都是常量池中的utf_8字面量的来描述的引用目标,只要能够无歧义的引用到目标即可;直接引用时对目标的直接指向,可能是一个指针或者句柄,意味着目标已经被加载内存中
eg:
在这里插入图片描述

i.类或者结构解析:全限定名传递给类加载器加载该类
ii.字段和方法解析,先在常量池中的符号引用,然后从类C本身开始向上递归搜索,若匹配到符号引用则返回字段或方法的直接引用
(5)类初始化
i.类构造器·<clinit()是编译器产生的为类变量赋值和执行静态方法、代码块等构造器,执行顺序由static出现在源码中顺序决定,静态代码块之后定义的类变量,代码块中只能赋值不能访问;
ii.<clinit()与实例构造器()不同,不需要显式调用父类<clinit(),jvm会自动调用父类<clinit()
iii.如果类中没有static变量、方法和代码块则不会生成<clinit()并执行,本质上还是看上面的几个类初始化的场景,如果出现了上述场景,则会生成类初始化函数
eg:
在这里插入图片描述

如果加final,则常量池中字面量的赋值不需要通过类初始化函数,上述截图中就不会用<clinit()
iv.函数执行(如static代码块中)默认是同步执行,多个线程执行时,如果static执行时间长,会造成其他线程阻塞,此外,只能有一个线程执行clinit函数,即多个线程也是只一个线程执行一次
⦁ 5.类加载器
⦁ 自定义个类加载器
⦁ (1)类比较相等时,要检查类本身和类加载器,都相等时instanceof才返回true
⦁ (2)双亲委派模型,能够保证加载出来的类是同一个,因为使用相同的类加载器
⦁ boostrap类加载器是c++实现的,所以在java中打印会是null,
双亲委派机制的原因:(1)使用 instanceof 等比较类是否相等的操作本质上除了比较类的限定名外。还要比较类加载器,双亲委派可以使得同限定名的类被同加载器加载,(2)安全,对于一些java底层jre中的类,比如String.class等双亲委派能够保证不能自定义一个同名的类,更加安全

在这里插入图片描述
ps:binary name指的就是类的全限定名:java.lang.Thread等
官网例子
在这里插入图片描述
⦁ 6.虚拟机字节码执行引擎
⦁ (1)jvm执行引擎完全由软件实现,有可能是解释执行,也有可能编译执行两种,或者两种混合,得看具体实现
在这里插入图片描述
⦁ .编译混合模式: 解释器和JIT热点代码编译,对于执行频率高的代码,一次性编译好放本地执行(本地机器可执行的代码),使用纯编译模式,缺点是jvm启动起来会比较慢(大量类的情况下),但是执行会很快,热点代码检测:多次被调用的方法(方法有方法计数器),多次被调用的循环(循环有循环计数器)
Xmixed–混合模式 默认,-Xint 使用解释模式,启动快,执行慢,-Xcomp 纯编译模式
⦁ (2)运行时栈帧结构
在这里插入图片描述

⦁ i.局部变量表
⦁ 在编译后max_locals就确定了方法分配的局部变量表的最大容量(变量槽), 变量槽是最小单位,可复用;java中32位以内的数据类型以及reference应用类型都占用一个变量槽,64位的数据类型long和double则占两个变量槽;jvm通过索引访问局部变量表,占用一个变量槽的数据N就是下标,而占两个槽则N和N+1一起访问;访问实例方法第0个变量槽放this
⦁ ii.操作数栈
⦁ 最大深度也是在编译时就确定的,32位数据类型占一个栈,64位占两个栈
⦁ eg:
在这里插入图片描述

⦁ iii.动态链接
⦁ 栈帧包含指向运行时常量池的该栈帧所属方法的引用,引用的一部分会在一开始转化为直接引用,另一部分则是在每次运行期间都转为直接引用,即动态链接
⦁ iv.方法返回值
方法退出两种方式:遇到return指令,正常退出;遇到异常且在异常表中没有搜索到处理器,则退出方法,且不会给调用者返回任何值;退出一般要做的操作:恢复调用者的局部变量表和操作数栈,并将返回值压入栈中供使用,pc计数器指向下一个指令
⦁ (3)方法调用
即确定具体
被调用的方法是哪一个,这时还没进入方法内部执行

⦁ i.解析(只针对方法)调用:符合“编译器可知,运行期不变”的方法的调用过程称为解析,即在类加载阶段的解析阶段对方法的解析(将符号引用转为直接引用),主要针对非虚方法,都不可重写,在类加载阶段版本唯一,非虚方法:静态方法,实例方法,私有方法,父类方法(单继承)和final方法;其他不能再类加载就确定调用版本的称为虚方法
⦁ ii.分派
静态分派也可认为是解析的一种,静态类型和实际类型见下图,依赖静态类型来决定方法执行版本的分派方法,都称为静态分派,最典型为方法重载,静态分派在类加载解析时就确定好了方法版本,跟解析调用类似
在这里插入图片描述
动态分派,根据对象的实际类型找对应的方法,使用invokevirtual指令,过程为:找到操作数栈顶元素指向的实际类型C,在C中找到方法则返回直接引用, 否则就从下到上找C的父类方法,并重复执行上述过程,以上就是方法重写(java中继承)的本质,既然继承的本质是invokevirtual执行过程,那么java中字段没有重写这么一说
在这里插入图片描述
(4)基于栈的字节码解释执行引擎
i.jvm中有解释执行和即时编译器编译后执行,具体怎么执行需要jvm判断
ii.编译过程
java是把抽象语法树(字节码)之前的通过javac来完成,剩下的目标代码生成过程jvm内部完成,而js是整个编译过程都封装在执行引擎中了
在这里插入图片描述

iii.基于栈的指令集与基于寄存器的指令集(即os中的指令集)的区别:前者指令不带参数,参数都是放在操作数栈中的,而后者指令是必须带参数的;基于栈可移植性好,基于寄存器执行速度肯定更快,当然前者经过解释执行后最终还是生成的后者
iv.解释器执行过程与人为逐条分析字节码指令结果虽然一样,但是解释器有很多优化手段,来提高解释效率,与逐条解释的过程可能大不相同
⦁ (5)字节码生成与动态代理
动态代理的原理就是根据传入的接口,生成了一个Proxy类,只是该class类是直接以字节的形式保存在内存中的,并没有生成到磁盘上,然后调用definedclas0方法进行类加载之后的验证初始化以及生成运行时数据等过程,而至于如何生成class文件,其实就是根据class文件的格式规范通过高效的方式去拼接字节码

⦁ eg:
⦁ 设置以下系统属性,可以将生成的代理类保存在磁盘上即生成class文件
System.getProperties().put(“sun.misc.ProxyGenerator.saveGeneratedFiles”, “true”);

在这里插入图片描述
四、程序编译与优化
1.前端编译(.java–>.class的过程)与优化
.java语法糖
(1)java是一种擦除式泛型,语法糖而已,经过javac编译后,在class层面所有泛型都会消失,但是从java1.5开始,所有的泛型擦除都是擦掉了Code属性中的字节码,实际元数据还保留了泛型信息,即可以通过反射拿到参数化类型,目前未找到合适的方法
(2)所有泛型化的实例类型,都是裸类型的子类型,这样类型转换就是安全的,即裸类型是所有该类型泛型化实例的共同父类型,裸类型的实现是直接简单粗暴的在编译时把ArrayList还原会ArrayList,仅仅在元素CRUD时进行类型检查和必要转换
(3)自动拆箱initValue()和装箱valueOf()
eg:将字面量赋值给Integer则需要装箱,做integer值得相加则需要拆箱
装箱和拆箱参考:https://docs.oracle.com/javase/specs/jls/se15/html/jls-5.html#jls-5.1.7
理想情况下,每个字面值装箱都应该产生同样的对象,即==->true,但是现在做不到,只能在Integer等包装类中加入cache, 部分的实现该功能
(4)条件编译,对于if(true)这种编译器可知执行分支的代码,会直接只编译可执行的代码段
2.后端编译优化
主要是热点代码(多次执行的方法代码块和循环体)检测到后编译到本地

⦁ 五、JMM java内存模型
⦁ 1. JMM与线程
⦁ (1)JMM的目的:屏蔽各种硬件与操作系统的内存访问差异,无论在哪种平台上都能达到一致的内存访问效果的一种jvm规范
⦁ (2)java内存模型中线程只能在自己的工作内存中修改变量副本,然后刷进主存,不能直接修改主存,JMM中的内存划分与jvm内存区域中的堆栈等划分层次不同,不能一概而论,比如对于一个线程而言,可能主内存是堆中对象实例的部分数据,而工作内存是虚拟机栈中部分数据,且工作内存可能直接在cpu高速缓存上,所以维度不同
在这里插入图片描述

⦁ (2)变量的副本并非是对整个变量的拷贝,比如访问一个10m的对象,可能只是对对象的引用进行了拷贝或者对象中某个需要用到的变量进行访问
⦁ (3)内存之间交互操作,8中原子操作(一旦还是就一直执行,不被打扰)
⦁ lock: 将主内存的变量标记为独占
⦁ unlock:释放主内存变量
⦁ read:读到一个主内存的变量,用于后面load
⦁ load:作用于工作内存的变量,将read读到的值放到工作内存副本中
⦁ use:执行引擎遇到一个需要使用变量的字节码指令时,就会执行这操作
⦁ assign: 将执行引擎接收的值付给工作内存中的,遇到赋值的字节码指令使用
⦁ store:将工作内存中变量的值传到主内存,供write使用
⦁ write:作用于主内存的变量,将store后的变量写回给主内存的变量
⦁ 以上,read和load,store和write顺序不可变
(3)再次明确,volatile是只保证可见性,不保证并发安全,指令重拍是通过内存屏障 SS LL SL LS;缓存一致性协议保证了可见性:在工作内存中,每次使用V都得从主存中拿到最新值,每次修改完V必须立刻同步到主存,写的操作又会导致其他线程的工作内存中的值无效
⦁ (4)原子性通过synchronize锁来实现可见性(立即感知)通过volatile、synchronized(在unlock之前必须将变量从工作内存store和write到主内存)和final来实现,有序性(指令的有序靠volatile、线程串行则靠synchronized)
⦁ 2.线程的实现–三种方式
⦁ (1)内核线程实现,由内核完成线程切换,并分配到不同cpu,跟普通用户的轻量级进程LWP是一对一的支持关系,优点是每个LWT都是独立调度单元,阻塞了也不影响整个进程,缺点是由于需要内核线程支持,需要系统调用,切换cpu状态,同时消耗内核资源,如内核栈空间等,支持的数量有限
⦁ 明显 java采用此种方式实现的线程
在这里插入图片描述

⦁ (2)完全建立在用户空间的线程1-N的实现,线程的建立完全在用户进程中,对于内核完全无感知,优点是支持并发高,缺点是实现非常复杂,如多核cpu分配任务,阻塞怎么办等
⦁ (3)以上两种方式结合
⦁ (4)jvm中线程的实现采用1-1映射的方式,每个java线程都直接映射一个OS原生线程(内核线程)
⦁ 3.线程调度
⦁ (1)调度方式
⦁ 协同式(非抢占):除非线程主动让出cpu时间,否则不进行线程切换 不常用
⦁ 抢占式:os分配每个线程占用cpu的时间,完全公平调度那一套
⦁ (2)线程优先级
⦁ java中线程优先级与不同平台os的线程优先级并非一一对应,如java中比window多,那么结果时 java中1和2优先级在window中是一样的,其次优先级只是某种建议,真正调度得看内核调度,也可能越过优先级,因此在runnable状态的线程队列中,无法通过优先级准确判断是否线程会优先执行
⦁ 4.线程安全的实现方式:
⦁ (1)互斥–保证同时刻只有一个线程使用变量,代表是synchronized的实现:
使用monitorenter和monitorexit指令,两个字节指令分别需要一个reference类型来指明要锁定和解锁的对象,前者使得对象的锁计数器+1,后者使得计数器-1,当计数器为0则释放锁,然后就能被其他线程获得
占用本地变量表
在这里插入图片描述

⦁ (2)重入锁 ReentrantLock 是lock接口常见实现锁,相对于synchronized增加了:等待可中断(正在等待锁的线程当长时间获取不到锁时可放弃)、公平锁(按时间顺序获取,synchronized是非公平的)、锁可绑定多条件(而synchronized和wait配合只能实现一个条件),而ReentrantLock通过多次调用newCondition方法即可
⦁ (3)乐观锁,非阻塞的同步方式,发现并发时就重试,比如CAS
第六部分 运行时数据区
在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值