深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)读书笔记

jvm

本文记录个人阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)》一书中的摘要。后续会逐步对jvm相关原理内容进行扩充。力求成为目前互联网最全的jvm介绍。

百度网盘下载(仅作学习交流使用,如有侵权请告知)

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

运行时数据区域

运行时数据区域

程序计数器

当前线程锁执行的字节码的行号指示器。

  • 每个线程都需要有一个独立的程序计数器。
  • 程序计数器没有定义OutOfMemoryError的区域

java虚拟机栈

  • 也是线程私有的,生命周期同线程相同。
  • 描述的是java方法的执行的内存模型。
  • 局部变量表存放了编译期可知的各种基本数据类型、对象引用类型和returnAddress类型(指向了一条字节码指令的地址)。
  • 局部变量表所需要的内存空间在编译期间完成分配。
  • 两种异常:
    1. 如果线程请求的栈的深度大于虚拟机所允许的深度,抛出“StackOverflowError”。
    2. 如果虚拟机栈可以动态扩展(大多数虚拟机都支持),扩展时无法申请到足够的内存,抛出“OutOfMemoryError”。

本地方法栈

为虚拟机执行Native方法服务。

java堆

是所有线程共享的一块内存区域,在虚拟机启动的时候创建。

  • 目的:存放对象实例。
  • 是java垃圾回收器主要管理的区域。

方法区

  • 线程共享
  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。
  • 不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。

运行时常量池

是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

  • 具备动态性:并不是预置Class文件中的常量池的内容才能进入方法运行时常量;运行时也可以将新的常量放入池中。如String的intern()方法。

直接内存

  • 不是虚拟机运行时数据区的一部分。

HotSpot虚拟机对象探秘

对象的创建

(待)

对象的内存分布

对象头、实例数据、对其填充

对象头

存储对象自身的运行时数据,如:HashCode、GC分代年龄、锁状态、线程持有的锁、偏向线程id(此后不需要cas操作来加锁或解锁。只需测试markword里是否存储偏向锁。)、偏向时间戳等。

  • 类型指针:对象指向它类元素的指针。
  • 不是所有jvm都保留类型指针,查找对象不一定经过对象本身(如果是java数组,对象头部还会记录数组长度的数据)。

实例数据

对象真正存储的有效信息。也是在程序中所定义的各种类型的字段内容。

这部分定义会受到jvm分配策略参数和字段在java源码中定义的顺序的影响。

对齐填充

仅是占位符的作用。

对象的访问定位

通过栈上的引用数据来操作堆上的具体对象。

取决于不同虚拟机的实现, 目前有两种方式:

  • 使用句柄:java堆中会划出一部分来存储句柄池。reference存储的是对象句柄地址。句柄中存放了对象实例数据与类型数据各自的具体地址信息。

通过句柄访问对象

通过直接指针访问

  • 通过直接指针访问:java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference存储的是对象的地址

  • 句柄的好处:reference存储的是稳定的句柄地址;对象被移动时,知会改变句柄中实例数据的指针,reference本身不变。

  • 指针好处:访问块,少了一次指针定位的开销。

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

对象已死

引用计数法

  • java虚拟机中不采用:无法解决循环引用问题。

可达分析算法

通过一系列称为“GC Roots”的对象为起点,从这些起点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链,则称为不可达,即可以被回收。

可作为GC Roots

  • 虚拟机栈中引用的对象
  • 方法去中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native 方法)引用的对象。

再谈引用

四种:强引用、软引用、弱引用、虚引用。

  • 强引用:程序中普遍存在的,如Object obj = new Object() 这类的引用。只要强引用在,垃圾回收期不会回收。
  • 软引用:还在用但并非必需的对象。在系统将要发生内存泄露之前,将这些对象列为回收范围,进行第二次回收。
  • 弱引用:用来描述非必须对象,但强度比软引用更弱一些。只能生存到下一次牢记税收之前,无论没存事都供 ,都会进行回收。
  • 虚引用:也称为幽灵引用或者幻影引用。完全不会对其生存时间够层影响。无法通过虚引用来取得一个对象实例。目的:在这个对象被回收的时候收到一个通知。

生存还是死亡

不可达的对象至少要经过2次标记过程。判断不可达后进行第一次标记,进行筛选,条件是对象是否有必要执行finalize()方法。**当对象没有覆盖finalize()方法或finalize方法已经被虚拟机调用过虚拟机视这两种情况为“没有必要执行”。

  • 如果对象被判定为有必要执行finalize方法,将此对象放到F-Queue对象中。稍后,有虚拟机自建一个低优先级的Finalize线程去执行它。
  • 任何对象的finalize方法只能被系统自动调用一次。

回收方法区

