深入理解Java虚拟机读书笔记

JVM相关知识集合
JVM有两种运行模式Server与Client。
推荐:服务器上请以Server模式运行,面客户端或GUI模式下就以Client模式运行
两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。
在JAVA_HOME/jre/lib/i386/jvm.cfg;中修改配置即可修改运行模式, 但是64位的只有server模式,
32位的JDK一般都支持server和client两种模式。
切换模式只需要将client和server的声明语句互换位置即可
哪个在上面就是哪个模式

并发与并行
并行: 指多条垃圾收集线程并行工作, 但此时用户线程仍然处于等待状态
并发: 指用户线程和垃圾收集线程同时执行, 可能是交替执行,

网络好文
https://www.cnblogs.com/wangzhongqiu/p/8908266.html
https://blog.csdn.net/TJtulong/article/details/89598598
https://blog.csdn.net/ning0323/article/details/76633446
第二章 Java内存区域与内存溢出异常
运行时数据区域:

程序计数器
当前线程所执行的字节码的行号指示器, 字节码解释器改变这个计数器的值来选取下一条需要执行的字节码指令, 循环, 分支等都需要依赖计数器完成
计数器需要是线程私有的,
线程执行Java方法, 计数器记录的是虚拟机字节码指令的地址,
如果是native方法, 则计数器为空
虚拟机规范中没有规定任何oom情况的区域

Java虚拟机栈
线程私有的, 与线程生命周期相同
每个方法执行的同时都会创建一个栈帧, 用于存储局部变量表, 操作数栈等信息,
一个方法调用到执行完毕, 就对应一个栈帧在虚拟机栈中的入栈到出栈过程

局部变量表存放了编译期可知的各种基本类型, 或引用
其中64长度的long和double类型的数据会占用2个局部变量空间(Slot), 其余占一个
局部变量表所需的空间在编译器就完成分配, 进入一个方法中, 需要在帧中分配多少局部空间是完全确定的, 运行期不会改变局部变量表的大小

在Java虚拟机规范中, 在这个区域规定了两种异常,
如果线程请求的深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常
如果虚拟机可以动态扩容, 如果无法申请足够的内存, 抛出OutOfMemoryError异常

Java虚拟机栈的深度: 每次方法调用都会有一个栈帧压入虚拟机栈,操作系统给JVM分配的内存是有限的,JVM分配给“虚拟机栈”的内存是有限的。
如果方法调用过多,导致虚拟机栈满了就会溢出。
这里栈深度就是指栈帧的数量

本地方法栈
与虚拟机栈非常相似, 就是为虚拟机使用的native方法服务的, 抛出的异常也和虚拟机栈一样, 许多具体虚拟机会将虚拟机栈和本地方法栈合二为一, hotspot就是这样

Java堆

Java Heap, 线程共享 存放对象实例, 是垃圾收集器的主要工作地,
现在的收集器都是采用分代收集的,
所以Java堆可以细分为新生代和老年代,
在细致一个eden空间, from survivor, to survivor 等
从内存分配角度来看, 线程共享的堆中可能划分出多个线程私有的分配缓存区(TLAB),
Java堆可以存在不同的物理不连续的内存空间中, 逻辑上需要是连续的, 如果在堆中没有完成实例的分配, 并且也无法扩展空间, 抛出oomError

方法区
线程共享, 存储已被虚拟机加载的类信息, 常量, 静态变量,
Hotspot使用永久代实现了方法区,
Java8中hotspot已经改用native Memory来实现方法区了, Java7中只是将字符串常量池移出到了本地内存,
这部分的回收确实是必要的, 在sun公司出现很多bug就是因为没有对此区收集导致
当无法满足内存需要时, 抛出oomError异常, 尤其注意一些jsp, 代理等操作产生的大量的class

运行时常量池
方法区的一部分
虚拟机加载了Class之后, 这个Class中的常量池, 也就是类用来存放编译器生成的各种字面量和符号引用, 这部分内容将在类加载后进入运行时常量池

运行时常量池相对于Class常量池具备动态性, 常量并不一定编译器产生, 并非一定预置入Class文件, 运行期间也可能将新的常量放入池中, 被开发人员利用较多的就是String.intern方法, 常量池无法申请空间时抛出oomError

直接内存
不是虚拟机规范中的概念, 但是也会出现oom
在Java1.4引入了nio, 引入了基于通道channel与缓冲区的io方式, 可以使用native函数库直接分配堆外内存, 使用不当使内存大于物理限制则oom

Hotspot虚拟机对象探秘

对象的创建
当虚拟机遇到一个new指令时, 先检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查这个类是否已经被加载, 如没有先加载,
接下来为新生对象分配内存, 对象需要内存在类加载后可确定, 把一块确定大小的内存分出来, 两种办法,

一种指针碰撞法, 假设堆中内存是规整的, 用过的放在一边, 没用过的放在另一边, 中间放个指针作为分界点指示器, 分配空间向空闲那边挪动同大小的指针距离,

二种空闲列表, Java堆不规整, 虚拟机需要维护一个列表, 记录那些内存是可用的, 分配时候找个足够大的内存分配, 并更新列表

Java堆规不规整取决于使用的回收器是否带压缩整理

除分配空间外, 还有个问题, 对象创建是非常频繁的, 即使仅仅修改个指针, 在并发下也不是线程安全的, 可能出现给a分配但是指针还没有修改, 又给b分配了原来指针指示的空间,

解决这个问题两种方案, 一种分配空间使用同步, 虚拟机采用cas加上失败重试保证更新原子性, 另一种,把内存分配动作按照线程分在不同空间中进行, 每个线程在堆中预先分配一小块内存, 称为本地线程分配缓冲(TLAB) , 哪个线程要分配内存, 就在哪个线程的TLAB上分配, 只有没有了,才需要同步锁定, 是否使用TLAB功能, 可以用-XX:+/-UseTLAB参数设定

内存分完, 虚拟机将分配到的内存空间都初始化零值, (不包括对象头), 如果使用TLAB, 这一工作也可以提前至TLAB分配时进行, 这一步保证了对象的实例字段在Java代码中可以不付初值就直接使用, 程序能访问到这些字段的数据类型所对应的零值

接下来对对象进行设置, 如这个对象是哪个类的实例, 对象哈希码, gc年龄等信息, 存放在对象头中, 是否偏向锁, 等信息也是对象头中

在上面工作都完成后, 在虚拟机角度一个新对象已经产生, 但Java程序角度对象创建才刚开始, init方法还没有执行, 所有字段还都为零, 所以只有new>init之后按照程序员意愿初始化后才是一个真正可用对象

