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

2016-08-25
读书笔记
  • 64位JVM
    • 由于指针膨胀和各种数据类型对齐补白的原因,运行于64位JVM上的Java应用需消耗更多内存,通常比32位JVM多10%~30%。
    • 64位JVM的运行速度与32位相比有15%左右的性能差距。

Java内存区域与内存溢出异常

运行时数据区域

  • 程序计数器、堆、虚拟机栈、本地方法栈、方法区。
程序计数器(线程私有)
  • 当前线程所执行的字节码的行号指示器。
  • JVM 的多线程是通过线程轮流切换并分配CPU时间来实现。为了线程切换后还能恢复到正确的执行位置,每条线程都需有自己的 PC。
  • 执行 Java 方法时,PC 记录的是正在执行的字节码指令地址。若执行 Native 方法,PC 为空(Undefined)。
Java虚拟机栈(线程私有)
  • 每个方法执行时创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口。
  • 局部变量表
    • 编译期可知的基本数据类型:boolean、bypte、char、short、int、float、long、double
    • 对象引用:reference类型
    • returnAddress类型:指向一条字节码指令地址。
  • 局部变量表所需内存在编译期完成分配,当进入一个方法时,此方法需在帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。
  • 异常
    • StackOverflowError:线程请求的栈深度大于 JVM 允许的深度。
    • OutOfMemoryError:扩展虚拟机栈时无法申请到足够的内存。
本地方法栈
  • 与虚拟机栈类似,会产生StackOverflowError、OutOfMemoryError
Java堆(线程共享)
  • 在 JVM 启动时创建堆,用于存放对象的实例。
  • 堆可分为新生代、老年代;也可分为Eden、Survivor等。
  • Java 堆可处于物理上不连续的内存空间,只要逻辑上连续即可。
  • 堆中没有内存完成实例分配且无法再扩展时,OutOfMemoryError
方法区(线程共享)
  • 存放已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码。
  • 方法区也称“永久代”。此区域的GC主要是针对常量池的回收和对类型的缷载。
  • 方法区无法满足内存分配需求,OutOfMemoryError
  • 运行时常量池(方法区的一部分)
    • Class 文件中包括类的版本、字段、方法、接口、常量池。常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
    • 运行期间也可能将新的常量放入池中,如 String 的 intern() 方法。
    • 常量池无法再申请到内存时 OutOfMemoryError
直接内存
  • 直接内存不是 JVM 运行时数据区的一部分。
  • NIO 引入了一种基于通道(Channel)与缓冲区(Buffer)的 IO 方式,可使用 Native 函数库直接分配堆外内存。然后通过堆中的一个 DirectByteBuffer 对象对这块内存进行操作。避免在 Java 堆和 Native 堆间来回复制数据。

对象访问

  • Object obj = new Object();
    • Object obj:Java 栈的本地变量表,reference类型
      • reference中直接存储堆中的对象地址。
    • new Object():Java 堆中
      • 堆中包含对象类型数据的地址信息,即包含方法区中的地址信息
    • 对象类型数据(对象类型、父类、实现的接口、方法):方法区中
    • 方法区存放对象类型数据,堆中存放对象实例数据。
  • Java 使用直接指针访问方式来实现对象访问。

实战:OutOfMemoryError异常

Java堆溢出
  • List list = new ArrayList<>();
  • while(true) list.add(new Object());
  • 分析具体是内存泄漏还是内存溢出
    • 内存泄漏:通过工具查看泄漏对象到 GC Roots 的引用链
虚拟机栈、本地方法栈溢出
  • -Xss设置栈大小。
运行时常量池溢出
  • -XX:PermSize、-XX:MaxPermSize限制方法区大小。
  • 使用String的intern()方法使运行时常量池溢出。
方法区溢出
  • 存放 Class 相关信息:类名、访问修饰符、常量池、字段描述、方法描述。
  • 运行时产生大量的类,可能会填满方法区。
  • CGLIB、大量JSP或动态产生JSP文件的应用、基于OSGi的应用均可能使方法区溢出。
本地直接内存溢出
  • DirectMemory可通过 -XX:MaxDirectMemorySize 指定,若不指定,默认与 Java 堆的最大值(-Xmx)一致。

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

  • 需要排查各种内存溢出、内存泄漏问题;当gc成为系统达到更高并发量的瓶颈时。需了解GC和内存分配。
  • 程序计数器、虚拟机栈、本地方法栈,随线程而生,随线程而灭。方法结束或线程结束,这部分内存跟随着回收了。每个栈帧分配多少内存基本上是在类结构确定下来时就已知。
  • Java 堆、方法区,这部分内存的分配和回收都是动态的,平时所说的GC是指 Java 堆和方法区的回收。在程序运行期间才能知道会创建哪些对象。

对象已死?

引用计数算法
  • 给对象添加一个引用计数器,每多一次引用,计数器加1,引用失效计数器减1。
  • Java 并没有选用引用计数算法来管理内存,因为它很难解决对象间的相互循环引用的问题。