主要回收两部分内容:废弃的常量、无用的类。

  • 废弃的常量:无对象引用。
  • 无用类
    1. 该类的所有实例都已经被回收,java堆中不存在该类的任何实例。
    2. 加载该类的ClassLoader已经被回收。
    3. 该类对应的java.lang.Class对象没有任务地方被引用,无法在任何地方通过反射访问到该类的方法。

      在使用反射,动态代理等,需要具备类的卸载功能,保证永久代不溢出。

垃圾收集算法

标记-清除算法

标记完成后,统一进行回收。

不足

  • 效率问题
  • 空间问题:标记清除后会产生大量不连续的内存碎片(**会导致分配大对象时,内存不足,导致垃圾回收)。

复制算法

为了解决效率问题。
将可用内存划分为大小两块,只是用其中一块。当这一块用完时,就将还存活的对象赋值到另一块上面,然后再把已使用过的内存空间一次清理掉。(每次都对半个内存区进行回收。)不用考虑内存碎片问题。

不足

  • 内存利用率不到,只使用一半。
  • 对存活率较高时进行较多的复制。操作,效率会变低。
    现在的商业虚拟机采用复制算法来回收新生代

标记-整理算法

针对老年代。
标记后,对存活的对象都向同一端移动。然后直接清理掉端边界以外的内存。

分代收集

  • 新生代:使用复制算法。
  • 老年代:使用“标记-清理”或者“标记-整理”。

HotSpot的算法实现

枚举根节点

  • 会出现停顿现象。
  • 使用称为OopMap的数据结构来记录那些地方存放了对象的引用。

安全点

  • HotSpot在特定的位置记录安全点。GC只在到达安全点的时候才暂停。
  • 指令序列复用会导致“长时间执行”,如:方法调用,循环跳转,异常跳转等。
  • GC发生时让所有线程都跑到最近的“安全点”再停下来。两钟方式:抢先式中断和主动中断。

安全区域

一段代码引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,可以将SafeRegion当做是SafePoint的扩展。

垃圾收集器

Serial收集器