对象的内存布局
在hotspot虚拟机中对象在内存存储布局分为3块, 对象头, 实例数据, 对齐填充

对象头包含两部分, 第一部分存自身运行时数据, 如哈希码, gc年龄, 锁状态, 持有锁, 偏向锁id, 这部分数据长度在32位中占32bit, 在64位中占64bit, 官方称为Mark Word, 它会根据对象的状态复用自己的存储空间, markword通过标志位标注存储内容的状态,
另一部分是类型指针, 通过它确定是哪个类的实例, 并不是强制要求这个指针, 查找对象元数据并不一定要经过对象. 另外如果对象是数组, 头中还要有一块用于记录数组长度的数据, 因为不能通过数组元信息确定数组的大小

然后是实例数据部分, 是对象存储的有效值, 父类继承和子类定义都要记录, 存储顺序受到虚拟机分配策略参数和字段在源码中定义的上下顺序的影响, hotspot会将相同宽度的字段分配到一起,

第三部分对齐不是必然存在的, 仅仅起到占位符的作用, 因为hotspot要求对象的起始地址必须是8字节的倍数, 对象头刚好8字节, 所以当实例数据部分没有对齐就需要补全

对象的访问定位
需要通过栈上的reference数据来操作堆上的具体对象, 具体实现根据虚拟机不同实现不同,
主流操作方式有两种使用句柄和直接指针

使用句柄, 堆中划分出一块内存作为句柄池, reference存储的就是对象的句柄地址, 句柄中包含了对象实例数据和类型数据各自具体的地址信息,

直接指针, reference中存储直接就是对象地址, 但是需要考虑类型地址, hotspot中就把类型地址放在了对象头中

两种各有优势, 句柄的好处就是reference中存储稳定的句柄地址, 对象被移动只会改变句柄中实例对象的地址, 直接指针就是速度快, 省了一次指针定位

实战: OutOfMemoryError异常
详情参考原书

Java堆溢出
不断创建对象,并保证GC Roots到对象有可达路径,
当堆溢出后除了OutOfMemoryError异常外还会进一步提示Java heap space,
先要分清楚是内存泄露还是内存溢出, 泄露是垃圾对象没有能被及时回收, 属于bug, 溢出往往是有用对象过多, 内存不够用了,

虚拟机栈和本地方法栈溢出
往往局部变量过多, 比如递归方法

方法区和运行时常量池溢出
注意一些框架对类进行增强, 比如使用cglib, 等动态生成类的操作

本机直接内存溢出
本机内存可以使用参数指定大小, 不指定默认与Java堆一样

第三章 垃圾收集器与内存分配策略

对象是否存活
判断对象是否存活

引用计数算法, 给对象加一个计数器, 有一个引用就加一, 引用失效一个就减一, 为零则可以回收了, 但是难以解决循环引用的问题, a引用b, b也引用a, 当着两个对象都不在使用时, 因为互相引用着, 所以无法被回收

可达性分析算法
通过一系列称为GC Roots的对象作为起点, 向下搜索, 走过的路径称为引用链, 当一个对象不可达时, 则证明对象不可用了,

Java语音中, 可作为GC Roots对象的包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(一般说的native方法)引用的对象

再谈引用
jdk1.2之后对引用的概念进行了扩充, 将引用分为强软弱虚四种, 引用强度依次减弱

强引用, 普遍存在的new这类, 引用还在, gc永远不会收集它
软引用, 有用但非必须的对象, 在系统将要发生内存溢出时, 会对这些对象进行回收, 如果回收后还没有足够空间,则oom, 提供SoftReference类来实现
弱引用, 被弱引用关联的对象只能生存到下次垃圾收集发生之前, 无论内存足够, gc都会收集他, WeakReference实现
虚引用, 唯一一个目的就是能在对象回收时收到一个系统通知, 使用PhantomReference实现,

生存还是死亡
不可达对象也不是非死不可, 暂时处于缓刑, 真正死亡至少要经历两次标记过程, 如果可达性发现没有与GC Roots相连, 会第一次标记, 并进行一次筛选, 筛选条件是否有必要执行该对象的finalize方法, 当对象没有覆盖方法, 或虚拟机已经调过finalize了, 都会视为没有必要执行,

如果判断有必要执行finalize方法, 会将该对象放到F-Queue的队列中, 并在稍后由虚拟机自动建立低优先级的finalize线程去执行它, 但不会等它的运行结果,原因是如果一个对象的finalize方法执行缓慢, 或者死循环了, 将会导致F-Queue的队列中其他对象永久处于等待,

如果对象在finalize()方法执行中,重新和GC Roots产生了引用链,则可以逃脱此次被回收的命运,比如将自己赋给某个变量, 这样在第二次标记时会被移出即将回收的集合,

但finalize()方法只能被虚拟机运行一次,所以并不能通过此方法逃脱下一次被回收
笔者不建议使用这个方法,建议大家完全忘掉这个方法的存在。

回收方法区
主要包括废弃常量和无用类的回收。

判断类无用:类的实例都被回收,类的ClassLoader被回收,类的Java.Lang.Class对象没有在任何地方引用。满足这三个条件,类才可以被回收(卸载)

HotSpot虚拟机通过 -Xnoclassgc 参数进行控制是否启用类卸载功能。在大量使用反射、动态代理、CGLib等框架,需要虚拟机具备类卸载功能,避免方法区发生内存溢出

垃圾回收算法
标记-清除
先标记出所有要回收的对象,在标记完成后统一进行对象的回收。有两个不足:
1 是效率问题,标记和清除的效率都不高。
2 是空间问题,会产生大量不连续的内存碎片,碎片太多会都导致大对象无法找到足够的内存,从提前触发垃圾回收。

复制算法
新生代分为一个Eden,两个Survival空间,默认比例是8:1。回收时,将Eden和一个Survival的存活对象全部放入到另一个Survival空间中,最后清理掉刚刚的Eden和Survival空间
当Survival空间不够时,由老年代进行内存分配担保

标记-整理
根据老年代对象的特点,先标记存活对象,将存活对象移动到一端,然后直接清理掉端边界以外的对象

分代收集
新生代采用复制算法,老年代采用标记-删除,或者标记-整理算法。

HotSpot算法实现
上述讨论了对象存活判断算法和垃圾收集算法, 一下是hotspot的实现

枚举根节点实现
可达性分析时会进行GC停顿,停顿所有的Java线程。Stop the world
HotSpot进行的是准确式GC,当系统停顿下来后,虚拟机有办法得知哪些地方存在着对象引用,HotSpot中使用一组称为OopMap的数据结构来达到这个目的, 类加载完后就会记录下来, 这样gc扫描时 就可以直接获取这些信息,