根搜索算法
  • Java 使用根搜索算法判定对象是否存活。
  • GC Roots对象作为起点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连,则此对象不可用。
  • GC Roots
    • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
    • 方法区中的类静态属性引用的对象。
    • 方法区中的常量引用的对象。
    • 本地方法栈中 JNI(即一般说的Native方法)的引用的对象。
再谈引用
  • 引用强度从强到弱:强引用、软引用、弱引用、虚引用。
    • 强引用
      • Object obj = new Object();
      • 只要强引用存在,GC 永远不会回收被引用对象。
    • 软引用
      • 描述还有用,但非必需的对象。
      • 将发生内存溢出前,将这些对象列入回收范围中并进行2次回收。若还是内存不足,才抛出内存溢出。
    • 弱引用
      • 描述非必需对象。
      • 所关联对象仅生存到下一次 GC 发生前。
    • 虚引用
      • 为一个对象设置虚引用关联的唯一目的是希望在回收此对象时收到一个系统通知。
生存还是死亡?
  • 一个对象的finalize()方法最多只会被系统调用一次。
  • 根搜索算法不可达的对象,并非“非死不可”,宣告对象死亡,需经历2次标记
    • 根搜索后发现对象未与 GC Roots 相连接,将第1次标记。之后筛选是否需要执行对象的 finalize() 方法。
      • 当对象未覆盖 finalize() 方法或 finalize() 方法已被 JVM 调用过,则无需再 finalize() 。
      • 当对象需执行 finalize() 方法时,可在 finalize() 方法中恢复对象的引用以逃脱回收。GC 对 F-Queue中的对象进行第2次小规模的标记。若finalize()中未恢复对象引用,则标记为可回收;若恢复引用,则将对象移出“即将回收”集合。
  • finalize() 方法能做的所有工作,使用try-finally或其他方式都可以做得更好,可以忘掉Java有 finalize() 方法。
回收方法区(永久代)
  • 永久代主要回收:废弃常量、无用的类。
    • 无用的类
      • 该类所有实例均已被回收,即堆中不存在该类的任何实例。
      • 该类的ClassLoader已被回收。
      • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 大量使用反射、动态代理、CGLib的场景,以及动态生成 JSP 和 OSGi这类频繁自定义 ClassLoader 的场景均需 JVM 提供类缷载功能,以保证永久代不会溢出。

垃圾收集算法(分代回收)

标记-清除算法
  • 标记出需回收对象,标记完成后统一回收。
  • 缺点:
    • 效率:标记、清除过程效率都不高
    • 内存碎片:标记清除后产生大量不连续的内存碎片,碎片太多会导致无法分配较大对象而提前触发另一次GC。
复制算法(新生代)
  • 将内存分为相等的2块,每次只使用1块。1块内存用完,将块中存活对象复制到另一块,并清空原内存块。
  • 商用实现:将内存分为一块较大的Eden空间和2块较小的Survivor空间,每次使用Eden和其中的一块Survivor。
    • Eden:Survivor:Survivor=8:1:1
    • 若另外一块Survivor空间不足以存放存活对象,这些对象将直接进入老年代。
标记-整理算法(老年代)
  • 标记出可回收对象,将存活对象都向一端移动,清理存活对象边界收外的内存。

垃圾收集器

  • 可通过参数配置各年代所使用的垃圾收集器。
  • 没有最好的收集器,需根据具体应用选择合适的收集器。
  • 垃圾收集器
    • 新生代:Serial、ParNew、Parallel Scavenge
    • 老年代:CMS、Serial Old、Parallel Old
    • 不分代:G1
新生代 老年代
Serial CMS、Serial Old
ParNew CMS、Serial Old
Parallel Scavenge Serial Old、Parallel Old
G1 G1
Serial 收集器(新生代,单线程)
  • JVM 在 Client 模式下的默认新生代收集器。
  • 单线程收集器
  • 暂停其他所有工作线程
  • 新生代:复制算法,老年代:标记-整理算法
ParNew 收集器(新生代,多线程)
  • 多线程收集,其他与Serial完全一样。
  • 老年代若使用CMS,则新生代默认为ParNew。
  • 默认开启线程数与cpu数相同,可使用-XX:ParallelGCThreads限制垃圾收集的线程数。
Parallel Scavenge 收集器(新生代,多线程)
  • 与ParNew区别
    • Parallel Scavenge 目标是达到一个可控制的吞吐量
    • 可开启自适应调节开关
  • 最大垃圾收集停顿时间 -XXMaxGCPauseMillis
  • 吞吐量大小 -XX:GCTimeRatio
  • 自适应调节开关 -XX:+UseAdaptiveSizePolicy
    • 无需指定新生代大小(-Xmm),Eden与Survivor比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)
Serial Old 收集器(老年代,单线程)
  • 与Serial一样,只是作用于老年代。
Parallel Old 收集器(老年代,多线程)
  • Parallel Old是 Parallel 的老年代版本。
  • 注重吞吐量及CPU资源敏感的场合,可以使用 Parallel Scavenge + Parallel Old 。