Serial/Serial Old收集器运行示意图

  • 单线程的收集器(GC时必须停止其他所有的工作线程,直到手机结束
  • 运行于Client模式

ParNew收集器

ParNew/Serial Old收集器运行示意图

  • 使用多线程进行垃圾回收。
  • 运行于Server模式下的首选。
  • 目前,除了Serial之外,只有它可以与CMS收集器配合使用。
  • cpu越多,性能越好。

Parallel Scavenge

arallel ScavengeParallel Old收集器

  • 使用复制算法的收集器,又是并行的多线程收集器。
  • 目的:打到一个可控的吞吐量。(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
  • 停顿时间越短适合需要与用户交互的程序,适合交互较多的任务;高吞吐量可以高效的利用CPU时间,主要适合后台运算不需要太多交互的任务。
  • GC停顿时间的缩短是牺牲吞吐量和新生代空间来换取的。

Serial Old收集器

  • 是Serial老年代版本。
  • 一个单线程收集器,使用“标记-整理”算法。
  • 主要意义:给Client模式的虚拟机使用。
  • 如果是Server模式:1、和JDK1.5以前的版本中的Parallel Scavenge收集器搭配之用;2、作为CMS收集器的后备预案。

Parallel Old收集器

  • Parallel Old是Parallel Scavenge老年代版本。
  • 使用多线程和“标记-整理”算法。
  • 在吞吐量优先的场合,优先使用Parallel Scavenge+Parallel Old收集器。

CMS收集器

CMS收集器
Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器。

  • 相应时间优先,用户体验优先,采用CMS收集器。
  • 给予“标记-清除”算法实现。
  • 步骤:
    1. 初始标记:只是标记一下GC Roots能直接关联到的对象,速度快,并发标记阶段就是进行GC Roots Tracing 的过程。重新标记是微利修正并发标记期间因用户程序继续运作而导致标记变动的那边分对象标记。
    2. 并发标记:可以与用户线程一起工作。
    3. 重新标记:
    4. 并发清除:可以与用户线程一起工作。

缺点

1. 对cpu资源非常敏感。CMS默认开启的回收线程数=(cpu+3)/4。随着cpu的增加而下降。为了避免这个缺陷,虚拟机提供了一种称为“增量式并发收集器”(在并发标记、清理的时候让GC线程和用户线程交替运行)**效果一般,不提倡使用**。
2. 无法处理浮动垃圾。
3. “标记-清除”导致内存碎片的产生。从而导致Full GC的产生。**为了解决这个问题,CMS提供一个-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。**

G1 收集器

G1 收集器

面向服务器端的收集器。

  • 并行与并发:充分利用pu来缩短Stop-The-World的停顿时间。
  • 分代收集:能够采用不同的方式去处理新创建的对象和已经存货一段时间、熬过多次GC的就对象以获取更好的手机效果。
  • 空间整合:整体采用“标记-整理”算法,从局部(两个Region之间)上来看是居于“复制”算法实现的。意味着G1在运行期间不会差生内存碎片。
  • 可预测的停顿:降低停顿时间,追求低停顿,建立可预测的停顿时间模型。
  • 步骤:
    1. 初始标记:标记与GC Roots直接关联的对象。
    2. 并发标记:从GC Roots开始对堆中的对象进行可达性分析,找出存货的对象,耗时场,可与用户程序并发执行。
    3. 最终标记:
    4. 筛选回收:

垃圾收集器总结

HotSpot收集器

两者存在连线,代表可以搭配使用。

内存分配和回收策略

对象优先在Eden分配

  • 对象有现在Eden区分配,Eden没有足够的空间时将发起一次Minor GC。

Minor GC:新生代GC,指发生在新生代的垃圾收集动作,速度快。
老年代GC(Major GC/Full GC):老年代发生的牢记收集动作,Major GC一般比Minor GC慢10倍以上。

大对象直接进入老年代

比如:很长的字符串以及数组。

长期生存的对象将进入老年代

为每个对象定义了一个年龄计数器,如果第一次Minor GC后仍然存活,并且被survivor容纳的话,将被移动到Survivor孔家中,并且对象年龄设为1;对象在Survivor去中熬过一次Monior GC,年龄增加1,档年龄增加到一定程度(默认15),将会移动到老年代。这个发着可以通过参数-XX:MaxTenuringThreshold设置。

动态对象年龄判断

为了更好地适应不同程度的内存状况,虚拟机并不是永远的要求对象必须到达了MaxTenuringThreshold才能晋升为老年代,如果在Survivor空间中相同年龄所有对象大小之和大于Survivor空间的一般,年龄大于或等于改年龄的对象将被移动到老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代左右对象总空间,如果成立,则Minor GC是安全的;如果不成立,会擦看HandlePromotionFailure设置值是否允许担保失败。如果允许那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次MiNor GC;如果小于则改为进行一次Full GC。

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

(略)

类文件结构

class类文件的结构

8位二进制为基础单位的二进制流。两种数据类型:无符号数和表

  • 无符号数:用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串。
  • 表:由多个无符号或者其他表作为数据项构成的符合数据类型。

魔数和Class文件的版本

  • 头4个字节为魔数(0xCAFEBABE),确定这个文件是否是一个能够被虚拟机接受的Class文件。(文件识别功能

  • 5、6字节记录此版本号;7、8字节记录主版本号。

常量池

常量池的大小不固定,入口放置一个计数值(constant_pool_count)。
**第0项常量空出来,为了后续表达“不引任何一个常量池项目”。

  • 主要存放两大类常量:字面量、符号引用
    包括:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

类索引、父类索引与接口索引集合

(待)

字段表集合

用于描述接口或者类中声明的变量。

包括类级变量以及实例变量,不包括方法内部声明的局部变量。

  • 不会列出从超类或父类继承过来的字段,单有可能列出原本java代码之中不存在的字段(如:内部类中,为了保护对外部类的访问性,会自动踢啊你接啊指向外部类的实例字段)。
  • java语言中字段无法重载(不允许字段名称相同),字节码允许重名字段。

方法表集合

方法里的代码去哪了?

存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

  • 父类的方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。

  • 有可能出现由编译器自动添加的方法最典型的就是类构造器“”方法和实例构造器“”方法。

  • **如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,用来描述某些场景专有的信息。

  • 不要求各个属性表的顺序

字节码指令

java是面向操作数栈,所以大多数指令都不包含操作数,知识操作码。

字节码与数据类型

大多数的指令都包含了其操作数据类型信息。

虚拟机类加载机制

将Class文件加载到内存,形成被虚拟机直接使用的java类型。

  • 运行时动态加载和动态连接特点。

类加载机制

加载、验证、准备、解析、初始化、使用和卸载。
Class生命周期

  • 解析:==有时可以在初始化的时候再再开始。这是为了支持java运行时绑定==

必须立即执行“初始化”

  • 遇到new、getstatic、putstatic或者invokestatic指令。(new 实例化对象的时候,读取或设置一个类的静态变字段、调用一个类的静态方法的时候。)
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发起初始化。
  • 初始化一个类的时候,如果其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机先初始化这个主类。
  • 使用动态语言时。

接口加载过程和类有些不同:接口没有static块,但是编译器会生成“()”类构造器,用于初始化接口所定义的成员变量。接口只有在真正使用的时候(引用接口中定义的常量)才会初始化。

类接加载过程

  • 通过类的全局限定名来后去定义此类的二进制字节流。
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象。作为方法区这个类的各种数据访问入口。
  • 用户可以通过自定义的类加载器去控制字节流的获取方式(重写一个类加载器的loadClass()方法)。
  • 数组类比较特殊,本身不是通过类加载器创建的,它是由java虚拟机直接创建的。

数组的创建过程

  • 如果数组的组件类型是引用类型,则递归进行类加载
  • 如果不是引用类型,java虚拟机将会把数组c标记为与引导类加载器关联。
  • 数组类的可见性和它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

验证

确保Class文件的字节流中包含信息符合虚拟机要求。

  • 文件格式验证:字节流正确解析并存储于方法区内。
  • 元数据验证:对字节码描述信息语义分析。
  • 字节码验证:通过对数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:符号引用转换为直接引用(这个转换将在解析阶段中发生)。目的:确保解析动作能正常执行。

准备

为变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中使用。

  • 仅包括类变量,被static修饰的变量;实例变量放在java堆中。
  • 仅设置数据类型的零值。

特殊情况:当类字段的字段属性中存在ConstantValue属性,在编译时javac会将变量设为初始值。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用:可以直接指向目标的指针,相对偏移量或是一个能间接定位到目标句柄。** 和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的之间引用一般不会相同。如果有了直接引用,那么引用的目标必定已经存在内存中。

  • 虚拟机可以对第一次解析结果进行缓存(在运行时常量池中记录直接引用,并把常量表识为已解析状态),避免解析动作重复执行。

类和接口的解析

(待)

字段解析

(待)

类方法解析

(待)

接口方法解析

(待)

初始化

在准备阶段,变量已经赋过系统要求的额初始值。在初始化阶段,根据程序设置的初始值进行初始化。

  • () 方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生。静态语句块只能访问到定义在静态语句块之前的变量,定义在之后的变量在之前给静态块中可以赋值,但不能当问。
  • ()和类的构造函数不同吗,它不需要显示的调用父类的构造函数,虚拟机保证在子类的()执行之前,父类的()执行完毕。
  • 父类的()先执行,父类的静态块要优先于子类的类变量赋值操作。
  • ()方法对于类和接口不是必须的,如果一个类没有静态块,也灭有对变量的赋值操作,编译器不会为这个类生成()方法。
  • 接口中没有静态块,但仍然有赋值操作。接口可以有()方法,但是,与类不同的是:不需要先执行父类的()防范。只有当父类中定义的变量使用时,父类接口才会初始化。接口实现类初始化时也不会执行接口的()
  • 虚拟机会保证一个类的()方法在多线程环境中被加锁、同步。

类加载器

类与类加载器

类的唯一性确认:同一个类加载器和类

类相等:代表类的Class对象的equals、isAssignableForm、isInstance方法返回结果相同。

双亲委派模型

存在两种类加载器:启动类加载器(Bootstrap ClassLoader),属于虚拟机本身一部分;另一个是java实现的其他类加载器,独立于虚拟机外部,继承于ClassLoader

类加载器分类

  • 启动类加载器:负责将JAVA_HOME\lib目录中或者-Xbootclasspath参数所指定的路径中,并且虚拟机可识别的类加载虚拟机内存中。启动类加载器无法被java程序直接引用,用户自定义类加载器,如果需要把加载请求委派给引导类加载器,那么直接使用null
  • 扩展类加载器:负责加载JAVA_HOME\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路劲中的类库。开发者可以直接使用。
  • 应用程序加载器:由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称为系统类加载器。负责加载用户类路径上所指定的类库。开发者可以使用,如果没有自定义类加载器,此加载器为默认类加载器。

类加载双亲委派模型

双亲委派除了顶层的启动类加载器外,其他类加载器都应当有自己的父类加载器。父子关系不会一继承的方式实现,而是以组合关系。

  • 加载过程:不会自己去加载,而是先委派给父类加载器加载,如果父类无法加载,子类才尝试加载。

破坏双亲委派模型

  • 覆盖loadClass()方法。JDK1.2之后,建议把自己的类加载逻辑写到findClass方法中,在loadClass方法中的逻辑里如果加载失败,则会调用自己的findClass方法。
  • 线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程是还未设置,将会从父类线程中继承一个。如果全局都没设置,则使用默认的应用程序类加载器。
  • 程序的动态性,如:代码热部署(HotSwap)、模块热部署(Hot Deployment)等。OSGI实现模块热部署的关键则是他们自定义的类加载机制的实现。

    OSGI类加载器不再是双亲委派结构,而是网状结构。

虚拟机字节码执行引擎

运行时栈帧结构

栈帧适用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区的虚拟机栈的栈元素。

  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法的返回地址等信息。
  • 方法的运行对应着虚拟机栈里面的入栈出栈的过程。

堆栈的概念结构

局部变量表

局部变量表是一组变量值存储空间,用于存放方法的参数和方法内部定义的局部变量。

  • 虚拟机通过索引的方式使用局部变量表。
  • 为了节省栈帧的空间,局部变量表中的slot是可以重用的。
  • 如果局部变量表中slot还存在着对数据对象的引用,没有被其他变量复用,作为GC Roots的一部分的局部表量表依旧保持着对它的引用。这个引用并没有被及时打断。手动设置为null可以看作是一中特殊情况。(==赋值null的操作在经过JIT编译优化后就会被清除,这时候将变量设置为null就没有意义==了。)
  • 局部变量表不存在“准备阶段”。类变量有两次赋值的过程(准备阶段,设置系统初始值;初始化阶段设置程序设置的初始值)

操作数栈

方法刚开始执行的时候,这个方法的操作数栈是空的。

动态连接

每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为例支持方法调用过程中的动态连接。

方法返回地址

方法执行后,两种方式退出方法:

  1. 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令,这是可能有返回值返回上层调用者。
  2. 异常完成出口:遇到异常,并且这种异常没有在方法体内得到处理。这种退出不会给上层调用返回值。

附加信息

方法调用

并不等同于方法执行,唯一的任务就是确定被调用方法的版本,不涉及到方法内部的具体执行过程。一切方法调用在Class文件中都是符号引用,而不是方法在实际运行时内存布局中的入口地址。(需要在类加载或者运行时才能确定目标方法的直接引用)。

解析

解析阶段会将其中一部分符号引用转换为直接引用。

前提:

  1. 方法在程序真正执行之前就有一个可确定的调用版本
  2. 并且这个调用版本在运行期是不可改变的。

主要包括静态方法和私有方法两大类

  • 解析调用时一个静态过程,在编译期间就完全确认,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期。

分派

  • 静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。典型应用就是方法的重载。静态分派发生在编译阶段。
  • 动态分派:在运行期根据实际类型确定方法的执行版本的分派。体现在方法的重现
  • 单分派与多分派:方法的接收者与方法的参数统称为方法的宗量。静态分派属于多分派类型;动态分派属于单分派类型。
  • 虚拟机动态分派的实现:为了优化动态分派,使用了徐方法表(Vritual Method Table)和接口方法表(Interface Method Table)。

动态类型语言支持

特征:类型检查的主体过程是在运行期间而不是编译期。

  • invokedynamic: JDK1.7 引入。分派逻辑是有程序员决定。

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

解释执行

基于栈的指令集与基于寄存器的指令集

  • 指令流中的指令大部分都是零地址指令,他们依赖操作数栈进行工作。(另外一种是基于寄存器的指令集)

两种指令集对比:

基于栈的优点:

  1. 可移值;寄存器依赖于硬件的寄存器。
  2. 嗲吗更加紧凑。
  3. 编译实现更简单。

    基于栈的缺点:

    1. 执行速度稍慢。
    2. 比基于寄存器的指令数要多,栈的实现是语句内存的,内存访问成为瓶颈。

基于栈的解释器执行过程

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

Web服务器需要解决的问题

  1. 部署同一个服务器的两个Web应用程序所使用的java类库可以实现相互隔离。
  2. 部署同一个服务器的两个Web应用程序所使用的java类库可以互相共享。
  3. 服务器尽可能保证自身的安全不受Web应用影响。
  4. 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。

单独的一个ClassPath就无法满足需求了,所以各个Web服务器提供了好几个ClassPath路劲供用户存放第三方类库。

OSGI 灵活的类加载器架构

  • osgi的bundle类加载器之间只有规则,没有固定的委派关系。
  • 各个bundle的类加载器是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造bundle间的委派和依赖。

字节码生成技术与动态代理的实现

(待)

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

待解决问题

  1. 如何编译提交到服务器上的java代码
    • 使用tools.jar中的com.sun.tools.javac.Main类来编译java文件。
    • 把编译好的Class文件上传到服务器执行。
  2. 如何执行编译后的java代码
    • 让类加载器加载这个类生成的一个Class类,然后通过反射调用某个方法。(要考虑如何实现java类执行完后的卸载和回收
  3. 如何收集java代码执行结果
    • 把System.out的符号引用替换为我们准备的PrintStream的符号引用。

实现

(四个类)

验证

早起(编译器)优化

java语法糖的味道

java中的泛型、自动装箱、自动拆箱、遍历循环、变长参数和条件编译等。

泛型与类型擦除

  • java在字节码编译后会将泛型擦除,运行时,已经转换为原生类型。
  • 方法重载:具有不同的返回值得重载方法,可以在Class文件中共存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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));
    }

条件编译

  • 使用条件为常量的if语句可以达到编译就运行。
  • 编译器会将不成立的分支擦除。
  • 实现语句块级别的条件编译。

实战:插入式注解处理器

(略)

晚期优化

  • 为了提高热点代码的执行效率,运行时,虚拟机会将这些代码编译成本地平台相关的机器码,进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器)。

