虚拟机 JVM高级特性与最佳实践》 读后日志"/>
《深入理解java虚拟机 JVM高级特性与最佳实践》 读后日志
深入理解Java虚拟机 JVM高级特性与最佳实践 读后日志
- 走进java
Java技术的一个重要优点是:在虚拟机层面隐藏了底层技术的复杂性以及机器与操作系统的差异性。
-
- 走进Java/2
- 概述
- java不仅仅是一门编程语言,这是一个由一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系统、移动终端、企业服务器、大型机等各种场合。
- Java技术体系
- Sun官方所定义的java技术体系:
- Java程序设计语言
- 各种硬件平台上的java虚拟机
- Class文件格式
- Java API类库
- 来自商业机构和开源社区的第三方java类库
- JDK(Java Development Kit)是用于支持java程序开发的最小环境,包括:
- Java程序设计语言
- Java虚拟机
- Java API类库
- JRE(Java Runtime Environment)是支持Java程序运行的标准环境,包括:
- Java SE API子集
- Java虚拟机
- Java技术体系可以分为4个平台:
- Java Card:支持一些java小程序(Applets)运行在小内存设备(如智能卡)上的平台。
- Java ME(Micro Edition):支持java程序运行在移动端(手机、PDA)上的平台。对java API有所精简,并加入了针对移动终端的支持,这个版本以前称为J2ME。
- Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的java平台,提供了完整的java核心API,这个版本以前称为J2SE。
- Java EE(Enterprise Edition):支持使用多层架构的企业应用(如ERP、CRM应用)的java平台,除了提供Java SE API外,还对其做了大量的扩充并提供了相关的部署支持,这个版本以前称为J2EE。
- Java虚拟机发展史:
- Sun Classic/Exact VM
- Sun HotSpot VM
- Sun Mobile-Embedded VM/Meta-Circular VM
- BEA JRockit/IBM J9 VM
- Azul VM/BEA Liquid VM
- Apache Harmony/Google Android Dalvik VM
- Microsoft JVM
- 展望Java技术未来:
- 模块化
- 混合语言
- 多和并行
- 进一步丰富语法
- 64位虚拟机
- 实战:自己编译JDK
- Sun官方所定义的java技术体系:
- 概述
- 走进Java/2
- 自动内存管理机制
- Java内存区域与内存溢出异常
- 运行时数据区域:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
- 程序计数器(Program Counter Register):是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。
- 由于java虚拟机的虚拟机的多线程是通过轮流切换分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程直线计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 程序计数器的内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- Java虚拟机栈(Java Virtual Machine Stacks):java虚拟机栈是线程私有的,与线程的生命周期相同。
- 虚拟机栈描述的是java方法执行的内存模型:每个方法的执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
- 64位长度的long和double类型的数据都会占用2个局部变量空间(slot),其余的数据类型只占用1个。
- 两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果虚拟机栈可以动态扩展(当前大部分的java虚拟机都可以动态扩展只不过java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,将会抛出OutOfMemoryError异常。
- 本地方法栈(Native Method Stack):与虚拟机栈类似,虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈是为虚拟机使用的Native方法服务。
- 也会抛出StackOverflowError异常和OutOfMemoryError异常。
- Java堆(java heap)
- 目的是:存放对象实例,几乎所有的对象实例都在这里分配内存。
- Java虚拟机规范的描述:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都在堆上也渐渐变得不是那么“绝对”了。
- Java堆是垃圾收集器管理的主要区域,因此有很多时候也被称作“GC堆”(Garbage Collected Heap)。
- Java堆可分为:新生代 和 老年代。
- 方法区(Method Area):是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、敞亮、静态变量、即时编译期编译后的代码数据。
- Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆)。
- 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
- 运行时常量池(Runtime Constant Pool):术语方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类 加载后进入方法区的运行时常量池中存放。
- 运行时常量池具备动态性,java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的internal()方法。
- 当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
- 直接内存(Direct Memory):
- NIO(New Input/Output)类引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
- 受物理内存大小限制,可能会导致OutOfMemoryError。
- 程序计数器(Program Counter Register):是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。
- HotSpot虚拟机对象探秘
- 对象的创建:
- 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池总定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后就可以确定下来:
- 假设java堆中内存时绝对规整的,所有用过的内存都放在一边,空闲的内存都放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
- 如果java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种方式称为“空闲列表”(Free List)。
- 对象的创建:
- 运行时数据区域:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
- Java内存区域与内存溢出异常
选择哪种分配方式由java堆是否规整决定,而java堆是否规整由所采用的垃圾收集器是否带有压缩功能决定。
-
-
-
-
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。
- 接下来,虚拟机要对对象进行必要的设置,例如这个对象时哪个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息(这些信息均存放在对象头中(Object Header))。
- 对象的内存布局:
- Hotspot虚拟机中对象在内存中存储的布局分为:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- 对象头包括两部分信息:
- 用于存储对象自身运行时数据
- 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 实例数据是对象真正存储的有效信息,也是在程序代码中所定义的名称类型的字段内容。
- 对其填充并不是必然存在的。
- 对象头包括两部分信息:
- Hotspot虚拟机中对象在内存中存储的布局分为:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- 对象的访问定位
- Java程序需要通过栈上的reference数据来操作堆上的具体对象。
- 主流访问方式:
- 使用句柄访问:java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。正在上传…重新上传取消正在上传…重新上传取消
- 使用直接指针访问,java堆对象放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。正在上传…重新上传取消
- 实战:OutOfMemoryError异常
-
-
- 垃圾收集器与内存分配策略
- 如何判断对象是否死亡
- 引用计数器算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器减1;任何时刻计数器为0的对象是不能在被使用的。
- Java虚拟机里没有使用引用计数算法来管理内存,最主要原因是它很难解决对象间相互循环引用的问题。
- 可达性分析算法:通过一些列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则证明此对象时不可用的。
- Java中可以作为GC Roots对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法区中JNI(即一般说的Native方法)引用的对象。
- Java中可以作为GC Roots对象:
- Java中的引用:
- 强引用:强引用在,垃圾回收器就不会回收被引用的对象。
- 软引用:SoftReference,当将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
- 弱引用:WeakReference,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时受到一个系统通知。
- 对象的真正死亡要经过两次标记过程:
- 如果对象在进行一次可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机都将这两种情况视为“没有必要执行”。
- 如果这个对象判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。如果在finalize()方法中对象重新与引用链上的任何一个对象建立了关联,那在第二次标记时会将它移除出“即将回收”的集合,如果对象这个时候还没有逃脱那基本上它就真的被回收了。
- 任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
- 回收方法区(对应HotSpot虚拟机中的永久代):
- 永久代的垃圾收集主要回收两部分内容:废弃常量、无用的类
- 判定无用的类的条件:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 引用计数器算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器减1;任何时刻计数器为0的对象是不能在被使用的。
- 垃圾收集算法
- 标记 - 清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 如何判断对象是否死亡
-
正在上传…重新上传取消
-
-
-
-
- 缺点:
- 效率低下,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 缺点:
- 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
-
-
-
正在上传…重新上传取消
-
-
-
-
- 现在商业虚拟机都采用这种收集算法来回收新生代。
- 标记 - 整理算法:过程与“标记 - 清除”算法一样,但后续步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
-
-
正在上传…重新上传取消
-
-
-
- 分代收集算法:根据对象存活周期的不同将内存划分为几块,一般把java堆内存分为新生代和老年代。
- 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 老年代中因为对象的存活率高、没有额外的空间对它进行分配担保,就必须使用“标记 - 清理”或者“标记 - 整理”算法进行回收。
- 分代收集算法:根据对象存活周期的不同将内存划分为几块,一般把java堆内存分为新生代和老年代。
- HotSpot的算法实现
- 枚举根节点:
- 从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Root的节点主要在全局性的引用(如常量、静态属性)和执行上下文(如栈帧中的本地变量表)中。
- 在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到得知对象引用地址这一目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
- 安全点:
- 程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。
- 线程停顿方式:抢先式中断和主动式中断。
- 抢先式中断:不需要现成的执行代码主动配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上(没有虚拟机采用这种方式)。
- 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
- 安全区域:指在一段代码片段中,引用关系不会发生变化。
- 枚举根节点:
- 垃圾收集器
-
-
正在上传…重新上传取消
-
-
-
- Serial收集器:这是一个单线程收集器,它只会使用一个CPU或一条现成去完成垃圾收集工作,在它进行垃圾收集时,必须暂停其他所有的工作现成,直到它收集结束。
-
-
正在上传…重新上传取消
-
-
-
- ParNew收集器:就是Serial收集器的多线程版本,使用多条线程进行垃圾收集。除了Serial收集器外,目前只有它能与CMS收集器配合工作。
-
-
正在上传…重新上传取消
-
-
-
- Parallel Scavenger收集器:是一个新生代收集器,也是使用复制算法收集器,且是并行的多线程收集器,目标是达到一个可控制的吞吐量(Throughput)。
- 停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调节这些参数以提供最适合的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
- Parallel Scavenger收集器:是一个新生代收集器,也是使用复制算法收集器,且是并行的多线程收集器,目标是达到一个可控制的吞吐量(Throughput)。
-
-
正在上传…重新上传取消
-
-
-
- Serial Old收集器:是一个单线程收集器,使用“标记 - 整理”算法。
-
-
正在上传…重新上传取消
-
-
-
- Parallel Old收集器:是Parallel Scavenger收集器的老年代版本,使用多线程和“标记 - 整理”算法。
-
-
正在上传…重新上传取消
-
-
-
- CMS收集器(Concurrent Mark Sweep):是一种以获取最短回收停顿时间为目标的收集器,是基于“标记 - 清除”算法实现的。
-
-
正在上传…重新上传取消
-
-
-
-
- 收集过程:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 缺点:
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
- 收集结束时会有大量空间碎片产生。
- 收集过程:
- G1收集器:
-
-
-
正在上传…重新上传取消
-
-
-
-
- 特点:
- 并发与并行:不需要停顿java线程执行的GC动作
- 分代收集:通过采用不同的方式去处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象以获得更好的手机效果。
- 空间整合:整体上看是基于“标记 - 整理”算法,局部(两个Region之间)上看是基于“复制”算法实现的。
- 可预测的停顿:建立了可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过M毫秒。
- 运行过程:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
- 特点:
- 理解GC日志:
- 当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以看到user或sys时间超过real时间完全正常。
- 垃圾收集器参数总结:
-
-
-
参数 | 描述 |
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew+ CMS + Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器组合进行内存回收 |
UserParallelOldGC | 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Survivor = 8:1 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
MaxTenuringThreshold | 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代 |
UseAdaptiveSizePolicy | 动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时进行内存回收的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值是99, 即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效 |
MaxGCPauseMillis | 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupancyFraction | 设置CMS收集器在老年代时间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |
-
-
- 内存分配与回收策略:
-
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
正在上传…重新上传取消
-
-
-
- 对象优先在Eden分配
- 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的手机策略里有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
- 大对象直接进入老年代
- 大对象指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。
- 长期存活的对象将进入老年代
- 动态对象年龄判定:
- 虚拟机并不是永远都要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
- 空间分配担保
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
- 对象优先在Eden分配
-
- 虚拟机性能监控与故障处理工具
- JDK的命令行工具
- jps:虚拟机进程状况工具
- 可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。
- jps [ option ] [ hostid ]
- jstat:虚拟机统计信息监视工具
- 用于监视虚拟机各种运行状态信息的命令行工具。
- jstat [ option vmid [ interval [ sims ] [ count ] ] ]
- jinfo:Java配置信息工具
- 作用是实时查看和调整虚拟机各项参数。
- jinfo [ option ] pid
- jmap:Java内存映射工具
- 用于生成堆转储快照(一般称为heapdump或dump文件)
- jmap [ option ] vmid
- jhat:虚拟机堆转储快照分析工具
- 用来分析jmap生成的堆转储快照
- jstack:Java堆栈跟踪工具
- 用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。
- jstack [ option ] vmid
- HSDIS:JIT生成代码反汇编
- jps:虚拟机进程状况工具
- JDK的可视化工具
- JConsole:Java监视与管理控制台:基于JMX的可视化监视、管理工具。
- VisualVM:多合一故障处理工具
- JDK的命令行工具
- 调优案例分析与实战
- 案例分析
- 高性能硬件上的程序部署策略
- 集群间同步导致的内存溢出
- 堆外内存导致的溢出错误
- 外部命令导致的系统缓慢
- 服务器JVM进程崩溃
- 不恰当数据结构导致内存占用过大
- 由Windows虚拟机导致的长时间停顿
- 实战:Eclipse运行速度调优
- 案例分析
-
- 虚拟机执行子系统
- 类文件结构
- 无关性的基石
- 实现语言无关性的基础仍然是虚拟机和字节码存储格式。
- 无关性的基石
- 类文件结构
正在上传…重新上传取消
-
-
- Class类文件的结构:有 无符号数和表 两种数据类型
-
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以“_info”结尾。
- 无论符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
类型 | 名称 | 数量 |
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文件版本
- 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用时确定这个文件是否为一个能被虚拟机接受的Class文件。(java中为0xCAFEBABE)
- 第5和第6个字节是次版本号(Minor Verson)
- 第7和第8个字节是主版本号(Major Version)
- 常量池(紧随主次版本号)
- Class文件结构中只有常量池的容量计数是从1开始的。
- 常量池包括两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 字面量:比较接近于java语言层面的常量概念,如文本字符串、声明为final的常量值等。
- 符号引用:属于编译原理方面的概念,包括下面三类常量:
- 类和接口的权限定名(Fully Qualified Name)
- 字段和名称的描述符(Descriptor)
- 方法的名称和描述符
- 魔数与Class文件版本
-
-
正在上传…重新上传取消
正在上传…重新上传取消
-
-
-
- 访问标志(接着常量池)
- 这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
- 访问标志(接着常量池)
-
-
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 是否为Public类型 |
ACC_FINAL | 0x00 10 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x00 20 | 是否允许使用invokespecial字节码指令的新语义. |
ACC_INTERFACE | 0x02 00 | 标志这是一个接口 |
ACC_ABSTRACT | 0x04 00 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x10 00 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x20 00 | 标志这是一个注解 |
ACC_ENUM | 0x40 00 | 标志这是一个枚举 |
-
-
-
- 类索引、父类索引与接口索引集合
- 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
- 字段表集合
- 字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
- 可以包括的信息:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本数据类型、对象、数组)、字段名称。
- 简单名称是指没有类型和参数修饰的方法或者字段名称。
- 方法表集合
- 包括访问标志(access_flags)、名称索引(name_index)、描述符索引(description_index)、属性表集合(attributes)
- 属性表集合
- Code属性:java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。
- Exceptions属性:Exceptions属性的作用时列举出方法中肯那个抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。
- LineNumberTable属性:用于描述java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
- localVariableTable属性:用于描述栈帧中局部变量表中的变量与java源码中定义的变量之间的关系。
- SourceFile属性:用于记录生成这个Class文件的源码文件名称。
- ConstantValue属性:作用是通知虚拟机自动为静态变量赋值。
- InnerClasses属性:用于记录内部类与宿主类之间的关联。
- Deprecated及Synthetic属性
- Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated。
- Synthetic属性代表此字段或者方法并不是由java源码直接产生的,而是由编译器自行添加的。
- StackMapTable属性:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查器(Type Checker)使用,目前在于代替以前比较消耗性能的基于数据流分析的类型推到验证器。
- Signature属性:
- BootstrapMethods属性:用于保存invokeddynamic指令引用的引导方法限定符。
- 类索引、父类索引与接口索引集合
- 字节码指令简介
- 字节码与数据类型
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令
- 公有设计和私有实现
- 虚拟机实现的方式主要有以下两种:
- 将输入的java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
- 将输入的java虚拟机代码在加载或执行时翻译成宿主机CPU本地指令集(即JIT代码生成技术)
- 虚拟机实现的方式主要有以下两种:
- Class文件结构的发展
-
- 虚拟机类加载机制
- 类加载的时机
-
正在上传…重新上传取消
-
-
-
- 生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。
- 验证、准备、解析3部分统称为连接(Linking)。
- 立即对类进行“初始化”的5种情况:
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF——invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
- 场景:
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
- 类加载的过程
- 加载
- 在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的权限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 数组类(数组的元素类型指的是数组去掉所有维度的类型)的创建过程:
- 如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
- 如果数组的组件类型不是引用数据类型(例如int[]数组),java虚拟机将会把数组C标记为与引导类加载器关联。
- 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将是默认为public。
- 加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未王城,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
- 在加载阶段,虚拟机需要完成以下3件事情:
- 验证
- 加载
-
-
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-
-
-
-
- 文件格式验证:第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证:第二阶段都是对字节码描述的i型南溪进行语义分析,以保证其描述的信息符合java语言规范要求。
- 字节码验证:第三阶段通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:是对自身以外(常量池的各种符号引用)的信息进行匹配性校验。
- 准备
- 是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
- 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 解析动作主要针对:类或借口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
- 初始化:初始化阶段实质性类构造器<clinit>()方法的过程。
- <clinit>()方法是由编译期自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
- <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
- 由于父类的<clinit>()方法先执行完毕,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
- <clinit>()方法对于类或接口并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译期可以不为这个类生成<clinit>()方法。
- 接口中不能使用静态语句块,单仍然有变量初始化的赋值操作,因此接口与类一样都会先生成<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类的初始化时也一样不会执行接口的<clinit>()方法。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕。
-
- 类加载器:通过一个类的全限定名类获取描述此类的二进制字节流
- 类与类加载器
- 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
- 双亲委派模型
- Java虚拟机角度只存在两种不同的类加载器:
- 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
- 另一种是其他类加载器,这些类加载器由java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader。
- 3中类加载器:
- 启动类加载器(Bootstrap ClassLoader):识别加载<JAVA_HOME>\lib目录下的类库
- 扩展加载器(Extension ClassLoader):由sun.Launcher$ExtClassLoader实现,负责<JAVA_HOME>\lib\ext目录下的类库
- 应用程序类加载器(Application ClassLoader):由sun.Launcher$AppClassLoader实现,负责用户路径下的类库
- 双亲委派模型(Parents Delegation Model)中类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是使用组合(Composition)关系来复用父加载器的代码。
- 双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类的加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围都没有找到所需的类)时,子加载器才会尝试自己去加载。
- Java虚拟机角度只存在两种不同的类加载器:
- 类与类加载器
-
-
转存失败重新上传取消
-
-
-
- 破坏双亲委派模型
- 在OSGI(开放服务网关协议,Open Service Gateway Initiative)环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGI将按照下面的顺序进行搜索:
- 将以java.*开头的类委派给父类加载器加载。
- 否则,将委派类表名单内的类委派给父类加载器加载。
- 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
- 在OSGI(开放服务网关协议,Open Service Gateway Initiative)环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGI将按照下面的顺序进行搜索:
- 破坏双亲委派模型
-
- 虚拟机字节码执行引擎
- 概述:
- 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是由自己实现的。
- 执行引擎在执行java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过及时编译器产生本地代码执行)两种选择。
- 运行时栈帧结构
- 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
- 对于执行引擎来说,活动线程中,只有位于栈帧才有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。
- 概述:
-
转存失败重新上传取消
-
-
-
- 局部变量表(Local Variable Table):是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
- 局部变量的容量以变量槽(Variable Slot)为最小单位,每个slot都应能存放一个boolean、byte、char、short、int、float、reference、returnAddress类型的数据。
- 对于64位的数据类型,虚拟机会以告慰对齐的方法为其分配两个连续的slot空间。
- 虚拟机通过索引定位的方式使用局部变量表,索引值的方位是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个。
- 在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的。
- 操作数栈(Operand Stack):也称为操作数栈,它是一个后入先出栈。
- Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。
- 局部变量表(Local Variable Table):是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
-
-
转存失败重新上传取消
-
-
-
- 动态连接(Dynamic Linking):每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接。
- Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每次运行期间转化为直接引用,这部分称为动态连接。
- 方法返回地址
- 方法退出的两种方式:
- 正常完成出口(Normal Method InvocationCompletion)
- 异常完成出口(Abrupt Method Invocation Completion)
- 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
- 方法退出的两种方式:
- 附加信息:虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中。
- 动态连接(Dynamic Linking):每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接。
- 方法调用:方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
- 解析(Resolution):方法在程序真正运行之前就有一个可确定的调用版本,并且这个犯法的调用版本在运行期是不可改变的。这类方法的调用称为解析。
- 静态方法、私有方法、实例构造方法、父类方法,在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法。
- 分派
- 静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。(重载)
- 静态类型与实际类型的区别是,静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
- 动态分派:在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。(重写)
- 单分派与多分派
- 方法的接收者与方法的参数统称为方法的宗量。
- 单分派是根据一个宗量对目标进行选择,多分派则是根据对于一个宗量对目标方法进行选择的。
- Java语言中,静态分派属于多分派;动态分派属于单分派。
- 虚拟机动态分派的实现:
- 使用虚方法表索引来代替元数据查找以提高性能。
- 需方发表中存放着各个方法的实际入口地址。如果某飞方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
- 方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类方发表也初始化完毕。
- 静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。(重载)
- 动态类型语言支持
- 动态类型语言
- 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。
- JDK1.7与动态类型
- Java.lang.invoke包:提供了一种新的动态确定目标方法的机制,称为MethodHandle。
- Reflection与MethodHandle的区别:
- Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。
- Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象包含跟多信息。
- MethodHandle是对字节码的方法指令调用的模拟。理论上支持虚拟机在这方面的各种优化,而Reflection不行。
- Reflection API设计目标只是为了Java语言服务的,而MethodHandle则设计成可服务于所有Java虚拟机之上的语言。
- Reflection与MethodHandle的区别:
- Invokeddynamic指令:每处含有invokeddynamic指令的位置都称为“动态调用点”(Dynamic Call Site)。
- 掌握方法分派规则
- 使用MethodHandle获取祖类方法
- 动态类型语言
- 基于栈的字节码解释执行引擎
- 解析(Resolution):方法在程序真正运行之前就有一个可确定的调用版本,并且这个犯法的调用版本在运行期是不可改变的。这类方法的调用称为解析。
-
-
正在上传…重新上传取消
-
-
-
-
- 解释执行
- Java语言中,Javac编译器完成了程序代码经过词法分析、语义分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在java虚拟机之外进行的,而解释器在虚拟机的内部,所以java程序的编译就是半独立的实现。
- 基于栈的指令集与基于寄存器的指令集
- 基于栈的指令集的优点是可一直,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。
- 栈架构指令集的主要缺点是执行速度相对来说会慢一些。
- 基于栈的解释器执行过程
- 解释执行
-
-
- 类加载及执行子系统的案例与实战
- 案例分析
- Tomcat:正统的类加载器架构
- 案例分析
-
正在上传…重新上传取消
-
-
-
- OSGi:灵活的类加载器架构
- OSGi(Open Service Gateway Initiative)是OSGI联盟(OSGI Alliance)制定的一个基于Java语言的动态模块化规范。
- 在OSGI中,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少在外观上如此),而且类库的可见性能得到非常精确的控制,一个模块里面只有被Export过的Package才可能由外界访问,其他的Package和Class将会隐藏起来。
- OSGi类加载查找规则:
- 以java.*开头的类,委派给父类加载器加载。
- 否则,委派列表名单内的类,委派给父类加载器加载。
- 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
- 否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
- OSGi:灵活的类加载器架构
-
-
正在上传…重新上传取消
-
-
-
- 字节码生成技术与动态代理的实现
- JDK动态代理基于java反射实现的
- Restrotranslator:跨越JDK版本(将高版本转为低版本)
- JDK升级新增功能分类(Restrotranslator只支持前两种):
- 在编译器层面做的改进
- 对Java API的代码增强
- 需要在字节码中进行支持的改动
- 虚拟机内部的改进
- JDK升级新增功能分类(Restrotranslator只支持前两种):
- 字节码生成技术与动态代理的实现
- 实战:自己动手实现远程执行功能
-
-
- 程序编译与代码优化
- 早期(编译期)优化
- 概述
- Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把*.java文件转变成*.class文件的过程;也可能是指虚拟机的后端运行期编译期(JIT,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接吧*.java文件编译成本地机器代码的过程。
- Javac编译器
- Javac的源码与调试
- Javac编译过程大执法可分为:
- 解析与填充符号过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生成过程
- Javac编译过程大执法可分为:
- Javac的源码与调试
- 概述
- 早期(编译期)优化
转存失败重新上传取消
-
-
-
-
- 解析与填充符号表
- 词法、语法分析
- 词法分析是将源代码的字节流转变为标记(Token)集合。
- 语法分析是根据Token序列构造抽象语法树的过程。
- 填充符号表
- 符号表(Symbol Table)是由一组符号地址和符号信息构成的表格。
- 词法、语法分析
- 注解处理器
- 语义分析与字节码生成
- 标注检查
- 数据流及控制流分析
- 解语法糖
- 字节码生成
- 解析与填充符号表
- Java语法糖的味道
- 反省与类型擦除
- 自动装箱、拆箱与循环遍历
- 条件编译
- 实战:插入式注解处理器
-
-
- 晚期(运行期)优化
- HotSpot虚拟机内的即时编译器
- 解释器与编译器
- 解释模式、编译模式、混合模式
- 解释器与编译器
- HotSpot虚拟机内的即时编译器
-
转存失败重新上传取消
-
-
-
- 编译对象与触发条件
- 热点代码:被多次调用的方法、被多次执行的循环体
- 判断一段代码是不是热点代码,是不是需要出发即时编译,这样的行为称为热点探测:
- 基于采样的热点探测
- 基于计数器的热点探测(hotspot使用这种)
- 编译对象与触发条件
-
-
转存失败重新上传取消
方法调用计数器触发即时编译
转存失败重新上传取消
回边计数器触发即时编译
-
-
-
- 编译过程
- Client Compiler编译过程:
- 在第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion,HIR)。HIR使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译期会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HiR之前完成。
- 在第二阶段,一个相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representaion,LIR),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
- 最后阶段是在平台相关的后端使用线性扫描算法(Liner Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。
- Client Compiler编译过程:
- 编译过程
-
-
转存失败重新上传取消
-
-
-
-
- Server Compiler的寄存器分配器是一个全局着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。
- 查看及分析即时编译结果
-
- 编译优化技术
- 优化技术概览
-
-
类型 | 优化技术 |
编译器策略(compiler tactics) | 延迟编译(delayed compilation) 分层编译(tiered compilation) 栈上替换(on-stack replacement) 延迟优化(delayed reoptimization) 静态单赋值表示(static single assignment representation) |
基于性能监控的优化技术(profile-based techniques) | 乐观空值断言(optimistic nullnuess assertions) 乐观类型断言(optimistic type assertions) 乐观类型增强(optimistic type strengthening) 乐观数组长度增强(optimistic array length strengthening) 裁剪未被选择的分支(untaken branch pruning) 乐观的多态内联(optimistic N-morphic inlining) 分支频率预测(branch frequency prediction) 调用频率预测(call frequency prediction) |
基于证据的优化技术(proof-based techniques) | 精确类型推断(exact type inference) 内存值推断(memory value inference) 内存值跟踪(memory value tracking) 常量折叠(constant folding) 重组(reassociation) 操作符退化(operator strength reduction) 空值检查消除(null check elimination) 类型检测退化(type test strength reduction) 类型检测消除(type test climination) 代数简化(algebraic simplification) 公共子表达式消除(common subexpression elimination) |
数据流敏感重写(flow-sensitive rewrites) | 条件常量传播(conditional constant propagation) 基于流承载的类型缩减转换(flow-carried type narrowing) 无用代码消除(dead code elimination) |
语言相关的优化技术(language-specific techniques) | 类型继承关系分析(class hicrarchy analysis) 去虚拟化(devirtualization) 符号常量传播(symbolic constant propagation) 自动装箱消除(autobox elimination) 逃逸分析(escape analysis) 锁消除(lock elision) 锁膨胀(lock coarsening) 消除反射(de-reflection) |
内存及代码位置变换(memory and placement transformation) | 表达式提升(expression hoisting) 表达式下沉(expression sinking) 冗余存储消除(redundant store elimination) 相邻存储合并(adjacent store fusion) 交汇点分离(merge-point splitting) |
循环变换 (loop transformations) | 循环展开(loop unrolling) 循环剥离(loop peeling) 安全点消除(safepoint elimination) 迭代范围分离(iteration range splitting) 范围检查消除(range check elimination) 循环向量化(loop vectorization) |
全局代码调整(global code shaping) | 内联(inlining) 全局代码外提(global code motion) 基于热度的代码布局(heat-based code layout) Switch调整(switch balancing) |
控制流图变换(control flow graph transformation) | 本地代码编排(local code scheduling) 本地代码封包(local code bundling) 延迟槽填充(delay slot filling) 着色图寄存器分配(graph-coloring register allocation) 线性扫描寄存器分配(linear scan register allocation) 复写聚合(copy coalescing) 常量分裂(constant splitting) 复写移除(copy removal) 地址模式匹配(address mode matching) 指令窥空优化(instruction peepholing) 基于确定有限状态机的代码生成(DFA-based code generator) |
-
-
-
- 公共字表达式消除:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生改变,那么E的这次出现就成为了公共字表达式。
- 数组边界检查消除:如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0,foo.length)之内,那在整个循环中就可以把数组的上下界检查消除,这可以节省很多次的条件判断操作。
- 方法内联:编译器在进行内联时,如果是非虚方法,那么直接进行内联就可以了,这时候 的内联是有稳定前提保障的。如果遇到虚方法,则会向CHA(类继承关系分析,Class Hierarchy Analysis)查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那么也可以进行内联,不过这种内联就属于激进优化,需要预留一个“逃生门”,称之为守护内联(Guarded Inlining)。
- 逃逸分析:逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它还可能被外部方法所引用,例如作为调用参数传递到其他方法中,称之为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
- 优化方式:
- 栈上分配
- 同步消除
- 标量替换:标量(scalar)指一个数据已经无法分解成更小的数据来表示了。
- 优化方式:
- Java与C/C++的编译器对比
- Java即时编译器运行占用的是用户程序的运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本。
- Java是动态的类型安全语言,这就意味着需要由虚拟机来确保程序不会违反语言语义或访问非结构化内存。
- Java即时编译器的一些优化难度远大于C/C++静态优化编译器。
- Java语言是可以动态扩展的语言,运行时加载新的类可能改变程序类型的集成关系,编译期无法看到程序的全貌,许多全局的优化措施都只能以激进的方式完成。
- Java语言中对象的内存分配都是堆上进行的,只有方法中的局部变量才能在栈上分配。
-
-
- 高效并发
- Java内存模型与线程
- 硬件的效率与一致性
- Java虚拟机的即时编译器也有指令重排序(Instruction Reorder)优化
- 硬件的效率与一致性
- Java内存模型与线程
正在上传…重新上传取消
-
-
- Java内存模型
- 主内存与工作内存
- Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
- 主内存与工作内存
- Java内存模型
-
正在上传…重新上传取消
-
-
-
- 内存间交互操作
- 操作:
- lock(锁定):作用越主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量,它把一个处于锁定状态的变量释放出来,接是否的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量,的值的字节码指令时会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
- 规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
- 如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就顺序地执行store和write操作。Java内存模型只要求上述两个操作必须顺序执行,没有保证是连续执行。
- 操作:
- 对于volatile型变量的特殊规则
- 被volatile修饰后具备的特性:
- 保证此变量对所有线程的可见性。
- 禁止指令重排序优化
- 可以通过volatile保证原子性的场景:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束
- volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
- 被volatile修饰后具备的特性:
- 对于long和double型变量的特殊规则
- 如果有多个线程共享一个并未声明为volatile的long和double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。
- 原子性、可见性与有序性
- 原子性(Atomicity):
- 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- 有序性(Ordering):如果在本地线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
- 先行先发原则:判断数据是否存在竞争、线程是否安全的主要依据
- 先行先发是java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行先发于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
- Java内存模型下的先行先发关系:
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行先发于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行先发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行先发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行先发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都现行先发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回这等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行先发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行先发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果操作A先行先发生于操作B,操作B先行先发生于操作C,那就可以得出操作A先行先发生于操作C的结论。
- 内存间交互操作
- Java与线程
- 线程的实现(java线程的实现因平台而异):
- 使用内核线程实现
- 线程的实现(java线程的实现因平台而异):
-
-
正在上传…重新上传取消
-
-
-
-
- 使用用户线程实现
-
-
-
正在上传…重新上传取消
-
-
-
-
- 使用用户线程加轻量级进程混合实现
-
-
-
正在上传…重新上传取消
-
-
-
- Java线程调度:指系统为线程分配处理器使用权的过程
- 协同线程调度(Cooperative Threads-Scheduling):下次南横的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
- 抢占式线程调度(Preemptive Threads-Scheduling):线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
- 状态转换:
- 线程的状态:
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runable):Runable包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在执行,也可能正在等待着CPU为它分配执行时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object.wait()方法。
- 没有设置Timeout参数的Thread.join()方法。
- LockSupport.park()方法。
- 限期等待(Timed Waiting):处于这种状态的线程也不会分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后他们会由系统自动唤醒。以下犯法会让线程进入限期等待状态:
- Thread.sleep()方法。
- 设置了Timeout参数的Object.wait()方法。
- 设置了Timeout参数的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
- 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程进入这种状态。
- 结束(Terminated):已终止线程的线程状态,线程已经结束执行。
- 线程的状态:
- Java线程调度:指系统为线程分配处理器使用权的过程
-
-
正在上传…重新上传取消
-
- 线程安全与锁优化
- 线程安全:当多个线程方位一个对象时,若果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用任何其他的协作操作,调用这个对象的行为都可以获取正确的结果,那这个对象时线程安全的。
- Java语言中的线程安全
- 不可变:基本数据类型用final修饰、枚举类、java.lang.Number的部分子类,如Long、Double等数值包装类型、BigInteger和BigDecimal。
- 绝对线程安全:不管运行时环境如何,调用者都不要任何额外的同步措施。
- 相对线程安全:它需啊保证对这个对象单独的操作时线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序地连续调用,就可能需要调用端使用额外的同步手段来保证调用的正确性。
- 线程兼容:指对象本身不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全地使用。
- 线程对立:指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。
- 线程安全的实现方法
- 互斥同步:synchronize、ReentrantLock
- 非阻塞同步:CAS(Compare-and-Swap)
- 无同步方案
- 可重入代码(Reentrant Code):如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可冲入性的要求,也就是线程安全的。
- 线程本地存储(Thread Local Storage):java.lang.ThreadLocal
- 锁优化
- 自旋锁与自适应自旋:
- 为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
- 自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定。
- 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
- 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,这时虚拟机会把锁同步的范围扩展(粗化)到整个操作的外部。
- 轻量级锁:轻量级锁不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。
- 自旋锁与自适应自旋:
- Java语言中的线程安全
- 线程安全:当多个线程方位一个对象时,若果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用任何其他的协作操作,调用这个对象的行为都可以获取正确的结果,那这个对象时线程安全的。
- 线程安全与锁优化
正在上传…重新上传取消
正在上传…重新上传取消
-
-
-
-
- 偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。这个锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
-
-
-
正在上传…重新上传取消
更多推荐
《深入理解java虚拟机 JVM高级特性与最佳实践》 读后日志
发布评论