安全点
程序执行时不能并非在所有地方都停顿下来执行GC, 只有达到安全点才能暂停,
HotSpot没有为每个指令都生成OopMap,只在特定的位置记录这些信息,这些位置称为安全点。安全点的选定不能太少,也不能太频繁,安全点的选定以“是否让程序长时间执行”为标准
抢先式中断, 在gc发生时, 中断所有线程, 发现有线程中断地方不在安全点, 就恢复让他继续跑, 到安全点上, 几乎没有虚拟机使用这种
采用主动式中断的方式让所有线程都跑到最近的安全点上停顿下来。设置一个标志,比如是一个boolean值, 各个程序执行的时候轮询这个标志,发现中断标志为真时自己就中断挂起, 轮训标志和安全点重合的
  
安全区域
程序没有被分配CPU时间时候, 比如Sleep等, 这时候就无法响应JVM中断请求, 走到安全点去了, 安全域指在一段代码中引用关系不会发生改变, 这个区域任意地方都是安全的, 安全域解决没有分配Cpu时间的暂时不执行的程序停顿。

垃圾收集器
下图如果两个收集器之间有连线,说明可以搭配使用。没有最好的收集器,也没有万能的收集器,只有对应具体应用最合适的收集器。

所有新生代都使用复制算法,
  
Serial 收集器
新生代收集器,单线程回收, 单线程的意义不是说只会使用一个cpu一条线程, 而是收集时候回暂停其他所有工作线程, 。优点在于,简单而高效,对于运行在Client模式下的虚拟机来说是一个很好的选择(比如用户的桌面应用)
参数 -XX:UseSerialGC,打开此开关后,使用Serial+Serial Old的收集器组合进行内存回收

ParNew收集器
新生代收集器,Serial的多线程版本,除了Serial收集器之外,只有它能与CMS收集器配合工作。
-XX:+UseConcMarkSweepGC 选项后默认的新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它
ParNew收集器在单CPU的环境中,效果不如Serial好,随着CPU的增加,对于GC时系统资源的利用还是很有效的。
默认开启的收集线程数和CPU数相等,可以使用 -XX:ParallelGCThreads 指定

Parallel Scavenge收集器
吞吐量有限的收集器
新生代收集器,并行收集器,复制算法,和其他收集器不同,关注点的是吞吐量(垃圾回收时间占总时间的比例)。提供了两个参数用于控制吞吐量。
停顿时间短的适合与用户交互, 高吞吐量的适合在后台运算
-XX:MaxGCPauseMillis,最大垃圾收集停顿时间,减少GC的停顿时间是以牺牲吞吐量和新生代空间来换取的,不是设置的越小越好
-XX:GCTimeRatio,设置吞吐量大小,值是大于0小于100的范围,相当于吞吐量的倒数,比如设置成99,吞吐量就为1/(1+99)=1%。
-XX:UseAdaptiveSizePolicy ,这是一个开关参数,打开之后,就不需要设置新生代大小(-Xmn)、Eden和Survival的比例(-XX:SurvivalRatio)、 晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,收集器会自动调节这些参数。动态调整这些参数以提供最适合的停顿时间或者最大吞吐量, 这种方式称为GC自适应调节策略, 这个也是与parnew的一个重要区别

Serial Old 收集器
单线程收集器,老年代,主要意义是在Client模式下的虚拟机使用。在Server端,用于在JDK1.5以及之前版本和Parallel Scavenge配合使用,或者作为CMS的后备预案。
使用标记整理算法

Palallel Old 收集器
是Parallel Scavenge的老年代版本。在注重吞吐量的场合,都可以优先考虑Parallel Scavenge 和Palallel Old 配合使用
使用多线程和标记整理算法

CMS 收集器
Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。基于标记-清除算法实现。

分为四个步骤进行垃圾回收:初始标记,并发标记,重新标记,并发清除。只有初始标记和重新标记需要停顿用户线程。

初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记就是进行GC Roots的Tracing。

重新标记为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,远比并发标记时间短。

耗时最长的并发标记和并发清除过程中,处理器可以与用户线程一起工作。

它并不是完美的,有如下三个比较明显的缺点:
1、垃圾回收时会占用一部分线程,导致系统变慢,总吞吐量会降低。cpu越少影响越大, 当cpu少于四个影响会更大, 多cpu还好些

2、无法处理浮动垃圾,需要预留足够的内存空间给用户线程使用,可以通过 -XX:CMSInitiatingOccupancyFraction 参数控制触发垃圾回收的阈值。
如果预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败,这时将启动应急预案,启用Serial Old 进行垃圾回收,停顿时间会变长
所以-XX:CMSInitiatingOccupancyFraction 参数的值设置的太高,会导致频繁“Concurrent Mode Failure”失败,性能反而降低。

3.标记-清理,容易产生内存碎片。-XX:+UseCMSCompactAtFullColletion 开启碎片整理功能,默认开启,但是停顿时间边长了,
-XX:CMSFullGCsBeforeCompaction,控制多少次不压缩的FullGC之后来一次带压缩的,默认0.也就是说每一次就压缩

G1 收集器
包括新生代和老年代的垃圾回收。和其他收集器相比的优点:
并行和并发,更好的使用cpu, 和用户线程并发执行
分代收集,
空间整合,基于标记-整理算法,从局部两个region之间, 看是基于复制, 不管怎么看都不会出现空间碎片
可预测的停顿。能让使用者指定在一个长度为M毫秒的时间片段内, 消耗在垃圾收集上的时间不超过N毫秒,

使用g1和其他收集区别, 将整个堆分为多个大小相等的独立区域,region, 虽然保留了新生代和老年代的概念, 但不再是物理隔离的了, 它们都是一部分region的集合

g1能避免在整个堆中全部空间进行回收, g1跟踪每个region中的垃圾堆积的大小, 在后台维护一个优先列表, 根据允许的收集时间, 优先回收价值最大的region, 保证了尽可能高的收集效率,

把内存化整为零的思想实现起来很复杂, 在写入数据时候需要维护Remembered set来避免全堆扫描,

垃圾回收分为以下几个步骤:
初始标记:标记GC Roots能够直接关联到的对象,这阶段需要停顿线程,时间很短
并发标记:进行可达性分析,这阶段耗时较长,可与用户程序并发执行
最终标记:修正发生变化的记录,需要停顿线程,但是可并行执行
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来执行回收计划

理解GC日志**
见原文
垃圾收集器参数总结**
见原文