HotSpot虚拟机内的即时编译器

需要解决的问题

  • 为何HotSport要使用解释器和编译器并存的架构?
  • 为何HotSport虚拟机要实现两种不同的即时编译器
  • 程序何时使用解释器执行,何时使用编译器执行
  • 哪些代码会被编译为本地代码?如何编译?
  • 如何从外部观察即时编译器的编译过程和编译结果?

    1. 当程序需要迅速启动和执行的时候,解释器省去了编译的时间,立即执行;运行后,编译器会将越来越多的代码编译为本地代码,以获得更高的执行效率。
    2. 内存限制较大的,使用解释器执行节约内存。
    3. 解释器和解释器的交互
      解释器和解释器的交互
    4. 使用-client或-server参数指定解释器的模式(mixed mode);使用-Xint 强制虚拟机运行于解释器模式(interpreted mode);使用-Xcomp强制运行于编译模式(complied mode但是在编译器无法进行时,解释器依旧会介入)。

编译对象与触发条件

何为热点代码

  • 被多次调用的方法
  • 被执行多次的方法(如:while循环语句块的代码等)

热点探测判定方式

  • 基于采样的热点探测:jvm会周期性的检查各个线程栈顶,如果发现某个(或某些)方法经常出现在栈顶,那么这个方法就是“热点方法”。
    1. 好处:简单、高效,和可以很容易地获取方法的调用关系。
    2. 缺点:很难精准地确认一个方法的热度,容易因为受到线程阻塞或别的外接因素的影响而扰乱热点探测。
  • 基于计数器的热点探测:jvm会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值就认为是“热点方法”。
    1. 优点:统计的更加精确和严谨。
    2. 缺点:实现麻烦,为每个方法建立并维护计数器,不能直接获取到方法的调用关系。
      调用计数器热度的衰减:当超过一定的时间限额,如果方法的调用次数仍然不足以让它交给即时编译器编译,那么方法的调用计数器就被减半。这段时间叫做半衰期。热度衰减是在虚拟机垃圾收集时进行的。