CMS 收集器(老年代,多线程)
  • 目标:使回收停顿时间最短。适用于互联网站等重视服务响应速度的场景。
  • 回收过程
    • 初始标记
      • 标记 GC Roots 直接关联的对象
      • stop the world
    • 并发标记
      • GC Roots Tracing
      • 与用户线程一起并发执行
    • 重新标记
      • 修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
      • stop the world
    • 并发清除
      • 与用户线程一起并发执行
  • 缺点
    • 消耗cpu资源
    • 无法处理浮动垃圾,即GC时用户线程还在运行,新产生的垃圾。
    • 空间碎片:使用的是 标记-清除 算法。
G1 收集器
  • 使用 标记-整理 算法,不会产生空间碎片。
  • 可精确控制停顿:可让使用者时确指定在M毫秒时间段内,消耗在GC上的时间不超过N毫秒。
  • G1可实现在不牺牲吞吐量的前提下完成低停顿的GC,因为G1能极力避免全区域的GC
  • G1将整个堆(包括新生代、老年代)划分为多个大小固定的独立区域,并跟踪各区的垃圾堆积程序。同时在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域。
  • 区域划分及有优先级的区域回收,保证了G1在有限的时间内可以获得最高的收集效率。
垃圾收集器参数总结
参数 描述
UseSerialGC JVM 在Client模式下的默认值,Serial + Serial Old
UseParNewGC ParNew + Serial Old
UseConcMarkSweepGC ParNew + CMS + Serial Old。CMS出现Concurrent Mode Failure后使用Serial Old
UseParallelGC JVM 在Server模式下的默认值,Parallel Scavenge + Serial Old
UseParallelOldGC Parallel Scavenge + parallel Old
SurvivorRatio 新生代中Eden与Survivor容量比,默认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在老年代空间被使用多少后触发GC,默认68%,仅 CMS 时有效
UseCMSCompactAtFullCollection CMS完成GC后是否要进行一次内存碎片整理,仅 CMS 时有效
CMSFullGCsBeforeCompaction CMS在进行若干次GC后再启动一次内存碎片整理,仅 CMS 时有效

内存分配与回收策略

对象优先在Eden分配
  • 当Eden区空间不足,发起一次 Minor GC
  • Minor GC(新生代GC):发生在新生代的GC
  • Major GC / Full GC(老年代GC):发生在老年代的GC,一般比 Minor GC 慢10倍。
大对象直接进入老年代
长期存活的对象将进入老年代
  • JVM 给每个对象定义了一个对象年龄计数器。对象每经过一次 Minor GC ,年龄加1,年龄达到一定程度(默认15),晋升老年代。
动态对象年龄判定
  • 并不是对象年龄必须达到MaxTenuringThreshold才晋升老年代。若 Survivor 空间中相同年龄所有对象大小的部和大于 Survivor 的一半,年龄大于或等于该年龄的对象就可直接进入老年代。
空间分配担保

虚拟机性能监控与故障处理工具

JDK的命令行工具

  • JAVA_HOME/bin目录下有很多命令行工具,这些工具大多为jdk/lib/tools.jar类库的一层薄包装,体积非常小。
  • 命令
    • jps:显示指定系统内所有的 HotSpot 虚拟机进程
    • jstat:收集 HotSpot 虚拟机各方面运行数据
    • jinfo:虚拟机配置信息
    • jmap:生成内存转储快照,即heapdump文件
    • jhat:分析heapdump文件,它会建立一个 HTTP/HTML服务器,让用户可在浏览器上查看分析结果
    • jstack:虚拟机线程快照
jps:虚拟机进程状况工具
  • 输出信息
    • 正在运行的虚拟机进程
    • 虚拟机执行主类的名称(main()函数所在类)
    • 进程的本地虚拟机的唯一ID
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具

JDK的可视化工具

Jconsole:Java监视与管理控制台
VisualVM:多合一故障处理工具

调优案例分析与实战

案例分析

高性能硬件上的程序部署策略
集群间同步导致的内存溢出
堆外内存导致的溢出错误
外部命令导致系统缓慢
服务器JVM进程崩溃

实战:Eclipse运行速度调优

调优前的程序运行状态
升级JDK1.6的性能变化及兼容问题
编译时间和类加载时间的优化
调整内存设置控制垃圾收集频率
选择收集器降低延迟

类文件结构

  • 字节码命令所能提供的语义描述能力比Java语言本身更强大。因此Java无法有效支持的语言特性并不代表字节码本身无法支持。

Class类文件的结构

  • Class文件是一组以 8bit 为基础单位的二进制流,各数据项目按序紧凑排列在Class文件中,中间无任何分隔符。
  • Class文件格式中只存在2种数据类型:无符号数、表。
    • 无符号数
      • u1、u2、u4、u8表示1、2、4、8字节的无符号数。
      • 基本数据类型,用来描述数字、索引引用、数量值或按UTF-8编码构成字符串值。
      • 复合数据类型,由无符号数或其他表构成。
      • 整个Class文件本质上就是一张表。
  • 当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。