内存分配和回收策略
对象优先在Eden分配,如果启动了本地线程分配缓冲, 将按线程优先分配到TLAB上, 当新生区没有足够的内存是,虚拟机发起一次Minor GC, 使用-XX:PrintGCDetails这个收集器日志参数, 打印收集日志, Eden区和Survivor区的空间比例是8:1, gc期间发现已有的数据无法放到Survivor空间, 通过分配担保机制提前转移到老年代中去

Minor GC和Full GC
Minor GC新生代gc, 回收频繁, 速度快
Major GC/Full GC 老年代gc, 出现Major GC一般会伴随一次Minor GC, 但也并非绝对, 老比年轻慢10倍左右

大对象直接进入老年代。大对象是指需要大量连续内存空间的对象,如很长的字符串或数,虚拟机提供了参数 -XX:PretenureSizeThreshold(只对Serial,PerNew两个回收器起效),令大于这个值得对象直接在老年代分配,避免了Eden和两个Survival之间发生大量的内存复制。

长期存活的对象将进入老年代。虚拟机给每个对象定义了对象年龄计数器(Age),如果对象在Eden出生,经过第一次Minor GC后依然存活,并且能被Survival容纳的话,将被移动到Survival,对象年龄设为1。对象在Survival中每熬过一次Major GC,年龄就增加1,达到一定程度(默认是15),就会被晋升到老年代。对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreShold 指定

动态对象年龄判断。如果在Survival空间中相同年龄所有对象的大小综合超过了Survival空间的一半,年龄大于等于这个年龄的对象都会被晋升到老年代。无需等待年龄超过MaxTenuringThreShold指定的年龄

空间分配担保。只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小,
这样才能保证这次Minor gc是安全的, 否则就是有风险的, 因为如果对象过大另一个Survival区中不能保存, 则需要将这些数据放到老年代中保存,
最新的方式是只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小就进行Minor GC,否则进行Full GC, 从而确保空间的分配得到保证

逃逸分析与栈上分配
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。如果想要StringBuffer sb不逃出方法,可以写成:return sb.toString();