1. 可以使用虚拟机参数:**-XX:UseCounterDecay**来关闭热度衰减。
2. 使用-XX:CounterHalfLife参数设置半衰周期时间,单位:秒。

回边计数器:统计一个方法中循环体代码执行次数,在字节码中遇到控制流向后跳转的指令称为“回边”。目的是为了触发==OSR编译==。回边计数器没有热度衰减过程

1.通过-XX: CompileThreshold设置阀值。
2.间接通过-XX:OnStackReplacePercentage设置

编译过程

(略)

查看及分析即时编译结果

(略)

编译优化技术

优化技术概览

公共表达式消除

  • 仅限于程序基本块内

编译优化推荐书籍《龙书》

数组辩解检测消除

方法内联

  • 消除了方法调用的成本。

逃逸分析

  • 基本行为是分析对象动态作用域。
  • 方法逃逸:被外部方法访问到,如:作为调用参数传递到其他方法中。
  • 线程逃逸:被外部线程访问到,如:赋值给类变量或可以在其他线程中访问的实例变量。

变量的高效优化

  • 栈上分配:栈上分配的对象,会随着栈帧出栈而销毁。
  • 同步消除:不会逃逸出线程的变量,不需要同步。
  • 标量替换:将java对象拆分,恢复为原始类型来访问。