魔数与Class文件的版本
常量池
访问标志
类索引、父类索引与接口索引集合
字段表集合
方法表集合
属性表集合

Class文件结构的发展


虚拟机类加载机制

  • 类加载机制:JVM 将 Class 文件加载到内存,对数据进行校验、转换解析初始化,形成可被 JVM 直接使用的 Java 类型。
  • 与在编译期进行连接工作的语言不同,Java 类型的加载和连接过程都是在程序运行期完成。如编写一个接口,可以等到运行时再指定其实际的实现。

类加载的时机

  • 类生命周期:加载验证准备解析初始化使用缷载。验证、准备、解析统称为连接过程。
  • 初始化仅4种触发方式
    • 遇到new、getstatic、putstatic、invokestatic字节码指令,若类没被初始化,则触发初始化。
      • 使用 new 实例化对象
      • 读取或设置类的静态字段(被final修饰、在编译期把结果放入常量池的静态字段除外)
      • 调用类的静态方法。
    • 使用java.lang.relect对类进行反射时,若类未初始化则先初始化。
    • 初始化类时,若其父类未初始化,则先初始化父类。
    • jvm启动,用户需指定主类(包含 main() 方法),虚拟机会先初始化这个主类。
  • 子类 SubClass 使用父类 SuperClass 中的 static 字段,当执行 SubClass.staticField 时,仅触发父类的初始化。
  • 类中的 static 块,在初始化时执行。

类加载的过程

加载
  • 加载过程
    • 通过类的全限定名获取定义此类的二进制字节流。
      • 从zip包中读取,如jar、ear、war格式
      • 从网络中获取
      • 运行时生成,如java.lang.relect.Proxy
      • 由其他文件生成,如 jsp 应用
    • 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
    • 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。
  • 加载阶段既可使用系统类加载器,也可使用自定义类加载器,开发人员可自定义类加载器来控制字节流的获取方式。
验证
  • 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
  • Java本身是相对安全的语言,但 Class 并不一定是由Java编译而来。若 Class 文件的语义中 访问数组边界外的数据、将一个对象转为其未实现的类型、跳转到并不存在的代码行,则不安全。需在验证阶段验证不会发生这些情况。
  • 验证过程
    • 文件格式验证
      • 保证字节流能正确解析并存储于方法区,格式上符合描述一个 Java 类型的要求。经过此阶段验证后,字节流才会进入内存的方法区中进行存储。后面3个验证阶段均基于方法区的存储结构进行。
      • 验证魔数是否正确、主次版本号能否被当前虚拟机处理、常量池中的常量是否有不被支持的常量类型。。。
    • 元数据验证:保证符合 Java 语言规则
      • 是否存在父类(除 Object,所有类都应有父类)
      • 父类是否继承了不允许被继承的类(被 final 修饰)
      • 若非抽象类,是否实现父类或接口的所有方法
      • 类中字段与方法是否与父类产生冲突(如覆盖父类 final 字段,方法参数相同返回值不同等不符合规则的方法重载)
    • 字节码验证:数据流与控制流分析,对类的方法体进行校验,保证方法在运行时不会危害虚拟机安全。
      • 操作数栈的数据类型与指令代码序列能配合工作:如操作数栈中为 int ,使用时却按 long 来加载入本地变量表中。
      • 保证跳转指令不会跳转到方法体以外的字节码指令上。
      • 保证方法体中的类型转换有效:如可把子类赋值给父类数据类型,但不能把父类对象赋值给子类数据类型。
    • 符号引用验证
      • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 验证阶段对 JVM 的类加载机制来说,很重要但非必需。若代码已反复使用,在实施阶段可使用 -Xverity:none 参数来关闭大部分类验证措施,缩短虚拟机类加载的时间。
准备
  • 类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。这里的“初始值”通常指数据类型的0值。
    • public static int value = 123;
      • 准备阶段将 value 赋值为0,初始化阶段 value 才赋值为123。
    • public static final int value = 123;
      • 准备阶段将 value 赋值123。
  • 实例变量将在对象实例化时随着对象一起分配在 Java 堆中。
解析:常量池中的符号引用替换为直接引用
  • 符号引用
    • 以一组符号来描述引用目标,引用的目标并不一定已经加载到内存中。
  • 直接引用
    • 可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,同一符号引用在不同的虚拟机实例上翻译出的直接引用一般会不同。
    • 若有直接引用,那引用的目标必定已在内存中存在。
  • 解析过程
    • 类或接口的解析
    • 字段解析
    • 类方法解析
    • 接口方法解析
初始化
  • 初始化阶段才开始执行类中定义的 Java 代码,是执行类构造器<clinit>()方法的过程。
  • ()是编译器自动收集 static 变量赋值操作及 static 块中的语句合并产生的。
  • 与类构造函数(或者说实例构造器()方法)不同,()不需要显式地调用父类构造器,虚拟机会保证子类的()方法执行前,父类的()已执行完毕。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步。
使用与缷载

类加载器

  • 通过类的全限定名来获取描述此类的二进制字节流。实现类的加载过程。
