JDK
包含JRE
和JVM
,JRE
包含JVM
javac
用于编译java代码到字节码文件java
命令启动JVM
,字节码最终运行在JVM
上JavaFX
是一种富客户端技术,用于替换flash和Swing程序Java Web Start
第一次运行需要借助浏览器,之后生成快捷方式当web站点更新是程序也更新,效果类似SwingSun Classic VM
第一款商用的Java虚拟机,伴随着JDK1.0发布,只能使用纯解释器方式来执行java代码,只能外挂JIT编译器(just-in-time compilation)进行编译java代码,但是解释器和JIT编译器不能同时执行。存在较大性能问题。
Exact VM
Exact VM
全称为Exact Memory Management
【准确式内存管理】,编译器和解释器可以混合工作以及两级及时编译器。但是只能在Solaris平台使用。JDK1.2时发布。
HotSpotVM
从JDK1.3开始运用至今的HotSpot虚拟机(Oracle JDK和Open JDK)。加入热点代码探测技术。
KVM
面对移动设备和嵌入式设备的kilobyte虚拟机,运行速度较慢、简单、轻量、高度可移植。
JRockit
Bea公司(2008年被Oracle收购)开发的JRockit虚拟机,被称为当时世界上最快的Java虚拟机,专注于服务端应用。JDK7时部分功能被整合到HotSpot。不包含解释器实现,只有即时编译器。
J9
IBM开发类似HotSpotVM虚拟机桌面、服务器、嵌入式端都支持。IBM内部使用。
Dalvik
安卓操作系统所使用的虚拟机,并非是一个Java虚拟机,未遵循Java规范,基于寄存器架构【其它PC虚拟机基于栈架构】,不能执行.class
文件。可以通过.class
文件转化为.dex
文件运行在dalvik上,开发的语法也为Java,也可使用Java API。
MicorsoftVM
只能在windows平台下运行,最终因为法律问题被SUN公司禁止使用。
TaobaoVM
根据HotSpotVM深度定制,在淘宝内部使用,对硬件依赖性高。
Azul VM和 Liquid VM
Azul VM和BEA Liquid VM是一类运行在特定硬件平台的专有虚拟机,是”高性能”虚拟机。
Azul VM是Azul Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的Java虚拟机。每个Azul VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、为专有硬件优化的线程调度等优秀特性。
Liquid VM即是现在的JRockit VE(Virtual Edition),由Bea公司开发,可以直接运行在自家Hypervisor系统上的JRockit VM的虚拟化版本,Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如文件系统、网络支持等。由虚拟机越过通用操作系统直接控制硬件可以获得更好的性能,如在线程调度时,不需要再进行内核态/用户态的切换等,这样可以最大限度地发挥硬件的能力,提升程序的执行性能。
程序计数器
程序计数器是一块比较小的内存空间,可以看作当前线程字节码所执行的行号指示器。属于线程独占区。如果线程执行的是java方法,则计数器的值是正在执行的字节码指令的地址。如果线程执行的是native方法,则计数器的值为undefined。
虚拟机栈
虚拟机栈描述的是Java方法执行的动态内存模型
本地方法栈
本地方法栈为虚拟机执行Native服务,结构和虚拟机栈完全相同。HotSpotVM将虚拟机栈和本地方法栈放在一起管理不做区分。
Java堆
存放对象的实例、垃圾收集器管理的主要区域。
方法区
属于线程共享区,存储了虚拟机加载的类信息【版本、字段、方法、接口】、运行时常量池【字面量和符号引用】、静态变量、即时编译器编译后的代码等数据。在HotSpot中方法区是使用永久代实现的,所以永久代等于方法区。这里很少进行垃圾回收。
从Java8开始HotSpots使用
元空间
取代了永久代
,永久代
物理是是堆的一部分而元
空间属于本地内存。元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
直接内存【并非JVM规范定义的区域,不属于虚拟机运行时内存的一部分】
JDK1.4为了弥补IO缺陷引入NIO,运行直接在堆外分配内存,不受JVM制约,由操作系统分配。
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表
具体是使用空闲列表还是指针碰撞需要根据内存状况决定
不管使用哪种对象内存分配方式,在多线程环境时,如果一个线程正在给A对象分配内存,指针还没有来的及修改,其它为B对象分配内存的线程,而且还是引用这之前的指针指向,这样会带来分配问题。于是有两种处理方式。
分配时加锁
堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
TLAB【线程本地分配缓存区】
为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域称作TLAB。默认设定为占用Eden Space的1%。
对象头
实例数据
存储在程序代码中所定义的各种类型的字段内容
对齐填充
帮助对象凑满8字节的倍数,不一定存在
句柄访问
使用句柄访问。Java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
直接指针访问
使用直接指针访问。reference中存储的就是对象的地址。
HotSpot是用直接指针访问方式进行对象访问的。
引用计数法
在对象中添加一个引用计数器,当有地方释放这个引用时,对象上存储的引用计数减一,但是当出现互相引用时【对象实例3和对象实例5】引用计数依旧不为0,无法对其进行垃圾回收
可达性分析
该算法的核心算法是从GC Roots对象作为起始点,如果对象不可达到GC Root则认为此对象是要回收的对象。可作为GC Roots的对象
标记-清除算法
算法分为标记
和清除
两个阶段:先标记出所有需要回收的对象,完成后统一回收掉所有被标记的对象。
缺陷:①标记和清除过程的效率不高;②极易造成空间碎片问题
复制收集算法
复制收集算法将可用内存按容量划分为大小相等的两块,survivor
区每次只使用其中的一块。当这一块的内存用完了,就将eden
和survivor
还存活着的对象复制到另外一块survivor
上面,然后再把已使用过的内存空间一次清理掉。 这样内存分配时也就不用考虑内存碎片等复杂情况,实现简单运行高效。长期存活的对象移入oldGen
区这里面的对象很少执行垃圾回收。
标记-整理算法
当预估能回收的对象并不多时(例如老年代)采用复制收集算法就要执行较多的复制操作,效率将会变低且还浪费了大量空间,所以此时进行复制收集算法不明智。采用标记-整理
算法较为合适,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。且下次在此空间继续分配内存可以使用指针碰撞法提高速度。
分代收集算法
把Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代每次垃圾收集时都有大量对象需要回收那就选用复制算法。而老年代中因为对象存活率高就使用标记-清理
或标记-整理
算法进行回收。
Minor GC:清理年轻代,不会影响到永久代
Major GC :清理老年代,Major GC
大部分由Minor GC
触发
Full GC :清理整个堆空间—包括年轻代、老年代和永久代
Serial收集器【复制收集算法】
ParNew【复制收集算法】
Parallel Scavenge【复制收集算法】
Serial Old 收集器【标记-整理算法】
Parallel Old 收集器【标记-整理算法】
Parallel Scavenge
的老年版本cms 收集器【标记-清除算法】
[1]初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
[2]并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
[3]重新标记:为了修正并发标记期间因用户程序继续运行发生改变的对象的记录。仍然存在Stop The World。
[4]并发清除:对标记的对象进行清除回收。
G1收集器
G1垃圾收集器并没有将内存按照连续内存地址分为新生代、老年代。而是分成了一个个的区域
【Region】,采用了分代与分区算法。这些区域大小不固定,根据回收的情况进行评估,到达阀值视为老年代。在进行一次垃圾回收之后,会进行日志收集确定分代划分和是否进行混合清理(新生代和老年代一起清理)。
G1的内存结构如下所示:
JDK11之后G1触发FULL GC时可并行处理,以前只能串行处理
对象优先分配到Eden区
大对象直接分配到老年代
长期存活对象分配到老年代
空间分配担保
存入对象时发现年轻代空间不足,将判断老年代最大可用的连续空间是否大于当前年起代所有对象
在JDK7(包括)之后完全支持栈上分配和逃逸分析。
逃逸分析
如果某个方法之内创建的实例只在方法内被使用,方法结束之后没有任何对象引用它【即可被GC回收】,这样的对象叫做未发生逃逸对象。如果某个方法之内创建的实例在方法结束之后有对象引用它【即不可被GC回收】,这样的对象叫做逃逸对象。
栈上分配
如果一个对象未发生逃逸则这个对象的生命周期只存在一个方法体内,这样的对象可以直接在栈上分配提高效率,也方便回收【方法结束,栈销毁,未发生逃逸也就随着销毁】
JPS
显示当前所有java进程pid相关信息
参数 | 解释 |
---|---|
-q | 只显示pid,不显示class名称,jar文件名和传递给main方法的参数 |
-l | 输出应用程序main class的完整package名或者应用程序的jar文件完整路径名 |
-m | 输出传递给main方法的参数 |
-v | 输出传递给JVM的参数 |
-V | 隐藏输出传递给JVM的参数 |
jstat
对Java应用程序的资源和性能进行监控,包括了对类的装载、内存、jit编译和垃圾回收状况的监控。
用法: jstat [option] [pid] [
参数 | 解释 |
---|---|
-t | 打印时打印时间戳 |
-class | 显示ClassLoad的相关信息 |
-compiler | 显示JIT编译的相关信息 |
-gc | 显示和gc相关的堆信息 |
-gccapacity | 显示各个代的容量以及使用情况 |
-gcmetacapacity | 显示metaspace的大小 |
-gcnew | 显示年轻代信息 |
-gcnewcapacity | 显示年轻代大小和使用情况 |
-gcold | 显示老年代和永久代的信息 |
-gcoldcapacity | 显示老年代的大小 |
-gcutil | 显示垃圾收集信息 |
-gccause | 显示垃圾收集信息并显示最后一次垃圾回收的诱因 |
-printcompilation | 输出JIT编译的方法信息 |
jinfo
用于实时查看和调整虚拟机参数
用法:jinfo [option] [pid]
参数 | 解释 |
---|---|
-flag [name] | 输出对应名称的参数 |
-flag [[+/-]name] | 开启或者关闭对应名称的参数 |
-flag [name=value] | 设定对应名称的参数 |
-flags | 输出全部的参数 |
-sysprops | 输出系统属性 |
jmap
用于生成java程序的dump文件, 以及查看堆内对象的统计信息、ClassLoader的信息以及finalizer队列
用法:jmap [option] [pid]
参数 | 解释 |
---|---|
-histo[:live] | 显示堆中对象的统计信息 |
-clstats | 打印类加载器信息 |
-finalizerinfo | 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象 |
-dump:live,format=b,file=heap.bin | 内存转储dump数据,过程中会暂停应用 |
jhat
分析内存dump文件,会占用大量CPU和内存生成报告,默认最后会启动一个web服务端口7000,可访问查看相应指标信息,但是很少使用,不够直观灵活。
jstack
查看生成线程状态信息
用法:jstack [option] [pid]
参数 | 解释 |
---|---|
-l | 长列表. 打印关于锁的附加信息 |
jconsole
提供了内存、线程、线程死锁检测、类、VM概要等GUI查看功能
VisualVM
JDK8之后被移除默认的JDK安装包,需要单独下载,官方地址
Class文件是一组以8位字节为基础单位的二进制流,在当遇到8位字节以上数据项时,则按照高位在前的方式分割成若干个8位以上字节进行分别存储。各个数据项目按照严格顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。
Class文件中只有两种数据类型,分别是无符号数和表
顺序依次为:
①魔数[4字节]
②Class文件版本[4字节]
③常量池[长度不固定]
④访问标志[2字节]
⑤类索引[2字节]、父类索引[2字节]、接口索引集合[长度不固定]
⑥字段表集合[长度不固定]
⑦方法表集合[长度不固定]
⑧属性表集合[长度不固定]
长度 | 含义 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
魔数
用于区分是否是Class文件,占四个字节,十六进制表示值为CA FE BA BE
Class文件版本
占4字节为字节码版本,为小版本号minor_version[2字节]和主版本号major_version[2字节]的组合
常量池
长度不固定,先用两个字节描述长度,真实长度为描述长度减一【0位置代表无引用,它也占一位】
常量池的组成:
常量池计数器(constant_pool_count):占用前两个字节,记录着常量池的组成元素个数
常量池项(cp_info):元素信息,一共constant_pool_count-1
个数量
cp_info结构如下:
cp_info {
ul tag;
ul info[];
}
常量池数量是从1开始计数的并非从0开始,所有元素真实个数需要减一,将第0项常量空出来是为了满足执向常量池的索引数据在特定情况下表达“不引用任何一个常量池项”的含义
cp_info的类型:
常量类型 | 值 |
---|---|
CONSTANT_CLASS | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
可使用javap -verbose [name].class
查看常量池内容
访问标志
access_flages
占有两个字节,没有使用到的标志为要求一律为0
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 是否为public类型 |
ACC_FINAL | 0x00 10 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x00 20 | 是否允许使用invoke special字节码指令的新语义 |
ACC_INTERFACE | 0x02 00 | 标志这是一个接口 |
ACC_ABSTRACT | 0x04 00 | 是否为abstract类型,对于接口或者抽象类来说标志值为真 |
ACC_SYNTHETIC | 0x10 00 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x20 00 | 标志这是一个注解 |
ACC_ENUM | 0x40 00 | 标志这是一个枚举 |
可使用javap -verbose [name].class
查看访问标志
类索引、父类索引、接口索引集合
类索引:占用两个字节,指向常量池中的引用
父类索引:占用两个字节,指向常量池中的引用
接口索引集合:统计个数interfaces_count
占用两个字节,interfaces
占用interfaces_count
*2个字节,每个interfaces指向常量池中的引用
字段表集合
字段表【field_info】用于描述类中声明的变量,但是不包括在方法内部声明的局部变量
字段表的结构:
长度 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
access_flags
访问标志值:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x00 01 | 字段是否为public |
ACC_PRIVATE | 0x00 02 | 字段是否为private |
ACC_PROTECTED | 0x00 04 | 字段是否为protected |
ACC_STATIC | 0x00 08 | 字段是否为static |
ACC_FINAL | 0x00 10 | 字段是否为final |
ACC_VOLATILE | 0x00 40 | 字段是否为volatile |
ACC_TRANSTENT | 0x00 80 | 字段是否为transient |
ACC_SYNCHETIC | 0x10 00 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x40 00 | 字段是否为enum |
属性表集合
在class文件,字段表,方法表都可以携带自己的属性表集合用于描述某些场景专有的信息
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法表,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | 供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。JVM字节码指令是基于栈架构的指令,安卓虚拟机是基于寄存器架构的指令。
部分指令有类型区分:【i:integer】、【l:long】、【f:float】、【d:double】、【a:reference】
加载与存储指令
[i/l/f/d/a]load
[i/l/f/d/a]store
bipush
将取值为【-128~127】的常量入栈sipush
将取值为【-32768~32767】的将常量入栈ldc
将取值为【2147483648~2147483647】的将常量入栈ldc_w
从由常量池中取出指的索引位的一个字长的值,然后将其压入栈ldc2_w
从由常量池中取出指的索引位的两个字长的值,然后将其压入栈aconst_null
将null对象引用压入栈iconst_m1
将int类型且值为【-1】的压入栈iconst_value
将int类型且值为【0、1、2、3、4、5】压入栈lconst_value
将long类型且值为【0、1】压入栈fconst_value
将float类型且值为【0、1、2】压入栈dconst_value
将double类型且值为【0、1】压入栈wide
运算指令
[i/l/f/d]add
[i/l/f/d]sub
[i/l/f/d]mul
[i/l/f/d]div
[i/l/f/d]rem
neg
ishl
(逻辑左移)、ishr
(逻辑右移)、iushr
(算术右移)【其余类型不常见】ior
【其余类型不常见】iand
【其余类型不常见】ixor
【其余类型不常见】iinc
【其余类型执行压栈常量1再进行相加操作】类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换以及用来处理字节码指令集中的数据类型相关指令无法与数据指令一一对应的问题。
分类:宽化类型处理和窄化类型处理
i2l
、i2f
,i2d
,l2f
,l2d
,f2d
等l2i
、f2i
,d2i
,f2l
,d2l
,d2f
等对象创建与访问指令
new
newarray
、anewarray
、multianewarray
getfield
、putfield
、getstatic
、putstatic
[b/c/s/i/l/f/d/a]aload
[b/c/s/i/l/f/d/a]astore
arraylength
instanceof
、checkcast
操作数栈管理指令
pop
、pop2
dup
、dup2
、dup_x1
、dup_x2
swap
控制转移指令
控制转移指令可以让JVM有条件或无条件地从指定的位置执行而不是继续下一条指令执行程序,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
ifeq
、iflt
、ifle
、ifne
、ifgt
、ifge
、ifnull
、ifnonnull
、if_icmpeq
、 if_icmpne
、if_icmplt
、if_icmpgt
、if_icmple
、if_icmpge
、if_acmpeq
和if_acmpne
tableswitch
、lookupswitch
goto
、goto_w
、jsr
、jsr_w
、ret
方法调用指令
指令 | 解释 |
---|---|
invokevirtual | 指令用于调用对象的实例方法即非私有的实例方法,根据对象实际类型进行分派(虚方法分派) |
invokeinterface | 指令用于调用对象的接口方法,会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
invokespecial | 指令用于调用一些需要特殊处理的实例方法,包括初始化方法、私有方法和父类方法 |
invokestatic | 指令用于调用类的static方法 |
invokedynamic | JDK7之后支持,调用动态方法,在运行时动态解析出调用点限定符所引用的方法之后,调用该方法 |
方法返回指令
返回指令是根据返回的数据类型进行区分的,有ireturn
(返回值是boolean、byte、char、short和int)、lretrun
、freturn
、dreturn
、areturn
还有返回voide、实例初始化、类和接口的初始化使用的return
异常处理指令
athrow
用于显示抛出异常时(明确throw new RuntimeException和Exception均会),但是对于”i/0“这种未显示表明的抛出是不会使用athrow的。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED
访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter
和monitorexit
两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。
一个Java对象的创建过程往往包括两个阶段:类初始化阶段 和 类实例化阶段
类的加载:代表jvm将java文件编译成class文件后,以二进制流的方式存放到运行时数据的方法区中,并在java的堆中创建一个java.lang.Class对象,用来指向存放在方法堆中的数据结构。且虚拟机加载Class文件是懒加载
机制。
验证是连接的第一步,但是并非是必须的。这一阶段的目的是为了确保Class文件的字流中包含的狺息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全包含:
准备阶段正式为类的静态变量
分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。设置的初始值并非我们指定的值而是这种变量的默认值,但是如果是被final修饰的常量则会被初始为我们指定的值
解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic
指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为一解析状态),这样就避免了一个符号引用的多次解析。
类或者接口解析
要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤
如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载
如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer ,接着由虚拟机将会生成一个代表此数组对象的直接引用
如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常
字段解析
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
类方法解析
进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
接口方法解析
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。
否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
否则,查找失败
对于初始化阶段,虚拟机规范严格规定了有且只有5种情况必须对类进行初始化(加载、验证、准备自然需在此之前开始):
①遇到new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的java代码场景是:
使用new关键字实例化对象的时候
读取或设置一个类的静态字段【被final
修饰己在编译器把结果放入常量池的静态字段除外】
调用一个类的静态方法的时候
②使用java.lang.reflect
包的方法对类进行反射调用的时,如果类没有进行过初始化则需要先触发其初始化
③当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
④当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
⑤当使用JDK7动态语言支持时,如果MethodHandle
实例最后的解析结果REF_getstatic
、REF_putstatic
、REF_invokestatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
不执行初始化的例子
子类不会被初始化
类初始化时类加载过程的最后一步,前面的类加载过程中,除了在加载阶段(类加载过程的一个阶段)应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码。初始化阶段是执行类构造器<clinit>()
方法的过程。
<clinit>()
方法执行过程中可能会影响程序运行行为:
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问,属于非法向前引用。
public class Test {
static {
v = 3;
System.out.println(v); //编译报错,只能赋值不能访问
}
static int v = 1;
}
// 顺序执行最后v的值为"1"
<clinit>()
方法与类的构造函数(即类的实例构造器<init>()
方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()
方法一定是java.lang.Object
<clinit>()
方法对于类(抽象类)或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()
方法
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()
方法。但接口与类不同的是,执行接口的<clinit>()
方法不需要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()
方法
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。在实际应用中这种阻塞引起的问问往往是很隐蔽
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中。它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用。即Extension ClassLoader
的代码中的parent为null
注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)
扩展(Extension)类加载器
扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader
类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext
目录下或者由系统变量-Djava.ext.dir
指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
自定义(custom)类加载器
自定义类加载器由第三方实现,建议满足双亲委派模式。
只有被同一个类加载器加载的类才会相等,相同的字节码被不同的类加载器加载的类不相同。
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器【并非继承】采用组合关系
来复用父类加载器的相关代码,类加载器间的关系如下:
栈:线程私有,生命周期跟线程相同,当创建一个线程时,同时会创建一个栈,栈的大小和深度都是固定的。
栈帧:一个栈中可以有多个栈帧,栈帧是栈的元素,栈帧随着方法的调用而创建,随着方法的结束而消亡。栈帧存储了方法的局部变量表
、操作数栈
、动态连接
和方法返回地址
等信息。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表
局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽(Variable Slot)为最小单位。
操作数栈
操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值(max_stack)。
动态连接
在类加载解析时无法确定的符号引用(应该转为直接引用)在运行时进行动态确定转换为直接引用的过程叫做动态连接,包括动态分派、动态语言支持
方法返回地址
返回调用完成都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
附加信息
虚机规范中允许具的虚拟机实现增加一些规范里没有描述的到栈帧中。这部信息完全取决于虚拟机的实现。方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本
解析调用
在类加载的类解析阶段就已经调用方法的版本即在编译期已经确定了调用的方法,这种调用方式称之为解析调用。
在Java中被final、private、static修饰的方法以及构造方法都是属于这种类型,在编译期间就可确定,对应编译出的字节码指令为invokestatic【调用静态方法】
、invokespecial:调用非静态私有方法和final方法、构造方法(包括super)
静态分派调用
public class StaticDispatch {
static class Parent{
}
static class Child extends Parent{
}
public static void sayHello(Parent p){
System.out.println("hello,parent");
}
public static void sayHello(Child c) {
System.out.println("hello,child");
}
public static void main(String[] args) {
// 静态类型 实际类型
Parent parent = new Parent();
Parent child = new Child();
sayHello(parent); //hello,parent
sayHello(child); //hello,parent
}
}
我们把“Parent”称为变量的静态类型,后面的“Child”称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型在编译阶段可知
依旧是invokespecial
调用。而实际类型变化的结果在运行期才确定
,编译器在编译期并不知道一个对象的实际类型是什么。
静态分派的典型应用是方法重载。编译器在重载时是通过参数的静态类型而不是实际类型作为判定的依据。并且静态类型在编译期解析阶段可知,因此,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
动态分派调用
public class DynamicDispatch {
static class Parent{
public void sayHello(){
System.out.println("hello,i am parent");
}
}
static class Child extends Parent{
@Override
public void sayHello(){
System.out.println("hello,i am child");
}
}
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.sayHello(); // hello,i am parent
child.sayHello(); // hello,i am child
}
}
因为子类重写
父类的方法,在运行阶段才可确定类型在方法调用时候jvm其实是使用了invokevirtual
指令。java中采用oop-klass
二分模型表示一个对象。klass
保存着类的数据,在其中保存着方法表,方法表中保存着从父类继承下来,自己定义的所有方法,如果子类重写父类方法,那么在这个方法表中相同位置上的父类方法则会被覆盖。也就意味着从子类实例来说是无法找到父类该方法的。
静态类型语言:在非运行阶段,变量的类型是可以确定的,也就是变量是有类型的。
动态类型语言:在非运行阶段,变量的类型是无法确定的,也就是变量是没类型的。但是值是有类型的,也就是运行期间可以确定变量的值类型。
JDK7新增对动态语言调用的支持invokedynamic
,Java里没有函数指针无法在运行时动态调用方法。
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class MethodHandleTest {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
// 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
/**
* MethodType:代表“一个方法”
* 第一个参数 - 方法的返回值
* 第一个参数 - 方法的具体参数
*/
MethodType mt = MethodType.methodType(void.class, String.class);
/**
* lookup()方法来自于MethodHandles.lookup
* 作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
* bindTo()用于绑定本地变量表的第一个元素this
*/
MethodHandle methodHandle = lookup().findVirtual(obj.getClass(), "println", mt).bindTo(obj);
// 调用方法
methodHandle.invokeExact("Hello");
}
}
在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compile)。
即时编译器并不是虚拟机必须的部分,Java虚拟机规范并没有规定Java虚拟机内必须要有即时编译器存在,更没有限定或指导即时编译器应该如何去实现。但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。
JIT编译狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价。
符号引用(Symbolic References)
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
直接引用(Direct References)
(1)直接指向目标的指针【比如指向Class对象、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量【比如指向实例变量、实例方法的直接引用都是偏移量】
(3)一个能间接定位到目标的句柄