java与c/c++的编译器对比

(略)

java内存模型与线程

##java内存模型

主内存和工作内存

java内存模型主要的目标是定义程序各个变量的访问规则。

内存交换工作

  • lock:作用于主内存变量,把一个标量标记为一条线程独占。
  • unlock:作用于主内存变量,释放后的变量才能被其他线程访问。
  • read:作用于主内存变量,包变量从主存传输到工作内存。
  • load:作用于工作内存变量,将read的变量读入工作内存的变量副本中。
  • use:作用于工作线程,将工作线程中的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作线程的变量,把工作内存中的一个变量的值传送到主内存中,以便后续write操作。
  • write:作用于主内存变量,吧store操作从工作内存中得到的变量值放入主内存中。

对于volatile型变量的特殊规则

特性

  1. 保证此变量所有线程的可见性。

不符合以下两条规则的运算场景需要通过加锁保证原子性。

  • 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。
  1. 禁止指令重排序优化
    volatile指令会在本地代码中增加内存屏障指令,保证处理器不发色很难过乱序执行。

对于long和double型变量的特殊规则

  • 允许虚拟机实现不保证64位数据类型的load、store、ead、write的原子性。

原子性、可见性与有序性

原子性

提供了更高层次的字节码指令monitorenter和monitorexit来隐式使用两个操作,这两个字节指令放映到Java代码中的是synchronized关键字。