类与类加载器
  • 对任意一个类,均需由加载它的类加载器和类本身一同确定其在 Java 虚拟机中的唯一性。两个类来源于同一个Class,但类加载器不同,那这两个类也不相等。
双亲委派模型
  • 类加载器类型
    • 启动类加载器 Bootstrap ClassLoader
      • 加载/lib中且是虚拟机识别的类库到虚拟机内存中。(名字不符合的类库即使放在lib中也不会被加载)
      • 无法被Java程序直接引用。
    • 扩展类加载器 Extension ClassLoader
      • 加载/lib/ext中的类库
      • 开发者可直接使用扩展类加载器
    • 应用程序类加载器 Application ClassLoader
      • 加载用户类路径上所指定的类库
      • 若应用程序中没有自定义类加载器,则 Application ClassLoader 为默认类加载器。
  • 类加载器间的父子关系一般不会以继承(Inheritance)的关系来关现,而是通过组合(Composition)来复用父加载器的代码。
  • 如果一个类加载器收到加载请求,它首先委派给父类加载器去加载,父类加载器无法加载时才自己加载。
破坏双亲委派模型

虚拟机字节码执行引擎

运行时栈帧结构

  • 栈帧是用于支持 JVM 方法调用和方法执行的数据结构,是 JVM 运行时数据区中的虚拟机栈的栈元素。
  • 栈帧包括局部变量表操作数栈动态连接方法返回地址附加信息
  • 编译代码时,栈帧需要多大的局部变量表、多深的操作数栈都已确定,并写入到方法表的 Code 属性中。一个栈帧需分配多少内存,不会受到程序运行期变量数据的影响,而仅取决于具体的 JVM 实现。
  • 一个线程的方法调用链可以很长,多方法同时执行。活动线程中,只有栈顶的栈帧是有效的,称当前栈帧,此栈帧所关联方法称当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行。
局部变量表
  • 存放方法参数方法内的局部变量
  • Java 编译为 Class 时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需分配的最大局部变量表的容量。
  • 局部变量表的最小容量以变量槽(Variable Slot)为最小单位,每个 Slot 应能存放1个boolean、byte、char、short、int、float、refrence、returnAddress类型的数据。
    • refrence:指对象的引用,JVM 可从 refrence 中直接或间接查询到对象在堆中的起始地址索引和方法区中的对象类型数据
    • returnAddress:指向一条字节码指令地址。
  • 局部变量表作为GC Roots的一部分。若局部变量所占用的 Slot 还没被其他变量所复用,则局部变量表还保持着对此 Slot 的关联。此处内存区域不会被回收。
    • byte[] placeholder = new byte[64 1024 1024];System.gc();
    • 不会被回收
  • 局部变量与类变量不同,类变量在准备阶段赋初始值、初始化阶段赋程序定义的初始值。局部变量定义后没赋初始值则无法使用。不要认为 Java 中任何情况下 int 默认0、boolean 默认false。
操作数栈
  • 操作数栈的深度在编译时被写入到 Code 属性的 max_stacks 数据项中。
  • 方法开始执行时,此方法的操作数栈为空,方法执行过程中会有各种字节码指令向操作数栈中写入和提取内容。
动态连接
  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有此引用是为了支持方法调用过程中的动态连接。
  • Class 文件的常量池中存在大量符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
    • 静态解析:这些符号引用一部分在类加载阶段或第一次使用时转为直接引用。
    • 动态连接:另一部分在每一次的运行期间转为直接引用。
方法返回地址
  • 方法退出
    • 正常完成出口
      • 执行引擎遇到任意一个方法返回的字节码指令,此时可能会有返回值传递给上层的方法调用者。是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定。
    • 异常完成出口
      • 方法执行过程中遇异常,且此异常未在方法体内得到处理。
      • 一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
  • 方法退出后,都需返回到方法被调用的位置,程序才能继续执行,方法返回时可能需在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。
  • 方法正常退出时,调用者的PC计数器的值可作为返回地址,栈帧中很可能会保存这个计数器值。异常退出时,返回地址要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。
  • 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可执行的操作:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
附加信息

方法调用

  • 与方法执行不同,方法调用的唯一任务是确定被调用方法的版本(即调用哪个方法),不涉及方法内部的具体执行过程
解析
  • 所有方法调用中的目标方法在 Class 文件里面都是一个常量池中的符号引用。在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析的前提:方法在程序运行前就有一个可确定的调用版本,即调用目标在代码写好、编译器进行编译时就必须确定下来。
  • 这类方法包括静态方法私有方法
分派
  • 分派调用过程揭示多态性特征的一些基本体现(如重载、重写)在Java中是如何实现的。
  • 静态分派

    • Human man = new Man();
    • Human为变量的静态类型或外观类型,Man为变量的实际类型
    • 虚拟机重载时是通过参数的静态类型而不是实际类型作为判定依据的。
    • 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派典型应用为方法重载。
    • char可转型为int,但Character不能转型为Integer。
  • 动态分派

    • 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
    • 动态分派的典型应用为方法覆盖
  • 单分派与多分派

    • 方法的接收者与方法的参数统称为方法的宗量。分派基于多少种宗量,将分派划分为单分派、多分派。单分派根据一个宗量对目标方法进行选择,多分派根据多个宗量对目标方法进行选择。
    • JDK1.6是一门静态多分派、动态单分派的语言。
  • 虚拟机动态分派的实现

