JDK:用于支持Java程序开发的最小环境,包括Java程序设计语言、Java虚拟机、JavaAPI三部分。
JRE:支持Java程序运行的标准环境,包括Java SE API子集和Java虚拟机。
第一章 Java虚拟机发展史
1.Sun Classic/Exact VM
Classic VM:只能使用纯解释器方式来执行Java代码,如果要使用JIT编译器,必须进行外挂,而解释器与编译器不能配合工作。
Exact VM:虚拟机可以知道内存中某个位置的数据具体是什么类型。
2.Sun HotSpot VM
结合了前两款虚拟机特点,也拥有新优势,如特点代码探测技术。
3.Sun Mobile-Embedded VM/Meta-Circular VM
4.BEA JRockit IBM J9 VM
5. Azul VM /BEA Liquid VM
6. Apache Harmony /Google Android Dalvik VM
7. Microsoft JVM及其他
第二章 Java内存区域与内存溢出异常
- 运行时数据区
1.程序计数器:
是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码指示器工作就是改变这个计数器的值来选取下一条所需执行的字节码指令。
因为在任何一个时刻,一个处理器都只会执行一条线程中的指令,所以为了线程切换后能恢复到正确位置,每条线程都需要有有一个独立的程序计数器,各个线程计数器之间互不影响,独立存储,即线程私有。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.Java虚拟机栈
线程私有,它的生命周期与线程相同。
每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每个方法调用到执行完成,就对应着一个栈帧在虚拟机栈中入栈到出栈。
局部变量表存放了编译期可知的各种基本数据类型,对象引用和returnAddress类型。 64位长度的long和double类型数据会占据两个局部变量空间,其余只占一个。局部变量表所需内存空间在编译期间完成分配,在运行期间不会改变。
对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛StackOverlowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3.本地方法栈
与虚拟机栈类似,但区别在于虚拟机栈位虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用的Native方法服务。同样会抛出
StackOverlowError异常和OutOfMemoryError异常。
4.Java堆
被所有线程共享,在虚拟机启动时创建,为了存放对象实例(所有的对象实例都在堆中分配)
Java堆分为新生代和老年代,详细会分为Eden空间、From Survior空间、To Survivor空间等。
如果堆中没有内存完成实例分配并且堆也无法再扩展时,会抛出OutOfMemoryError异常 。
5.方法区:
线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
根据Java.虚拟机规范的规定,当方法区无法满足内存分配需求时,.将抛出OutOfMemoryError异常。
6.运行时常量池
是方法区的一部分,用于存放编译阶段生成的各种字面量和符号引用,这部分内容将在类加载后进入方法去的运行时常量池中存放,当常量池无法再申请到内存时也会抛出OutOfMemoryError异常。
7.直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。会受到本机总内存的限制,若各个内存区域总和大于物理内存限制,导致动态扩展时出现OutOfMemoryError异常。
- 对象
- 对象的创建
从虚拟机角度:类加载检验->分配内存(两种分配方式:1.指针碰撞2.空闲列表)->内存空间初始化->对象设置
考虑的问题:对象创建是否非常频繁?
解决方案:
1.对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方法保证更新操作的原子性2.本地线程分配缓冲(TLAB)即每个线程在Java堆中预先分配一块小内存,哪个线程要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的时才需要同步锁定。 通过-XX:+/-UseTLAB设定是否使用TLAB。
- 对象的内存布局
对象在内存中的存储布局可以分为三块区域:对象头、实例数据、对齐填充。 - 对象的访问定位
两种方式:句柄和直接指针。
句柄:
优势:对象被移动时只会改变句柄中的示例数据指针,而reference本身不需要修改
直接指针:
优势:速度更快,节省了一次指针定位的时间开销。
- OutOfMemoryError异常
-
Java堆溢出
-Xms堆的最小值
-Xmx堆的最大值 -
虚拟机栈和本地方法栈溢出
-Xss栈容量大小 -
方法区和运行时常量池溢出
-XX:MaxPermSize最大方法区容量
-XX:PermSize 方法区初始内存容量 -
本机直接内存溢出
-XX:MaxDirectMemorySize指定直接内存大小,若不指定默认与-Xmx一样
第三章 垃圾收集器与内存分配策略
GC需要完成的三件事情?
1.哪些内存需要回收?
2.什么时候回收?
3.如何回收?
- 如何确定垃圾?
1.引用计数算法
一个对象如果没有任何与之关联的引用,即他们的引用计数都为0时,则这个对象时可回收的。
2.可达性分析算法:
通过一系列的“GC roots”对象作为起点搜索,如果再“GC roots”和一个对象之间没有可达路径,说明对象不可达,至少变为两次标记过程才将面临回收。
可作为GC roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI(Native方法)引用的对象。
- 四种引用
这次中强度依次递减
1.强引用:把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,是不能被垃圾回收机制回收的,强引用是造成Java内存泄漏的主要原因之一。
2.软引用:需要SoftReference类来实现软引用,被软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象进入回收范围之中进行第二次回收。系统内存足够时不会被回收,不够时会被回收,通常用在内存敏感的程序中。
3.弱引用:也是描述非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。不论当前内存够用都会被回收。需要WeakReference实现。
4.虚引用:需要PhantomReference类实现,不能单独使用,必须和引用队列联合使用,主要是跟踪对象被垃圾回收的状态。
- 垃圾收集算法
1.标记-清除算法
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
存在问题:标记和清除两个过程的效率都不高,标记清除后会产生大量不连续的内存碎片
2.复制算法
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
实现简单,运行高效。但内存缩小为原来的一半,代价较高,如果对象存活率较高时就要进行较多赋值操作,效率降低,所以在老年代不能直接选用这种方法。
3.标记-整理算法
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
4.分代收集算法
根据对象的存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代。老生代的特点是每次垃圾回收时只有少量对象需要被回收(标记-清理或标记-整理),新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,只有少量存活(复制算法),因此可以根据不同区域选择不同的算法。
- 垃圾收集器
1.Serial收集器
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
2.ParNew收集器
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过**-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数**。ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
并行:Parallel 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发:Concurrent 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
3.Parallel Scavenge收集器
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精准控制吞吐量:
控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis和直接设置吞吐量大小-XX:GCTimeRatio
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别,即虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量。
4.Serial Old收集器
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
1.在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
新生代 Parallel Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
5.Parallel Old收集器
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。
如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器别无选择,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:
6.GMS收集器
是一种以获取最短回收停顿时间为目标的收集器。它使用多线程的标记-清除算法。
运行过程分为四部分:
1.初始标记:仅标记GC roots能直接关联到的对象,仍然需要暂停所有的工作线程。
2.并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
3.重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
4.并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
GMS三个缺点:
1.对CPU资源非常敏感
2.GMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
3.使用标记清除算法导致空间碎片产生,无法对大对象分配时会提前触发一次FullGC
7.G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
1.基于标记-整理算法,不产生内存碎片。
2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,仍保留新生代和老年代的概念,但不是物理隔离他们是一部分Region(不需要连续)的集合,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
与其他GC收集器相比,G1具备的特点:
1.并行与并发:充分利用多CPU和多核环境下的硬件优势来缩短停顿时间,G1可以通过并发的方式让程序继续执行
2.分代收集:不需要其他收集器配合就能独立管理整个GC堆
3.空间整合:从整体来看是基于标记-整理算法,从局部来看是基于复制算法,这意味着G1运作期间不会产生内存空间碎片,不会在分配大的对象时因为无法找到连续空间而提前次触发下一次GC
4.可预测的停顿:建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。(之所以能建立停顿时间模型是因为它可以有计划的避免在整个Java堆中及逆行全局域的垃圾收集)
运行过程:
1.初始标记
2.并发标记
3.最终标记
4.筛选回收
- 内存分配与回收策略
- -XX:SurvivorRatio=8 新生代中Eden区和一个Survivor区的空间比例是8:1
- -Xms 为jvm启动时分配的内存
- -Xmx 为jvm运行过程中分配的最大内存
- -Xmn 设置年轻代大小
- -Xss 为jvm启动的每个线程分配的内存大小
第四章 虚拟机性能监控与故障处理工具
jps:显示指定系统内所有HotSpot虚拟机进程
jstat:用于收集HotSpot虚拟机各方面的运行数据
jinfo:显示虚拟机配置信息
jmap:生成虚拟机的内存转储快照(heapdump文件)
jhat:用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstack:显示虚拟机的线程快照
第六章 类文件结构
- Class类文件的结构
- Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件都是运行的必要数据,没有空隙存在。
- Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。
无符号数:数据基本数据类型,以u1、u2、u4、u8来表示1248个字节的无符号数,它可以用来描述数字、索引引用、数量值或按照UTF-8编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯以“_info”结尾。用来描述具有层次关系的复合结构的数据。 - 魔数:每个Class文件的头4个字节称为魔数(0123),作用是确定这个文件是否为一个能被虚拟机接受的Class文件,如gif或者jpeg等文件头中都存有魔数。
- 版本号:紧接着魔数的后四个字节(5678),第五和第六个字节是次版本号,第七和第八个字节是主版本号
- 常量池:主次版本之后的是常量池入口,常量池为Class文件中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时他还是Class文件中第一个出现的表类型数据项目。常量池的索引是从1开始的。第0项常量空出来是因为要满足后面某些指向常量池的索引值的数据在执行情况下需要表达“不引用任何一个常量池项目”的含义。
常量池主要存放两大类常量:字面量和符号引用
字面量:接近于Java语言层面上的常量概念,如文本字符串、声明为final的常量值等。
符号引用:属于编译原理方面的概念,包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
Java在进行Javac编译时,和C/C++不一样,并不存在连接这一步骤,而是**在虚拟机加载Class文件的时候进行动态连接。**就是Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,无法被虚拟机直接使用。
常量池的每一项常量都是一个表,可以通过javap工具来分析Class文件字节码。 - 访问标志:常量池之后的两个字节代表访问访问标志,用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,是类的话是否声明为final等。
- 类索引、父类索引与接口索引集合:类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
- 字段表集合:用于描述接口或者类中声明的变量,字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
- 方法表集合:如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息
- 属性表集合:在Class文件、字段表、方法表都可以携带自己的属性表集合。
Code属性:Java程序方法体的代码经过Javac编译器处理后,编译后的字节码存储在其中。接口和抽象类的方法就不存在Code属性。
Exception属性:列举出方法中可能抛出的受查异常,也就是方法描述时throws关键字后面列举的异常。
LineNumberTable属性:描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable属性:描述栈帧中局部变量表中的变量和Java源码中定义的变量的关系。
SourceFile属性:记录生成这个Class文件的源码文件名称
ConstantValue属性:通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这个属性
InnerClasses属性:记录内部类与宿主类之间的关联
Deprecated及Synthetic属性:都属于标志类型的布尔属性,只有存在和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类、字段或者方法已经被作者定为不再推荐使用,@deprecated注解使用;Synthetic属性代表此字段或方法并不是由Java源码产生的,二十由编辑器自己添加的。
StackMapTable属性:在虚拟机类加载字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据分析的类型验证推导验证器。
Signature属性:记录泛型类型签名信息
BootStrapMethods属性:保存invokedynamic执行引用的引导方法限定符
- 字节码指令
大部分指令都没有支持整数类型byte、char、short或时boolean类型,编译器会在编译期或者运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型,随后使用相应的int字节码指令进行操作处理。
加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
将一个局部变量加载到操作栈:iload
将一个数值从操作数栈存储到局部变量表:istore
将一个常量加载到操作数栈:iconst_
扩充局部变量表的访问索引:wide
运算指令:
加法:iadd,ladd
减法:isub,lsub
类型转换指令:Java虚拟机直接支持以下数值类型的宽化类型转换,即小转大
int->long float double
long->float double
float->double
。。。
第七章 虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
- 类加载时机
类从被加载到虚拟机内存中开始,直到卸出内存为止、它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载这七个阶段。其中验证、准备、解析这三个部分统称为连接。
加载、验证、准备、初始化和卸载这五个阶段顺序是确定的,类的加载过程必须按这种顺序开始,而解析不一定,它可以在初始化阶段之后再开始。
初始化
1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。(使用new关键字实例化对象、读取或设置一个类的静态字段(被finnal修饰、已在编译器就把结果放入常量池的静态字段除外)、调用一个类的静态方法)
2.使用java.lang.reflect包的方法对类进行反射调用,如果类没有被初始化,必须先触发其初始化
3.当初始化一个类时,如果其父类还没有进行过初始化,则需先触发其父类的初始化。
4.虚拟机启动时,用户需要指定一个要执行的主类(含main()方法的类)虚拟机会先初始化这个主类
5.当使用JDK 1.7的动态语言支持时,如果-个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、 REF_putStatic、 REF _invokesStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
具体场景:
1.子类引用父类的静态字段,不会导致子类初始化
2.通过数组定义来引用类不会触发此类的初始化
3.常量在编译阶段会存入调用的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
接口和类的初始化区别:
接口中不能使用static{}语句块,当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类接口全部都初始化完成,只有在真正用到父接口的时候才会初始化。
- 加载
加载是类加载过程的一个阶段。加载和连接阶段部分内容是交叉进行的。
虚拟机需要在加载阶段完成的三件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证
验证是连接阶段的第一步,这一阶段目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1.文件格式验证:字节流是否符合Class文件格式规范(这阶段的验证基于二进制字节流进行,通过这个阶段的验证字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段都是基于方法区进行的,不会再直接操作字节流)
2.元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
4.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,将在连接的第三个阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外的信息进行匹配性校验,可以确保解析动作能正常执行。
- 准备
是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中进行分配。此时进行内存分配的仅包括类变量(static修饰)而不包括实例变量。实例变量将在对象实例化时随着对象一起分配在堆中。此时的初始值时数据类型的零值。
public static int value=123;
准备阶段之后的value值为0而不是123,在初始化阶段才会赋值为123
- 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
**符号引用:**以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现地内存布局无关,引用地目标不一定已经加载到内存中。
**直接引用:**可以是直接指向目标地指针、相对偏移量或是一个能间接定位到目标地句柄。直接引用是和虚拟机实现地布局相关地,同一个符号引用在不通虚拟机实例上翻译出来的直接引用一般不同,如果有了直接引用那么引用地目标必定在内存中存在。
解析动作主要针对于类或接口、字段、类方法、接口方法、方法实例、方法句柄和调用点限定符七类符号。
- 初始化
到了初始化阶段才正式开始执行类中定义的Java程序代码,在准备阶段变量已经赋值过一次系统要求的初始值,而在初始化阶段,执行类构造器<client>()方法,<client>()方法是编译器自动收集类中的所有变量的复制动作和静态语句块中的语句合并产生的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,前面的静态语句块可以赋值但不能访问。
- 类加载器
通过一个类的全限定名来获取描述此类的二进制字节流,这个动作放在Java虚拟机外部去实现,实现这个动作的代码模块就叫类加载器。
双亲委派模型
从Java虚拟机的角度来看只有两种不同的类加载器:
1.启动类加载器(Bootstrap ClassLoader):使用C++实现,是虚拟机自身的一部分
2.其他的类加载器:由Java语言实现,独立于虚拟机之外,并且全都继承自抽象类java.lang.ClassLoader
从Java开发人员角度来看有三种:
1.启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的类库加载到虚拟机中。
2.扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\etc目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库
3.应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库
类加载的双亲委派模型:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里类加载器之间的父子关系一般不会以继承的关系实现,通过组合关系来复用父加载器的代码。
工作过程:如果一个类加载器收到了类加载请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次都如此,因此所有的类加载请求都应该最终传达到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,字加载器才会尝试自己去加载。
使用双亲委派模型的好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系
破坏双亲委派模型:
1.自定义类加载器,重写loadClass方法;
2.使用线程上下文类加载器,父类加载器请求子类加载器去完成类加载任务。
这里有一个总结图~
第八章 虚拟机字节码执行引擎
- 运行时栈帧结构
栈帧是虚拟机运行时数据区中的虚拟机栈的栈元素,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。从方法的调用开始到执行完成,都对应着一个栈帧在虚拟机栈里从入栈到出栈。
一个栈帧需要分配多少内存,不会受到程序运行期数据变量的影响,仅取决于具体的虚拟机实现。
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表建立在线程的堆栈上,是线程私有的数据,无论独写的Solt(变量槽,容量的最小单位)是否为原子操作,都不会引起数据安全的问题。虚拟机通过索引定位的方式使用局部变量表。
操作数栈
也常称为操作栈,它是一个后入先出栈(FIFO),方法开始执行的时候,这个方法的操作数栈是空的,在方法执行过程中会有各种字节码指令往操作数栈中写入和提取内容,即入栈出栈。
大多数虚拟机实现中会做一些优化处理,令两个栈帧出现一部分重叠,让下面栈帧的部分操作数栈与上面的部分局部变量表重叠在一起,做到共用一部分数据,无需进行额外的参数复制传递。
动态连接
每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。
1.正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,这时候可以能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定。
2.异常完成出口:方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,只要是在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来决定,栈帧中一般不会保存。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
- 方法调用
方法调用不等同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。
解析
调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析。
解析调用是一个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分配调用则可能是静态的也可以能是动态的。
分派
1.静态分配
虚拟机在重载时是通过参数的静态类型而不是实际类型作为依据,而静态类型在编译期可知,因此在编译阶段Javac编译器会根据参数的静态类型决定使用哪个重载版本。
Human man =new Man();
Human就是变量的静态类型,Man称为变量的实际类型。
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。典型应用就是方法重载。
2.动态分派
运行期根据实际类型确定方法执行版本的分派过程叫做动态分派,典型应用是方法重写。
3.单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
4。动态分配的实现
为类在方法区中建立一个虚方法表,使用虚方法表索引来替代元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址,方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
5.掌控方法分派规则
第十章 早期(编译器)优化
Javac编译器本身由java语言编写。
编译过程:
1.解析与填充符号表过程
2.插入式注解处理器的注解处理过程
3.分析与字节码生成过程
- 解析与填充符号表
解析包括了词法分析和语法分析两个过程。
1.词法、语法分析
词法分析是将源代码的字符流转换为标记(Token)集合,单个字符式程序编写过程的最小元素,而标记则是编译过程的最小元素。
语法分析是根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每个结点都代表着程序中的一个语法结构。
2.填充符号表
- 注解处理器
- 语义分析与字节码生成
1.标注检查
变量使用前是否声明,变量与赋值之间的数据类型能否匹配。
2.数据及控制流分析
对程序上下文逻辑更进一步的验证,可以检查出程序局部变量使用前是否赋值,方法每条路径是否都有返回值等。
3.解语法糖
计算机语言中添加的某种语法,这种语法对功能并没有影响,但更方便程序员使用,可以增强程序的可读性。
4.字节码生成
不仅仅把前面各个步骤生成的信息(语法树、符号表)转化成字节码写入到磁盘中,编译器还进行了少量的代码添加和转换工作。
- Java语法糖的味道
泛型与类型擦除
泛型本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数,这种参数类型可用在类、接口和方法的创建中,称为泛型类,泛型接口和泛型方法。Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
自动装箱、拆箱与遍历循环
public static void main(String []args)
{
Integer a=1;
Integer b=2;
Integer c=3;
Integer d=3;
Integer e=321;
Integer f=321;
Long g=3L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
}
输出
true
false
true
true
true
false
条件编译
根据布尔值的真假,编译器会把分支中不成立的代码块擦除。
第十一章 晚期(运行期)优化
热点代码:虚拟机认定某个方法或代码块运行特别频繁的代码(1.被多次调用的方法 2.被多次执行的循环体)
JIT编译器(即时编译器):把热点代码编译成与本地平台相关的机器码
- 解释器和编译器
当程序需要快速启动和执行时,解释器首先发挥作用,省去编译的时间,立即执行。
程序执行后,编译器把越来越多的代码编译成本地代码,可以获取更高的效率。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler或简称为C1和C2编译器。Cilent Compiler获取更高的编译速度,Server Compiler获取更好的编译质量。
热点探测:判断一段代码是不是热点代码,需不需要触发即时编译,这样的行为称为热点探测。
热点探测的方法:
1.**基于采样的热点探测:**虚拟机周期性地检查各个线程地栈顶,如果发现某些方法经常出现在栈顶,就是“热点方法”。 好处是实现简单,高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法地热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
2.基于计数器的热点探测:会为每个方法建立计数器,统计方法执行次数,如果超过某个阈值,就认定是热点方法。优点统计结果准确,缺点实现麻烦,需要为每一个方法建立并维护计数器,而且不能直接获取到方法的调用关系。
HotSpot虚拟机使用第二种方法,为每个方法创建了方法调用计数器和回边计数器![方法调用计数器![](https://img-blog.csdnimg.cn/20200714150346168.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzA0OTcyNQ==,size_16,color_FFFFFF,t_70)
ServerCompiler会执行一些优化动作:无用代码消除,循环展开,循环表达式外提,消除公共子表达式,常量传播,基本块重排序等。
- 编译优化技术
方法内联(1.去除方法调用的成本 2.为其他优化建立良好基础)
冗余访问消除
复写传播
无用代码消除
1.公共子表达式消除
如果一个表达式E已经计算过了,并且先前的计算到现在E中所有变量的值都没有变化,那么E的这次出现就成为了公共子表达式,对于这种表达式不必花时间计算,直接替代即可。
2.数组边界检查消除
3.方法内联
把目标方法的代码”复制“到发起调用的方法之中,避免发生真实的方法调用。
4.逃逸分析
分析对象动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为参数传递到其他方法中,称为方法逃逸。甚至可能被外部线程访问到,比如赋值给类变量或者可以在其他线程中访问的实例变量,称为线程逃逸。
栈上分配:大量的对象随着方法的结束自动销毁,垃圾收集系统的压力就会减少。
同步清除:如果逃逸分析确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就不会有竞争,对这个变量的同步措施就可以清除
标量替换:如果把一个Java对象拆散根据程序访问的清空,将其使用到的成员变量恢复原始类型来访问。
第十二章 Java内存模型与线程
- Java内存模型
主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则。规定所有的变量(实例字段、静态字段和构成数组对象的元素,不包括局部变量与方法参数)都存储在主内存中。
每条线程都有自己的工作内存,其中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作等都必须在工作内存中操作,不能直接读写主内存的变量,不同线程之间也不能直接访问对方工作内存的变量,线程间的变量传递需要通过主内存完成。
内存间交互操作
虚拟机实现时必须保证下面每一种操作都是原子的,不可再分的。
lock,unlock,read,load,use,assign(赋值),store(存储),read
对于volatile型变量的特殊规则
volatile关键字的作用:
1.第一个语义只保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,其他线程可以立即得知。而普通变量则不行,在线程间的传递需要通过主内存完成。
不能保证数据的原子性,需要保证原子性就需要1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值2.变量不需要与其他的状态变量共同参与不变约束
2.第二个语义是禁止指令重排优化,因为指令重排会干扰程序的并发执行
设置了volatile的变量相当于在赋值后加了一个内存屏障,指令重排时不能把后面的指令重新排序到内存屏障的前面
对long和double型变量的特殊规则
对于64位的数据类型,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即虚拟机可以实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的long和double的非原子性协定。
原子性、可见性、有序性
Java内存模型的三个特征:原子性、可见性、有序性
原子性:由java内存模型来直接保证的原子性变量操作包括load、read、assign、use、store、write,可以认为基本数据类型的访问读写是具备原子性的。
可见性:当一个线程修改了共享变量的值,其他线程能够立刻知道这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量,这种依赖主内存作为传递媒介的方式来实现可见性。 普通变量和volatile变量的区别是:volatile的特殊规则保证了新值能够立刻同步到主内存,以及每次使用前立即从主内存刷新。
还有synchronized和final可以实现可见性,synchronized的可见性是由"对一个变量执行unlock操作之前,必须先把此变量同步回主内存中"这条规则获得的,final的可见性是指,被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看到finnal字段的值。
**有序性:**如果在本线程中观察所有操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。
Java提供了volatile和synchronized保证有序性,volatile关键字本身包含了禁止指令重排的语义,而synchronized“一个变量在同一时刻只允许一条线程对其进行lock操作”,保证了持有同一个锁的两个同步块只能串行进入。
先行发生原则
是Java内存模型中定义的两项操作之间的编序关系,如果说A先行发生于B,其实就是发生操作B之前,A产生的影响能被B观察到,影响包括修改了内存中共享变量的值、发送了消息、调用了方法等。
Java中的先行发生关系:
程序次序规则、管程锁定规则、volatile变量规则、线程启动规则、线程终止规则、线程终结规则、传递性。
- Java与线程
实现线程的三种方式:
1.使用内核线程实现:直接由操作系统内核支持的线程,由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
2.使用用户线程实现
3.使用用户线程加轻量级线程混合实现
Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有:1.协同式线程调度 2.抢占式线程调度
状态转换
1.新建(new):创建后尚未启动的线程处于这种状态
2.运行(Runable):包括了操作系统线程状态中的Running和Ready,此状态的线程可能在执行,也有可能在等待CPU为它分配执行时间。
3.无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,他们需要等待被其他线程显示唤醒
4.限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其他线程显示换性,在一定时间后他们会由系统自动唤醒。
5.阻塞(Blocked):等待着获取到一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生,在程序等待进入同步区域的时候,线程进入阻塞状态,而“等待状态“则是等待一段时间,或者唤醒动作的发生。
6.结束(Terminated):已终止线程的线程状态,线程已经结束执行。
第十三章 线程安全与锁优化
- Java中的线程安全
线程安全的代码:本身封装了所有必要的正确性保证手段(互斥同步),令调用者无需关心多线程问题,更无需自己采取任何措施去保证多线程的正确调用。
把Java语言中各种操作共享的数据分为五类:1.不可变 2.绝对线程安全 3.相对线程安全 4.线程兼容 5.线程对立
1.不可变
不可变得对象一定是线程安全的,无论是对象的实现还是方法的调用者,都不需要再采用任何线程安全的保障措施,只要一个不可变的对象被正确的构建出来,那其外部的可见状态永远不会改变,永远不会看到它在多个线程之中处于不一致的状态。“不可变“带来的安全性是最简单最纯粹的。
基本数据类型:定义是使用final关键字修饰就可以保证不可变性
对象:需要保证对象行为不影响自己状态,最简单的途径就是把对象中带有状态的变量都声明为final,这样在构造函数之后他就是不可变的
2.绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施
3.相对线程安全
需要保证对这个对象单独的操作是线程安全的,我们在调用时不需要做额外的保障措施,但对于一些特定顺序的连续调用,就需要在调用段使用额外的同步手段保证调用的正确性
大多数线程安全类都属于这种类型:Vector、HashTable、Collections的synchronizedCollection()方法包装的集合。
4.线程兼容
指对象本身并不是线程安全的,但可以通过调用端正确的使用同步手段保证对象在并发环境中安全使用。如对应的ArrayList和HashMap
5.线程对立
无论调用端是否采取同步措施,都无法在多线程环境中并发使用的代码。
如Thread类的suspeng()和resume()方法。
- 线程安全的实现方法
1.互斥同步
同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,而互斥是实现同步的一种手段,临界区、互斥量和信号线都是主要的互斥实现方式。
最基本的互斥同步手段就是synchronized关键字,编译之后会在同步块前后形成monitorenter和monitorexit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。也可以使用重入锁ReentrantLock来实现。
互斥同步带来的最大问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步,互斥同步是一种悲观的并发策略
2.非阻塞同步
基于冲突检测的乐观并发策略,先操作,如果没有其他线程争用共享数据那么操作成功,如果有再采取补偿措施。这种乐观的并发策略不需要把线程挂起,称为非阻塞同步。
比较并交换(CAS):需要三个操作数,内存位置即更新的变量V,旧值A,新值B。CAS指令执行时,当且仅当V符合A时,处理器用B更新V的值,否则说明其他线程已经改过了,不更新,但无论是否更新V,都会返回V的就职,这就是个原子操作。
CAS的ABA问题:如果V初次读取时候是A,准备赋值时检测仍是A,但如果这个阶段被该成过B又被改为A,那CAS操作就会认为它没有改变过。
3.无同步方案
1.可重入代码:也叫纯代码,可以在执行的任何时刻终端他,转去执行另一段代码,而在控制权返回后,原来的程序不会出现任何错误。它可以保证线程安全。
2.线程本地存储
- 锁优化
1.自旋锁与自适应自旋
让线程等待,我们只需要让线程执行一个忙循环(自旋),这就是所谓的自旋锁。
2.锁消除
对一些代码是要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。
判定依据来源于逃逸分析的数据支持,如果判断一段代码中,堆上所有的数据都不会逃逸出去从而被其他线程访问到,说明这是线程私有的,可以无需同步加锁。
3.锁粗化
如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,那就会把枷锁范围扩展(粗化)到整个操作序列的外部。
4.轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
5.偏向锁
目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。