可见性

当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

关键字:volatile、synchronized、final

有序性

java提供了volatile和synchronized保证了线程之间的有序性。volatile包含禁止指令冲排序的语义;synchronized保证同一时刻只允许一条线程对其进行lock操作。

先行发生原则

happens-before:判断数据是否在竞争、线程是否安全的主要依据。

  • 程序现行规则
  • 管程锁定规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则
  • 线程终端规则
  • 线程终结规则
  • 传递性

java与线程

线程的实现

  • Thread所有的关键方法都是Native的,native方法意味着这个方法无法使用或无法使用平台无关的手段实现(也可能为了执行效率而使用native方法。)。
  • 线程实现方式:内核线程实现、用户线程实现、用户线程+轻量进程实现

使用内核线程实现

由内核完成线程切换。程序不会直接使用内核线程,而是使用内核线程的一种高级接口。
轻量级进程:内核线程 = 1:1

局限性

  • 由于是由内核线程实现的,线程的操作都需要进行系统调度。系统调用的代价高
  • 每个轻量级进程都需要内核线程的支持,所以轻量级进程要消耗一定的内核资源。

使用用户线程实现

用户线程建立在用户空间的线程库上,用户线程的操作不需要内核的帮助,完全在用户态中完成。

进程:用户线程 = 1:N

优势

  • 不需要系统内核支援。

劣势

  • 线程操作都需要用户程序自己处理。

使用用户线程加轻量级进程混合实现。

用户线程:轻量级进程 = N:M

java线程的实现

Sun JDK

  • windows和linux使用一对一模式。
  • solaris支持一对一和多对多模式;通过-XX:UseLWPSynchronization(默认)和-XX:USEBoundThreads来指定。

java线程调度

线程调度是指系统为线程分配处理器使用权的过程。只要有两种:协同线程调度和抢占式线程调度。

  • 协同调度:

    优点:
    实现简单,线程干完自己的工作后才进行线程切换,所以不存在线程同步问题。

缺点:
执行时间不可控,如果线程出现问题,不告知系统,程序一直阻塞。

  • 抢占式调度
    每个线程将由系统分配执行时间。线程切换不由本身决定。

    优点:
    执行时间可控

状体切换

java定义了线程的5种状态

  • 新建(new):创建后尚未启动的线程。
  • 运行(Runnable):包括系统线程状态中的Running和Ready,处于这个状态的线程有可能正在运行或者正等待cpu分配时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配cpu执行时间,他们要等待被其他线程显示地唤醒。

    1. 没有设置Timeout参数的Object.wait()方法。
    2. 没有设置Timeout参数的Thread.join()方法。
    3. LockSupport.park()方法。
      +限期等待(Timed Waiting):处于这种状态的线程也不会被分配cpu执行时间,不过无须等待其他线程显示地唤醒,在一定时间之后他们由系统自动唤醒。
    4. Thread.sleep()方法。
    5. 设置Timeout参数的Object.wait方法。
    6. 设置了Timeout参数的Thread.join()方法。
    7. LockSupport.parkNanos()方法。
    8. LockSupport.parkUntil()方法。
  • 阻塞(Blocked):线程被阻塞,阻塞状态和等待状态的区别:在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间,或者唤醒动作的发生。

  • 在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

线程安全与锁优化

线程安全

java语言中的线程安全

java语言中各种操作共享数据分为:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

不可变

  • 不可变的对象一定是线程安全的。
  • 如果共享数据是一个基础数据类型,那么只要在定义时使用final关键字修饰,就可以变为不可变的。如果共享数据是一个对象,那么需要保证对象的行为不会对其状态产生影响。

    途径

    1. 把对象中带有状态的变量声明为final,这样在构造函数结束之后,它就是不变的。(AtomicInteger使用volatile保证线程安全)

绝对线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//线程不安全
private static Vector<Integer> vector = new Vector<Integer>();

public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}

Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});

Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println((vector.get(i)));
}
}
});

removeThread.start();
printThread.start();

//不要同时产生过多的线程,否则会导致操作系统假死
while (Thread.activeCount() > 20);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//需要额外手段保证线程安全
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});

Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
System.out.println((vector.get(i)));
}
}
}
});