基于栈的字节码解释执行引擎

  • JVM 如何执行方法里的字节码指令。JVM 的执行引擎在执行Java代码时有解释执行(通过解释器)和编译执行(通过即时编译器产生本地代码)。
解释执行
  • 从程序代码到 JVM 可执行的指令集

    程序源码-->词法分析-->单词流-------->语法分析
                                          ↓
    解释执行<--解释器<--指令流(可选)<--抽象语法树
                                          ↓
    目标代码<--生成器<--中间代码(可选)<--优化器(可选)
    
    • 中间那条分支为解释器执行过程。
    • Javac编译器:程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流。这一部分是在 JVM 之外进行的。
    • 解释器是在 JVM 内部。
基于栈的指令集与基于寄存器的指令集
  • 基于栈的指令集:Java编译器输出的指令流,其中的指令大部分都是0地址指令,它们依赖操作数栈进行工作。
    • 优势:可移植性、代码相对更紧凑、编译器实现更简单
    • 缺点:执行速度相对较慢。
  • 基于寄存器的指令集:PC中直接支持的指令集加构,这些指令依赖寄存器工作。
基于栈的解释器执行过程

类加载及执行子系统的案例与实战

案例分析

Tomcat:正统的类加载器架构
OSGi:灵活的类加载器架构
字节码生成技术与动态代理的实现
Retrotranslator:跨越JDK版本

实战:自己动手实现远程执行功能

目标
思路
实现
验证

早期(编译期)优化

Javac 编译器

Javac 的源码与调试
解析与填充符号表
注解处理器
语义分析与字节码生成

Java 语法糖的味道

泛型与类型擦除
自动装箱、拆箱与遍历循环
条件编译

实战:插入式注解处理器

实战目标
代码实现
运行与测试
其他应用案例

晚期(运行期)优化

HotSpot 虚拟机内的即时编译器

解释器与编译器
编译对象与解发条件
编译过程
查看与分析即时编译结果

编译优化技术

优化技术概览
公共子表达式消除
数组边界检查消除
方法内联
逃逸分析

Java与C/C++的编译器对比


Java内存模型与线程

  • TPS(每秒事务处理数):1秒内服务端平均能响应的请求总数。
  • 每个cpu有自己的缓存,同时各cpu共享同一主内存。
  • cpu会对输入代码进行乱序执行优化,在计算之后将乱序执行的结果重组,保证与顺序执行的结果一致。

Java内存模型

主内存与工作内存
  • 所有变量均存储在主内存,每条线程又有各自的工作内存。
  • 线程需使用某变量时,不可直接读写主内存,需从主内存获取副本拷贝到自己的工作内存,之后操作自己的工作内存。
  • 不同线程无法直接访问对方工作内存中的变量,线程间变量值的传递需通过主内存。
  • 主内存主要对应Java堆中对象的实例数据部分;工作内存对应虚拟机栈中的部分区域。
内存间交互操作
  • 主内存与工作内存间的操作(一般为原子操作,64位非volatile数据除外):
    • lock 锁定
      • 作用于主内存变量
      • 将一个变量标识为线程独占状态
    • unlock 解锁
      • 作用于主内存变量
      • 将锁定状态的变量释放
    • read 读取
      • 作用于主内存变量
      • 将主内存变量传输到工作内存供load
    • load 载入
      • 作用于工作内存变量
      • read获取的变量放入工作内存的变量副本
    • use 使用
      • 作用于工作内存变量
      • 将工作内存变量传给执行引擎
    • assign 赋值
      • 作用于工作内存变量
      • 将执行引擎接收到的新值赋值给工作内存变量
    • store 存储
      • 作用于工作内存变量
      • 工作内存变量传送到主内存供write
    • write 写入
      • 作用于主内存变量
      • store从工作内存获取的变量放入主内存变量中
  • read与load、store与write成对出现且有序。
  • 多次lock后,只有执行相同次数的unlock,变量才被解锁。
  • 对一个变量lock,将清空工作内存中此变量的值。若需使用此变量,需重新load或assign。
  • 不充许unlock被其他线程锁定的变量。
  • unlock前必须把变量同步回主内存(store和write)。
对于 volatile 型变量的特殊规则
  • volatile特性
    • 可见性:一条线程修改了变量值,新值对其他线程是可立即得知的。
      • 各线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用volatile变量均需先刷新,执行引擎看不到不一致的情况。
      • Java里的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
    • 禁止指令重排序优化。
  • volatile读操作的性能消耗与普通弯量几乎没有差别,但写操作会慢一些,因为需在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