第四章 虚拟机性能监控与故障处理工具***
详细使用参考原书和网络文章
jps
命令用法: jps [options] [hostid]
options:命令选项,用来对输出格式进行控制
hostid:指定特定主机,可以是ip地址和域名, 也可以指定具体协议,端口。
[protocol:][[//]hostname][:port][/servername]
功能描述: jps是用于查看有权访问的hotspot虚拟机的进程. 当未指定hostid时,默认查看本机jvm进程,否者查看指定的hostid机器上的jvm进程,此时hostid所指机器必须开启jstatd服务。 jps可以列出jvm进程lvmid,主类类名,main函数参数, jvm参数,jar名称等信息。
常用参数:-lmvV
jps -m 运行时传入主类的参数;
jps -v 虚拟机参数;
jps -l 运行的主类全名 或者jar包名称;
也可以一块使用 jsp -mlv。

命令选项及功能:
没添加option的时候,默认列出VM标示符号和简单的class或jar名称.如下:

-p :仅仅显示VM 标示,不显示jar,class, main参数等信息.

-m:输出主函数传入的参数. 下的hello 就是在执行程序时从命令行输入的参数

-l: 输出应用程序主类完整package名称或jar完整名称.

-v: 列出jvm参数, -Xms20m -Xmx50m是启动程序指定的jvm参数

-V: 输出通过.hotsportrc或-XX:Flags=指定的jvm参数
-Joption:传递参数到javac 调用的java lancher.

jstat。监视JVM内存工具。
语法结构:
Usage: jstat -help|-options
jstat - [-t] [-h] [ []]
例子: jstat -gcutil 25444 1000 5
详细说明:JDK之jstat的用法
jinfo。查看和修改JVM运行参数, 配置信息
java -XX:+PrintFlagsFinal -version|grep manageable 【查看JVM中哪些参数可以被jinfo动态修改】
jinfo -flag +PrintGCDetails 105704 【修改参数 PrintGCDetails 的值】
jmap。命令用于生成heap dump文件
如果不使用这个命令,还可以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候自动生成dump文件。
jmap不仅能生成dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
详细使用:JVM调优命令-jmap
jhat
分析heap dump文件建立一个HTTP/HTML服务器, 让可以在页面上查看分析结果

jstack
Java堆栈跟踪工具, 显示虚拟机的线程快照
详细使用:使用jstack精确找到异常代码,jstack 工具使用,性能调优
注意:dead lock问题,占用cpu时间最多的线程,频繁GC
入手点总结:
wait on monitor entry: 被阻塞的,肯定有问题,等待synchronized锁
runnable : 注意IO线程,IO阻塞的线程
in Object.wait(): 注意非线程池等待,调用Object.wait()的对象

HSDIS: JIT生成代码反汇编
作用是让Hotspot的指令调用它来把动态生成的本地代码还原为汇编代码输出, 同时还生成注释, 这样就可以通过输出的代码分析问题,

JConsole: java监视与管理控制台
管理本地或远程
VisualVM: 多合一故障处理工具
拥有大量的插件, 基本可以解决全部的问题

第五章 调优案例分析与实战***
第六章 类文件结构***
第七章 虚拟机类加载机制
特点
动态加载

过程分为: 加载、验证、准备、解析, 初始化, 使用, 卸载
验证、准备、解析统称为连接

其中: 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,但解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始。这是为了支持运行时绑定,

虚拟机规范严格规定了有且只有五种情况必须立即对类进行“初始化”:
1使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,已经调用一个类的静态方法的时候。
2使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
3当初始化一个类的时候,如果发现其父类没有被初始化就会先初始化它的父类。
4当虚拟机启动的时候,用户需要指定一个要执行的主类(就是包含main()方法的那个类),虚拟机会先初始化这个类;
5使用Jdk1.7动态语言支持的时候的一些情况。
针对这5中场景, 虚拟机规范给了很强烈的限定语: 有且只有
这5中场景称为对一个类进行主动引用,

除此之外所有引用类的方式都不会触发初始化称为被动引用,下面是3个被动引用例子:
1通过子类引用父类静态字段,不会导致子类初始化
2通过new数组定义引用类(数组类型),不会触发此类的初始化
3常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用定义常量的类,因此不会触发定义常量的类的初始化。

接口与类有所不同, 但是也有初始化, 但是没有static{}块, 编译器为它生成<clinit()>类构造器, 与类有区别是上述五种的第三种, 不会要求父接口先初始化,

类加载的过程
加载。
虚拟机要完成3件事情
通过类的全限定名获取到定义此类的二进制字节流
将字节流所代表的静态存储结构转化成方法区的运行时数据结构
在方法区生成这个类的java.lang.Class对象。作为方法区这个类的各种数据的访问入口

这三点要求并不具体, 例如第一条, 没有指明从哪里获取、怎样获取,是一个非常开放的平台
加载源包括:文件(Class文件,Jar文件)、网络、计算生成(代理$Proxy)、由其它文件生成(jsp)、数据库中

加载阶段和连接阶段的部分内容是交叉进行的。用户可以通过自己写的类加载器去控制字节流的获取方式(重写类加载器的loadClass()方法),

验证。
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

从整体上看,验证阶段大致上会完成4个阶段的校验工作:
文件格式、元数据、字节码、符号引用。
可以通过设置参数略过

准备
阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。注:这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

初始值通常是数据类型的零值;对于:public static int value = 123,那么变量value在准备阶段过后的初始值为0而不是123,这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。对于:public static final int value = 123;编译时Javac将会为value生成ConstantValue(常量)属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

解析。
是连接阶段的第三步。
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用(Symbolic References): 符号引用以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在。

初始化
到了初始化的阶段,才是真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器()方法的过程,它是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
public class InitTest1 {
static {
i = 0; //给变量赋值可以正常编译
System.out.println(i); //编译器提示:“非法向前引用”
}
static int i = 1;
}
父类中定义的静态语句块要优于子类的变量赋值操作。
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产()方法。
虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。如果两个类来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。

启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。getClassLoader()方法返回null。

扩展类加载器(Extension ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader):
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而都是使用组合的关系复用父类加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

类加载器具体层次关系:启动类加载器->扩展类加载器->系统类加载器->自定义类加载器。每一个类的加载,会优先由父加载器来加载。这种方式就称为双亲委派,双亲委派保证了java基本类的不会被破坏和替代

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系。

破坏双亲委派模型
主要出现过三次较大规模的破坏

继承ClassLoader覆盖findClass方法

基础类需要调用用户代码, 比如JNDI, 启动类不可能认识用户代码, 使用了线程上下文类加载器, 做一些舞弊事情,
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口
Java中基本涉及SPI加载动作基本都是采用这种方式, 如JNDI, JDBC,

第三次由于用户对程序动态性的追求导致, 如代码热替换, 模块热部署,
OSGI实现热部署

第八章 虚拟机字节码执行引擎
运行时的栈帧结构
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

栈帧中需要多大的内存在编译期已经确定, 并写入了方法表的code属性中, 不会受到运行期影响

一个线程中方法调用可能会很长, 只有位于栈顶的栈帧才是有效的, 称为当前栈帧, 这个方法称为当前方法

局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,对于64位的数据类型(long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间,是线程安全的。

reference类型表示一个对象实例的引用, 虚拟机没有规定怎样的结构, 但是需要能更找到堆中的实例对象和方法区中的类对象, 这个值根据不同虚拟机位数大小有所不同, 还与是否开启压缩有关

第0位的slot默认指向方法所属对象引用, this, 其余根据方法的参数顺序, 或其他一些规则排列slot数据

为了节省栈帧空间,局部变量Slot可以重用,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量的Slot就可以交给其他变量使用, 可以算是一种优化。如果没有其他变量使用, 并且方法没有结束, 那么这个引用还指向着堆中数据, 那么gc是不能回收这个数据的. 这样的设计会带来一些额外的副作用,比如:在某些情况下,Slot的复用会直接影响到系统的收集行为。

操作数栈
操作数栈(Operand Stack)是一个后入先出栈。当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。所操作的对象就是具体的数值, 比如两个int相加, 那么这时在栈顶的两个元素都是int的, 出栈操作取出后相加结果在放入栈中
在一些实现中会为了优化, 从而将两个栈帧的一些数据区重合, 这样就可以避免在方法调用的过程中出现参数复制,

动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接;有关class文件常量池中的静态解析和动态链接

方法返回地址
两种方法退出这个方法, 一是遇到返回的指令, 二是产生了异常
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

方法调用
方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本(继承和多态)。

解析
调用的目标在代码写好, 编译期间就能确定下来,
“编译期可知,运行期不可变”的方法(静态方法和私有方法),在类加载的解析阶段,会将其符号引用转化为直接引用(入口地址), 前者与类关联, 后者外部不可以访问, 所以可以确定这两个不会变。这类方法的调用称为解析(Resolution)
解析调用一定是个静态过程,

分派
分派可能是静态也可能是动态, 根据分派的宗量数还能分为单分派和多分派
静态分派最典型的应用就是方法重载。静态分派发生在编译阶段, 编译器虽然能确定重载版本但是只能确定一个更加合适的版本, 比如传入’a’, 重载的方法包括char入参, int入参,long入参等多种, 所以有优先级顺序,更匹配的优先

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。最典型的应用就是方法重写。

Java语言的静态分派属于多分派类型,动态分派属于单分派类型。

单分派和多分派
方法的接受者和方法的参数统称为方法的宗量,

虚拟机的动态分派的实现
分派是非常频繁的, 基于性能的考虑, 最常用的优化手段是在类的方法区中建立虚方法表

动态类型语言支持**

基于栈的字节码解释执行引擎**
讲解虚拟机如何执行方法中的字节码指令的

第九章 类加载及执行子系统的案例与实战
第十章 早期(编译期)优化
javac编译器
分析与填充符号表,

词法分析与语法分析
词法分析是将源代码的字符流集合转变为标记(Token)集合,
语法分析是根据Token序列构造抽象语法树的过程, 抽象语法树是用来描述语法结构的树形表示

填充符号表
在经历上述分析之后, 开始填充符号表, 是一组由符号地址和符号信息构成的表格, 在程序的编译的不同阶段都会用到

注解处理器

语义分析和字节码生成
这得到语法树之后分析语法的正确性等

java语法糖的味道
泛型与类型擦除
泛型只存在于编译期, java中的泛型的实现方式为类型擦除, 是伪泛型

拆装箱与遍历循环

条件编译
就是条件为常量的if语句在编译阶段就会”运行”, 从而编译出不同的字节码,
根据boolean类型值得真假从而不根本不会成立的一段擦掉, 在正常编写的程序中是不会出现这种情况的

第十一章 晚期(运行期)优化

概述
当某些代码执行的比较频繁的时候, 就会把这些代码认为是热点代码, 为了提高热点代码的执行效率, 虚拟机会把这些代码编译成与本地平台相关的机器码, 完成这个任务的编译器称为即时编译器, 即时编译器并不在虚拟机规范中

Hotspot虚拟机内的即时编译器
解释器与编译器
大部分主流商用虚拟机都包含这两个, 各有优势
当需要迅速启动时候, 解释器和起到作用, 省去编译时间, 随后编译器起作用, 将代码编译成本地代码, 增加执行效率,
当内存小时候可以使用解释器来节省内存, 当内存大时可以使用编译器来提升性能
hotspot内置了两个即时编译器, c1和c2分别用于jvm的client和server两个模式

编译对象与触发条件
“热点代码”分2类:

  1. 被多次调用的方法
    以整个方法作为编译对象,虚拟机中标准的JIT编译方式
  2. 被多次执行的循环体
    以整个方法而不是单独循环体作为编译对象,因编译发生在方法执行过程中,故称栈上替换编译(On Stack Replacement,OSR编译)

判断一段代码是不是热点代码,是不是需要触发即时编译,的行为称热点探测(Hot Spot Detection)
2种热点探测:

  1. 基于采样的热点探测(Sample Based Hot Spot Detection)
    虚拟机周期性地检查各线程栈顶,若发现某个(或某些)方法经常出现在栈顶,则该方法是“热点方法”
    好处,实现简单、高效,可很容易地获取方法调用关系(将调用堆栈展开即可)
    缺点,很难精确地确认一个方法的热度,容易被线程阻塞或其他外界因素扰乱
  2. 基于计数器的热点探测(Counter Based Hot Spot Detection)
    虚拟机为每个方法(甚至是代码块)建立计数器,统计方法执行次数,若执行次数超过阈值则认为是“热点方法”
    优点,统计结果相对更加精确和严谨
    缺点,麻烦,需为每个方法建立并维护计数器,不能直接获取方法调用关系

HotSpot使用第二种热点探测方法

因此它为每个方法准备了两类计数器:

  1. 方法调用计数器(Invocation Counter)
    统计方法调用次数
    有确定阈值,计数器超过阈值触发JIT编译
    默认阈值Client模式1500次、Server模式10000次
    阈值可通过参数-XX:CompileThreshold设定

当方法被调用,先检查该方法是否存在被JIT编译过版本
若存在,则优先使用本地代码执行
若不存在,则将该方法调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器阈值
若超过,则向即时编译器提交该方法代码编译请求
若无特殊设置,执行引擎继续按照解释方式执行字节码,直到提交的请求被编译器编译完成
当编译完成后,该方法调用入口地址会被系统自动改写为新的,下一次调用会使用已编译版本

Client模式方法计数器触发JIT编译:

若无特殊设置,方法调用计数器统计的是相对执行频率,即一段时间内方法被调用次数
当超过一定时间限度,方法调用次数仍然未超阈值,则该方法的调用计数器会减半,该过程称方法调用计数器热度衰减(Counter Decay),这段时间称方法统计半衰周期(Counter Half Life Time)
热度衰减动作在垃圾收集时顺便进行,可用参数-XX:-UseCounterDecay关闭热度衰减,可用-XX:CounterHalfLifeTime参数设置半衰周期时长,单位是秒

  1. 回边计数器(Back Edge Counter)
    有确定阈值,计数器超过阈值触发JIT编译
    统计方法中循环体代码执行次数,字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)
    目的是为了触发OSR编译

Client模式下回边计数器阈值计算公式:
方法调用计数器阈值(-XX:CompileThreshold)× OSR比率(-XX:OnStackReplacePercentage)/ 100
-XX:OnStackReplacePercentage默认值为933
若都取默认值,计算结果(默认阈值)为13995

Server模式下回边计数器阈值计算公式:
方法调用计数器阈值(-XX:CompileThreshold)×(OSR比率(-XX:OnStackReplacePercentage)- 解释器监控比率(-XX:InterpreterProfilePercentage)/ 100
-XX:OnStackReplacePercentage默认值为140
-XX:InterpreterProfilePercentage默认值为33
若都取默认值,计算结果(默认阈值)为10700

当解释器遇到回边指令,先查找将要执行代码片段是否有已编译好版本
若有,则优先执行已编译代码
否则,回边计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值
若超过,则提交OSR编译请求,并把回边计数器的降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果

Client模式下回边计数器触发JIT编译:

回边计数器无计数热度衰减过程

哪些程序代码会被编译为本地代码?如何编译为本地代码?
JIT编译默认为后台编译,用户可通过参数-XX:-BackgroundCompilation禁止后台编译
禁止后台编译后,执行线程提交编译请求后会一直等待,直到编译过程完成,再开始执行编译器输出的本地代码

编译过程
Client Compiler:
简单快速的三段式编译
第一个阶段
一个平台独立的前端将字节码构造成高级中间代码表示(High-Level Intermediate Representaion, HIR)
HIR使用静态单分配(Static Single Assignment, SSA)形式代表代码值,这可使得一些在HIR构造过程中和之后进行的优化动作更容易实现
在构造成HIR之前,编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等
第二个阶段
一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation, LIR)
在构造成LIR之前,会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式
最后阶段
在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码
Client Compiler架构:

Server Compiler:
专门面向服务端典型应用并为服务端性能配置特别调整过的编译器
也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-02参数时的优化强度
会执行所有经典的优化动作,如
无用代码消除(Dead Code Elimination)、
循环展开(LoopcUnrolling)、
循环表达式外提(Loop Expression Hoisting)、
消除公共子表达式(Common Subexpression Elimination)、
常量传播(Constant Propagation)、
基本块重排序(Basic Block Reordering)等
还会实施一些与Java语言特性密切相关的优化技术,如
范围检查消除(Range Check Elimination)、
空值检查消除(Null Check Elimination)等
还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如
守护内联(Guarded Inlining)、
分支频率预测(Branch Frequency Prediction)等
Server Compiler的寄存器分配器是一个全局图着色分配器,它可充分利用某些处理器架构(如RISC)上的大寄存器集合
编译速度远超传统静态优化编译器,相对Client Compiler代码质量有所提高,可减少本地代码执行时间,从而抵消额外的编译时间开销

如何从外部观察即时编译器的编译过程和编译结果?
-XX:+PrintCompilation 在即时编译时,打印被编译成本地代码的方法名称
-XX:+PrintInlining 在即时编译时,输出方法内联信息
-XX:+PrintAssembly 在即时编译时,打印被编译方法的汇编代码,虚拟机需安装反汇编适配器HSDIS插件,Product版虚拟机需加入参数-XX:+UnlockDiagnosticVMOptions打开虚拟机诊断模式
-XX:+PrintOptoAssembly 用于Server VM,输出比较接近最终结果的中间代码表示,不需HSDIS插件支持
-XX:+PrintLIR 用于Client VM,输出比较接近最终结果的中间代码表示,不需HSDIS插件支持
-XX:+PrintCFGToFile 用于Client Compiler,将编译过程中各阶段数据(如,字节码、HIR生成、LIR生成、寄存器分配过程、本地代码生成等)输出到文件中
-XX:PrintIdealGraphFile 用于Server Compiler,将编译过程中各阶段数据(如,字节码、HIR生成、LIR生成、寄存器分配过程、本地代码生成等)输出到文件中
注,要输出CFG或IdealGraph文件,需Debug或FastDebug版虚拟机支持,Product版的虚拟机无法输出这些文件

编译优化技术
以编译的方式执行本地代码比解释方式更快, 因为虚拟机设计团队几乎把所有的优化措施都集中在了即时编译器之中, 369页记录了全部的优化项
例子
第一步先方法内联, 就是将调用的方法中的代码放到当前方法中, 这样就能去除很多开销, 比如建立栈帧, 同时也为之后的优化做基础, 第二步消除冗余的访问, 比如多次调用某个方法, 第三步,复写传播,去掉不需要新建立的变量, 第四版消除无用代码, 去掉一些无意义的代码

最具代表性的4项优化技术:

  1. 语言无关的经典优化技术之一:公共子表达式消除
    普遍应用于各种编译器的经典优化技术
    若一个表达式E已经计算过,且从先前计算到现在E中所有变量值都未发生变化,则E的这次出现就成为了公共子表达式
    对于公共子表达式,无必要再花时间进行计算,只需使用前面计算过的表达式结果代替E即可
    若这种优化仅限于程序基本块内,则称局部公共子表达式消除(Local Common Subexpression Elimination)
    若这种优化范围涵盖多个基本块,则称全局公共子表达式消除(Global Common Subexpression Elimination)

例:
int d=(cb)12+a+(a+bc);
公共子表达式消除优化后:
int d=E
12+a+(a+E);
代数化简优化(另外一种优化)后:
int d=E13+a2;

  1. 语言相关的经典优化技术之一:数组范围检查消除
    Java语言中,访问数组元素foo[i]时系统会自动进行上下界范围检查,即检查i必须满足i>=0&&i<foo.length,否则抛出运行时异常java.lang.ArrayIndexOutOfBoundsException
    如上,虚拟机执行子系统每次数组元素的读写都带有一次隐含条件判定操作,对于拥有大量数组访问的程序代码,这是一种性能负担
    为了安全,数组边界检查是必须做的,但数组边界检查是不是必须在运行期间一次不漏地检查是可“商量”的
    如,
    数组访问发生在循环中,且使用循环变量进行数组访问
    若编译器只需通过数据流分析即可判定循环变量取值范围永远在区间[0,foo.length)内,则可在整个循环中把数组上下界检查消除
    这可节省很多次条件判断操作
    与语言相关的其他消除操作还有,
    自动装箱消除(Autobox Elimination)
    安全点消除(Safepoint Elimination)
    消除反射(Dereflection)等

  2. 最重要的优化技术之一:方法内联
    除消除方法调用成本外,更重要的意义是为其他优化手段建立良好基础
    看起来很简单,是把目标方法的代码“复制”到发起调用的方法中,避免发生真实的方法调用

只有使用invokespecial指令调用的私有方法、实例构造器、父类方法及使用invokestatic指令调用的静态方法才是在编译期进行解析的
除上述4种方法外,其他Java方法调用都需要在运行时进行方法接收者多态选择,且都可能存在多个版本的方法接收者
Java语言中默认的实例方法是虚方法

编译器在进行内联时
若遇到非虚方法,则直接进行内联,这时的内联有稳定前提保障
若遇到虚方法,则会向CHA(Class Hierarchy Analysis,类型继承关系分析)查询该方法在当前程序下是否有多个目标版本可供选择
若查询到一个目标版本,可进行内联,但这种内联属激进优化,需预留一个“逃生门”,称守护内联(Guarded Inlining)
在程序后续执行过程中
若虚拟机未加载到会令该方法接收者的继承关系发生变化的类,则该内联优化代码可继续使用
若加载了导致该方法接收者的继承关系发生变化的新类,则需抛弃已编译代码,退回到解释状态执行,或重新编译
若查询到多个目标版本,则编译器会进行最后一次努力,使用内联缓存(Inline Cache)完成方法内联
工作原理是:
在未发生方法调用前,内联缓存状态为空
当第一次调用发生后,缓存记录下方法接收者版本信息
每次进行方法调用时都比较接收者版本
若版本一致,则该内联继续可用
若版本不一致,则说明程序真正使用了该虚方法的多态特性,这时会取消内联,查找虚方法表进行方法分派

  1. 最前沿的优化技术之一:逃逸分析
    目前较前沿的优化技术,仍不是十分成熟,但是是JIT编译器优化技术的一个重要发展方向
    不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术

基本行为是分析对象动态作用域:
当一个对象在方法中被定义后,它可能被外部方法引用,如,作为调用参数传递到其他方法中,称方法逃逸
甚至可能被外部线程访问,如,赋值给类变量或在其他线程中访问实例变量,称线程逃逸

若能证明一个对象不会逃逸出方法或线程,则可为该变量进行一些高效优化,如,

栈上分配(Stack Allocation):
Java堆中的对象对各线程共享、可见
若逃逸分析能确定一个对象不会出现方法逃逸,则可让该对象在栈上分配内存,对象所占内存空间即可随栈帧出栈而销毁,垃圾收集系统的压力将会因此减小
同步消除(Synchronization Elimination):
线程同步本身是一个相对耗时的过程
若逃逸分析能确定一个变量不会出现线程逃逸,则该对变量实施的同步措施即可消除, 及锁的消除

标量替换(Scalar Replacement):
标量(Scalar)指一个数据无法再分解成更小数据来表示,Java虚拟机中的原始数据类型可称为标量
若一个数据可继续分解,则称聚合量(Aggregate),Java中的对象最典型的聚合量
若把一个Java对象拆散,根据程序访问情况,将其使用到的成员变量恢复原始类型访问叫做标量替换
若逃逸分析能确定一个对象不会被外部访问,且该对象可被拆散,则程序可不真正创建该对象,而是创建若干该对象使用的成员变量
将对象拆散后,可让对象的成员变量在栈上分配和读写,及为进一步优化创建条件

JDK1.6 Update 23 起默认开启逃逸分析,相关参数:
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+PrintEscapeAnalysis 打印逃逸分析结果
-XX:+EliminateAllocations 开启标量替换,需先开启逃逸分析
-XX:+EliminateLocks 开启同步消除,需先开启逃逸分析
-XX:+PrintEliminateAllocations 打印标量替换结果

第十二章 Java内存模型与线程
1、硬件的效率与一致性
完成计算任务,处理器必须和内存交互才能完成,比如读取运算数据,写入计算结果等。这个I/O操作是很难消除的。计算的处理器和存储设备的运算速度有几个数量级的差距。所以现代计算机加入了一层读写速度尽可能接近处理器的高速缓存
高速缓存解决了处理器和内存的速度矛盾,却引入了新的问题:内存一致性。多处理器系统中,各个处理器都有自己的高速缓存,又同时共用内存。为了解决这一问题,在读写内存时需要遵循缓存一致性协议。
处理器会对输入的代码进行乱序执行优化,类似的,Java虚拟机也存在着指令重排序优化。

2、Java内存模型
Java内存模型规定,所有的变量(这个变量和java编程中的变量有区别,它包括了实例字段、静态字段。不包括局部变量和方法参数,因为后者是线程私有的)都存储在主内存,每条线程有自己的工作内存,工作内存中保存了该线程使用到的变量的拷贝副本,线程对变量的所有操作都必须在工作内存中进行,线程间变量值得传递需通过主内存来完成

主内存和工作内存间交互协议,8种原子操作:
lock(锁定主内存中的一个变量)
unlock(解锁主内存一个变量)
read(读取主内存传输到工作内存,为load准备)
load(载入主内存至工作内存)
use(执行引擎使用工作内存中变量, 每当遇到一个指令时就执行此操作)
assign(接受执行引擎计算后的值赋值给工作内存)
store(存储工作内存至主内存,为write准备)
write(把工作内存写入主内存)

read和load, store和write必须安顺序执行, 但不保证连续执行

关于这八种操作有些限制,即线程的变量必须同步回主存, 不予许无原因的同步到主存, 对一个对象lock和unlock的次数相同,线程相同, unlock之前必须把值同步到主存

volatile是java虚拟机提供的轻量级的同步机制,对于volatile变量的特殊规则:
保证了变量对所有线程的可见性,当一个线程修改了这个变量的值,修改后的值对其他线程来说是立即可见的。普通变量,需要通过把新值会写到主内存,其他线程从主内存读取之后才可以看到最新值
禁止指令重排序优化。
无法保证符合操作的原子性,比如i++
通过内存屏障实现的可见性和禁止重排序。不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这些差异,由JVM来为不同的平台生成相应的机器码来完成。X86 处理器只会对写-读进行指令重排序,写volatile变量时,会加lock总线锁,将cpu缓存写入主存,其他cpu的读都会被阻塞,然后其他核的缓存某些对应数据会被标记为失效,那么其他核下次读的时候先读缓存发现失效了,然后去主存读

关于long和double类型变量的特殊规则:允许虚拟机将没有被volatile变量修饰的64位数据的读写操作划分为两次32位的操作来进行。这点就是long和double的非原子性协定

先行发生原则
指两个操作直接的顺序性, a操作必须发生在b操作之前, 因为a操作会影响b操作的结果
比如线程的start方法先行于后面线程的所有动作, 对象初始化先行于他的终结方法, 这些动作为先行发生原则, 不用控制线程之间的有序性,就可以有序的执行

3、Java与线程
Java虚拟机实现线程,有三种方式:
(1)通过内核线程实现。jvm中的一个线程对应一个轻量级进程,一个轻量级进程对应一个内核线程。CPU通过调度器对线程进行调度。缺点:
由于基于内核线程实现,各种线程操作需要系统调用,系统调用代价较高,需要在用户态和内核态之间来回切换
每个线程都需要一个内核线程的支持,因此轻量级进程会消耗内核资源,一个系统支持的轻量级进程是有限的

(2)使用用户线程实现。不需要切换回内核态,也可以支持规模更大的线程数量。部分高性能数据库的多线程就是使用用户线程实现的。缺点是没有系统内核的支援,所有问题需要自己考虑,程序实现比较复杂
(3)内核线程和用户线程结合

(4)JVM,对于Sun JDK来说,在Windows和LInux系统下,都是使用的一对一的线程模型实现的。一条线程就映射到一条轻量级进程中,

Java线程调度
协同式线程调度。线程的执行时间由自己控制,线程执行完毕,会主动通知系统, 如果一直占用则会导致其他线程饿死, 所以这种方式风险很大
java使用的是抢占式调度。每个线程有系统分配执行时间,线程的切换也有系统来决定,线程的执行时间是可控的。线程可以设置优先级,来争取更多的执行时间。Java一共设置了10个优先级,操作系统的优先级数量可能和java定义的不一致,另外操作系统还可以更改线程的优先级,所以Java中优先级高的线程并不一定被优先执行。

Java线程状态转换

第十三章 线程安全与锁优化
高效并发是从jdk1.5 到jdk1.6的一个重要改进,HotSpot虚拟机开发团队耗费了大量的精力去实现锁优化技术

Java中操作的共享变量分为五类
不可变: final
绝对线程安全: 调用者不做任何额外操作就能保证线程绝对的安全
相对线程安全: 通常意义上讲的线程安全, 多线程环境下需要同步操作
线程兼容: 本身不是安全的, 但是通过同步可以安全
线程对立: 不管怎样都不能保证安全

线程安全的实现方式
互斥同步
一个对象只能被一个线程使用, synchronized在1.6之后优化了, 首选syn关键字

非阻塞同步
互斥同步属于悲观锁, 这个属于乐观锁

自旋锁与自适应自旋。同步互斥对性能最大的影响就是线程挂起、恢复需要从用户态切换到内核态,切换的过程会造成系统消耗。往往锁定的代码段执行时间非常短,为了这个短的时间去挂起和恢复是不值得的。所以提出了自旋锁的概念,当线程申请获取一个其他线程占用的锁时,这个线程不会立即挂起,而是通过一定次数的循环自旋,这个过程不会释放cpu的控制权,自适应自旋就是根据上一次自旋的结果来决定这一次自旋的次数

锁消除。虚拟机即时编译器在运行时会把检测到不可能发生共享数据竞争的锁消除
从而就不需要加锁解锁的操作

锁粗化。一系列的操作都是对同一个对象的加锁和解锁,虚拟机检测到这种情况会将锁的范围扩大(粗化)

轻量级锁:
偏向锁。如果程序中大多数的锁总是被多个线程访问,那偏向锁模式就是多余的。可以使用参数 -XX:-UseBiasedLocking来禁止偏向锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值