相对线程安全

保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施(**但对于一些特定顺序的联系调用,需要额外的同步手段)。

线程兼容

指对象并不是线程安全的,但是可以通过在调用段正确的使用同步手段来保证对象的线程安全。

线程对立

指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
如:Thread类的suspend和resume,存在线程死锁的风险。(resume已经被废弃)

线程安全的实现方法

互斥同步

同步:保证数据在同一时刻只能被一个线程使用。
互斥:是实现同步的一种手段,临界区、互斥量和信号量都是互斥的实现方式。

  • java中synchronized是实现互斥同步的手段(会在字节码前后形成monitorenter和monitorexit这两个字节码指令,这两个指令都需要一个reference类型的参数来指明需要锁定和释放的对象。如果synchronized是指定对象参数,就是这个对象的reference;如果是实例方法或类方法,就是对象实例或Class对象作为锁对象)。
  • 还可以使用J.U.C中的重入锁(ReentrantLock)来实现同步。同样,具备线程重入特性。

    ReentrantLock高级功能

    1. 等待可中断:值当持有锁的线程长期不是放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事,可中断特性对处理执行时间非常长的同步块很有帮助。
    2. 公平锁:多个线程等待锁时,必须按照申请锁的时间顺序来一次获得锁;而非公平锁在锁释放时,任何一个等待线程都可以获得锁。synchronized是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造函数要求使用公平锁。
    3. 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而synchronized中,锁的wait、notify、notifyAll方法实现一个隐含的条件,如果要和多于一个的条件关联起来的时候,就不得不额外添加一个锁。而ReentrantLock不需要,只需要多次嗲用newCondition方法即可。

非阻塞同步

需要硬件指令集保证,如以下硬件指令:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap)
  • 加载连接/条件存储(Load-Linked/Store-Conditional)

CAS指令:
有3个操作数,分别是内存位置(用v表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当v符合旧预期值A时,处理器用新值B更新V的值,否则它不执行。但无论是否更新了V,都会返回V的旧值。

  • jdk1.6之后,程序中可以使用Unsafe类来实现CAS操作(**只有启动类加载器Bootstrap ClassLoader)加载Class才能访问它。如果不使用反射,只能通过其他java api来间接使用它,如J.U.C包里面的整数原子类。

CAS的ABA问题
J.U.C 为了解决此问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它通过控制变量值得版本来保证CAS的正确性(一般使用传统的互斥同步处理。)。

无同步方案

  • 可重入代码:这种代码也叫做纯代码。

    特性:
    不依赖存储在堆上的数据和公用的系统资源,用到的状态都是通过参数传入、不调用非可重入的方法等。

  • 线程本地存储:共享数据可见范围在同一个线程之内。

锁优化

适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁等。

自旋锁与自适应自旋

互斥同步缺点:挂起和恢复线程的操作需要转入内核态完成,带来较大的性能影响。

自旋锁

让请求线程稍等一下,不放弃处理器执行时间,看看持有锁的线程是否很快就会释放锁,为了对线程等待,只需要让线程执行一个忙循环(自旋),这就是自旋锁。

  • jdk1.4.2已经引入,只不过默认关闭。通过参数:-XX:UseSpining来开启。jdk1.6已经默认开启。
  • 不能代替阻塞,虽然避免了线程切换的开销,但需要占用处理器的时间。自旋的等待时间必须有一定的限额。如果超过了限定的次数,仍然没有获取到锁,就需要挂起线程。自旋的次数默认是10次(可以通过参数:-XXPreBlockSpin来改变)。

自适应自旋锁

自旋的时间不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

锁消除

指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

  • 锁消除的依据:来源于逃逸分析的数据支持。

锁粗化

  • 同步快的作用范围尽量小,便于等待锁的线程尽快获得锁。
  • 如果在循环体内频繁的加锁和解锁同样损耗性能(虚拟机探测到一串零碎的操作都是对同一个对象加锁,会将锁粗化处理)。

轻量级锁

相对于系统互斥量来实现的传统锁而言的。

  • 对象头信息是与对象自身无关的额外存储成本,“Mark Word”被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,会根据对象的状态复用自己的存储空间。
  • ==轻量级锁执行过程。==
  • 在无竞争条件下使用CAS操作区消除同步使用的互斥量。

偏向锁

目的:消除数据在无竞争情况在的同步原语,进一步提高程序运行的性能。

  • 在无竞争的情况下把整个同步都消除,持有偏向锁的线程将永远不需要再进行同步。
  • ==偏向锁执行过程==
Alan Zhang wechat
欢迎您扫一扫上面的微信公众号“补愚者说”,订阅我的博客!