对于 long 和 double 型变量的特殊规则
  • 允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位的操作来进行。即对64位数据的load、store、read、write可以不是原子的。
  • Java内存模型允许long、double的读写非原子,但强烈建议虚拟机将long、double的读写实现为原子操作。目前各商用JVM均为原子,编码时不需将long、double专门声明为volatile。
原子性、可见性与有序性
  • 原子性
    • 操作原子性read、load、assign、use、store、write
      • 基本数据类型的读写是原子的(long、double这种除外)
    • 范围原子性lock、unlock
      • lock、unlock对应的更高层次字节码指令是monitorenter、monitorexit,反映到Java代码即synchronized,因此synchronized块之间的操作也是原子的。
  • 可见性
    • volatile
      • 变通变量与volatile区别:volatile保证新值能立即同步到主内存,每次使用前立即从主内存刷新。
    • synchronized
      • 对一个变量unlock前,必先把此变量值同步回主内存(store、write)。
    • final
  • 有序性
    • volatile:禁止指令重排序
    • synchronized:1个变量在同一时刻只允许一条线程对其lock,因此持有同一个锁的2个同步块只能串行地进入。
先行发生原则

Java与线程

  • 并发可以为多线程、多进程,Java中一般为多线程。
线程的实现
  • 线程是cpu调度的最基本单元。
  • Thread类与大部分Java API不同,它的所有关键方法均声明为Native。Java中的Native意味着这个方法没有使用或无法使用平台无关的手段来实现。
  • Java线程的实现
    • 使用内核线程实现
      • 程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口–轻量级进程。
      • 基于内核线程实现,线程操作(创建、析构、同步)需进行系统调用。系统调用代价较高,需在用户态、内核态间切换。
      • 轻量级进程要消耗一定内核资源(如内核线程的栈空间),因此一个系统支持的轻量级进程数量有限。
    • 使用用户线程实现
      • 不需系统内核支援,但线程创建、切换、调度、阻塞如何处理等问题太困难,因此使用用户线程实现的程序一般都比较复杂。Java、Ruby均放弃了用户线程实现。
    • 混合实现
  • 一条Java线程映射到一条轻量级进程。
Java线程调度
  • 协同式
  • 抢占式
    • Java使用的线程调度方式为抢占式。
    • Java共10个线程优先级,优先级越高的线程越容易被系统选择执行。
    • 优先级不是很靠谱
      • Java线程被映射到系统的原生系统,系统的线程优先级并不见得与Java线程优先级一一对应。
      • 系统本身可能会越过优先级而为某线程分配更多时间。
状态转换
  • New、Runable、Waiting与Timed Waiting、Blocked、Terminated
  • New 创建:创建后尚未启动。
  • Runable 运行:可能正在执行,也可能等待执行。
  • Waiting 无限期等待:
    • 等待被其他线程显式地唤醒。
    • 进入Waiting状态的方法
      • 无TimeOut的Object.wait()
      • 无TimeOut的Thread.join()
      • LockSupport.park()
  • Timed Waiting 限期等待
    • 一定时间后由系统自动唤醒
    • 进入Timed Waiting的方法:
      • Thread.sleep()
      • 有TimeOut的Object.wait()
      • 有TimeOut的Thread.join()
      • LockSupport.parkNanos()
      • LockSupport.parkUntil()
  • Blocked 阻塞
    • synchronized:线程等待进入同步区时进入阻塞状态。
    • 阻塞是在等待一个排它锁。Waiting是等待一段时间或被主动唤醒。
  • Terminated

线程安全与锁优化

线程安全

Java语言中的线程安全
  • 按照线和安全的“安全程序”由强到弱,Java中各种操作共享的数据分为:不可变绝对线程安全相对线程安全线程安全线程对立
  • 不可变
    • 不可变对象一定是线程安全的。
    • 只要不可变对象被构建出来,其外部的可见状态就永远不会改变。
    • 基本数据类型用final修饰会即是不可变的。
    • 若想让一个对象不可变,则需保证对象的行为不影响自己的状态(如将内部状态全部声明为final)。
    • String不可变,使用方法subString()、replace()、concat()不会影响原值,会新构造一个String。
    • 不可变对象:String、Long、BigInteger。AtomicInteger不是不可变对象。
  • 绝对线程安全
    • Java API中标注线程安全的类,大多数都不是绝对线程安全的。
    • Vector的方法被synchionized修饰,但即使所有方法都修饰为同步,也不意味着调用时不需额外的同步手段。如多个线程对同一个Vector进行add、remove元素操作。此时遍历vector可能会ArrayIndexOutOfBoundsException,遍历时先对vector实例加锁。
  • 相对线程安全
    • 对象的单独操作是线程安全的,但对于一些特定顺序的连续调用,则需额外的同步手段来保证正确性。
    • Vector、HashTable、Collections.synchronizedXXX。
  • 线程兼容
    • 对象本身不是线程安全的,在调用端使用同步手段来保证安全。
    • Java API中大部分类均是线程兼容,如ArrayList、HashMap。
  • 线程对立
    • 不管调用端是否采取同步,均无法在多线程环境中并发使用代码。
    • Java中很少出现线程对立。
    • Thread的suspend()与resume()方法,中断与恢复线程,多线程尝试去中断或恢复同一个thread,可能死锁。
线程安全的实现方法
  • 互斥同步

    • 同步:多线程并发访问共享数据时,同一时刻仅有一条线程可访问。
    • 互斥:实现同步的一种手段。具体包括临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)。
    • Java中最基本的互斥同步为synchronized,编译后生成monitorenter、monitorexit字节码指令。
    • synchronized对同一线程是可重入的,不会出现自己把自己锁死的问题。
    • Java线程会映射到操作系统原生线程,阻塞或唤醒线程,需从用户态切换到核心态,状态转换会耗费更多cpu时间。
    • ReentrantLock也可实现同步,有一些高级特性:等待可中断、可实现公平锁、锁可绑定多个条件。
      • 等待可中断:B持有锁,线程A等待获取锁,A可选择中断等等而去处理其他事情。
      • 公平锁:多线程同时等待同一个锁,按申请锁的时间顺序依次获得锁。synchronized、ReentrantLock默认均非公平锁,ReentrantLock可通过带布尔值的构造函数要求使用公平锁。
      • 绑定多个条件:一个ReentrantLock对象可同时绑定多个Condition对象。
    • 与ReentrantLock相比,synchronized能实现需求的情况下尽量使用synchronized进行同步。
  • 非阻塞同步

    • 先进行操作,若产生了对共享数据的争用,再进行其他的补偿措施(如不断重试直至成功)。
  • 无同步方案

    • 若一个方法不涉及共享数据,则无须同步。
    • 变量被多线程访问,可用volatile声明它为“易变的”。变量被某线程独享,可用java.lang.ThreadLocal类来实现线程本地存储的功能。
    • 每个线程的Thread对象均有一个ThreadLocalMap对象,key为ThreadLocal.threadLocalHashCode,value为本地线程变量。
    • 每个ThreadLocal变量均有一个threadLocalHashCode,以此查询ThreadLocalMap中对应的本地线程变量。

锁优化

  • 适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁。
自旋锁与自适应自旋
  • 线程挂起和恢复需消耗cpu时间,若共享数据的锁定时间很短,为这段时间去挂起和恢复线程不值得。
  • 自旋等待本身虽避免了线程切换开销,但它要占用cpu时间,若锁被占用的时间很短,自旋等待的效果就会非常好。
  • 自旋次数默认为10次,可通过-XX:PreBlockSpin来配置。
  • JDK1.6采用了自适应自旋
锁消除
  • 即时编译器在运行时,若检测到不存在共享数据竞争,则消除锁。
锁粗化
  • 编码时,尽量使同步块作用域最小。但如果一系列连续操作均是对同一个对象加锁解锁,频繁地进行互斥同步会对性能有所损耗,此时虚拟机会扩大锁的作用域。
轻量级锁
  • 对象头信息
    • 对象自身的运行时数据(Mark Word,实现轻量级锁与偏向锁的关键):哈希码、GC分代年龄。32Bits或64Bits
      • 32位vm,25Bits存储对象哈希码
      • 4Bits存储对象分代年龄
      • 2Bits存储锁标志位
      • 1Bits固定为0
    • 指向方法区对象类型数据的指针
  • HotSpot对象头 Mark Word
存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需记录信息 11 GC标记
偏向线程ID、偏向时间戮、对象分代年龄 01 可偏向
  • 轻量级锁过程
    • 进入同步代码块后,若同步对象未锁定(“01”状态),JVM先在当前线程的栈帧中建立锁记录,用于存储 Mark Word 的拷贝。
    • JVM使用CAS操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。
      • 若成功,则线程就拥有了对象锁,并将对象 Mark Word 的锁标志位变为“00”。
      • 若失败,检查对象的 Mark Word 是否指向当前线程的栈桢,如指向则说明当前线程持有锁,可直接进入同步块,否则说明锁对象被其他线程抢占了。
      • 若2条以上线程争用同一个锁,那轻量级锁不再有效,膨账为重量级锁,标志位变为“10”。
  • 对于绝大部分的锁,在整个同步周期内都是不存在竞争的。
  • 若无竞争,轻量级锁使用CAS操作避免了使用互斥量的开销。但若存在锁竞争,除互斥量开销,还额外发生了CAS操作,会比传统重量级锁更慢。
偏向锁
  • 轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,偏向锁是在无竞争情况下把整个同步都消除掉,连CAS操作都不做了。
  • JVM默认启用了偏向锁(-XX:+UseBiasedLocking)
  • 锁对象第一次被线程获取,将对象头中标志位置为偏向模式“01”。同时使用CAS操作把获取到这个锁的线程ID记录在对象的 Mark Word。
    • 若CAS操作成功,持有偏向锁的线程以后每次进入此锁相关的同步块,JVM可不再进行任何同步操作。
  • 当另一个线程去获取这个锁时,偏向模式结束,撤销偏向后恢复到未锁定“01”或轻量级锁定“00”状态。

Kommentare: