《深入理解Java虚拟机》
1.2 Java技术体系
JDK、JRE与JVM之间的关系:
 JDK全程为Java Development Kit(Java开发工具),提供了编译和运行Java程序所需的各种资源和工具,包括:Java 程序设计语言、Java 虚拟机、Java API类库。
 JRE全称为Java runtime environment(Java运行环境),包括:Java SE API 子集、Java 虚拟机。
 
Java技术体系可分为4个平台,分别为:

2.2.1 程序计数器(线程私有)
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。
如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值为空。
程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。
2.2.2 Java虚拟机栈(线程私有)
Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期与线程相同。
 虚拟机栈描述的是Java方法执行的动态内存模型。
 栈帧: 每个方法执行都要创建一个栈帧,方法执行完毕,栈帧销毁。用于存储局部变量表,操作数栈,动态链接,方法出口等。
 局部变量表:存放编译期可知的各种基本数据类型,引用类型,局部变量表的大小在编译期便已经可以确定,在运行时期不会发生改变。
每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
 这个区域有两种异常情况:
 StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
 OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时
2.2.3 本地方法栈(线程私有)
虚拟机栈为虚拟机执行 Java 方法(字节码)服务。
 本地方法栈(Native Method Stacks)为虚拟机使用到的 Native 方法服务。
2.2.4 Java堆(线程共享)
java虚拟机最大的内存区域,唯一目的是存放对象实例,也是垃圾收集器管理的主要区域,分为新生代(由Eden 与Survivor Space 组成)和老生代,可能会抛出OutOfMemoryError异常。
 
2.2.5 方法区
存储虚拟机加载的类信息(类的版本、字段、方法、接口),常量,静态常量,即时编译后的代码等数据,也可能会抛出OutOfMemoryError异常。
 方法区与永久代实际并不等价,对于HotSpot中才有永久代的概念。
2.2.6 运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符合引用,这部分内容将在类加载后进入方法区的内容常量池中存放。
public class Changliang {
    public static void main(String[] args) {
        // s1与s2是相等的,为字节码常量
        String s1 = "abc";
        String s2 = "abc";
		
        // s3创建在堆内存中
        String s3 = new String("abc");
		
        // intern方法可以将对象变为运行时常量
        // intern是一个native方法
        System.out.println(s1 == s3.intern()); // true
    }
}
2.2.7 直接内存
直接内存并不是虚拟机运行时数据区的一部分也不是Java虚拟机规范中定义的内存区域。
 jdk1.4中增加了NIO,可以分配堆外内存(系统内存替代用户内存),提高了性能。
2.3 虚拟机对象探秘
2.3.1 对象的创建

 如何在堆中给对象分配内存
 两种方式:指针碰撞和空闲列表。我们具体使用的哪一种,就要看我们虚拟机中使用的是什么垃圾回收机制了,如果有压缩整理,可以使用指针碰撞的分配方式。
 指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存度放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞
 空闲列表:有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录,这就叫做空闲列表。
2.3.3 对象的内存布局
对象的内存布局分为三块:对象头、数据实例、类型填充
 对象的结构
 Header(对象头)分为
- 自身运行时数据(32位~64位 MarkWord):哈希值、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳
  
- 类型指针(指向什么类的实例)
InstanceData:数据实例,即对象的有效信息,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前。
 Padding:占位符填充内存
2.3.3 对象的访问定位
常用的有句柄访问和指针访问。
 句柄访问:Java堆中会划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
 指针访问:引用变量中存储的就直接是对象地址了,在堆中不会分句柄池,直接指向了对象的地址,对象中包含了对象类型数据的地址。HotSpot采用直接定位。
2.4 实战:OutOfMemoryError异常
2.4.1 Java堆溢出
堆内存抛出异常:
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
要解决这个区域的异常,一般先通过内存映像分析工具对Dump出来的堆转储快照进行分析。
2.4.2 虚拟机和本地方法栈溢出
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
  
如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowError异常。
 如果虚拟机在扩展栈时无法申请到足够的内存,则抛出OutOfMemoryError异常。
2.4.3 方法区和运行时常量次溢出

public class RuntimeConstantPool {
	public static void main(String[] args) {
		String s = new StringBuilder("计算机").append("软件").toString();
		System.out.println(s.intern() == s);
		String s1 = new StringBuilder("ja").append("va").toString();
		System.out.println(s1.intern() == s1);
	}
}
1.6会返回两个false,intern() 方法会把首次遇到的字符串实例复制到永久代,StringBuilder创建的在堆上,不是一个引用。
 1.7 返回一个true,一个false。intern()不会复制,只在常量池记录首次出现的引用。“java”之前已经出现过,不符合首次引用。
2.4.4 本机直接内存溢出
垃圾收集器和内存分配策略
3.2 对象已死吗?
在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象中哪些还“存活”着,哪些已经“死去”。
3.2.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它,计数器就+1,;当引用失效时,计数器就-1;任何时刻计数器都为0的对象就是不能再被使用的。
 缺点:很难解决对象之间的循环引用问题。
3.2.3 可达性分析算法
以GC Roots为起点,向下搜索,搜索走过的路径称为引用链,当一个对象到达GC Roots没有任何引用链时,就会别判定可回收。

可作为GC Roots的对象:
- 虚拟机栈
- 方法区的类属性所引用的对象
- 方法区中常量所引用的对象
- 本地方法栈中引用的对象
3.2.3 再谈引用
在JDK1.2后,引用被分为强引用、软引用、弱引用、虚引用四种。
- 强引用 Strong Reference
Object obj =  new  Object();
代码中普遍存在的,像上述的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
 2. 软引用 Soft Reference
 用来描述一些还有用,但并非必须的对象。软引用所关联的对象,有在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围,并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存异常。提供了 SoftReference 类实现软引用。
 3. 弱引用 Weak Reference
 描述非必须的对象,强度比软引用更弱一些,被弱引用关联的对象,只能生存到下一次垃圾收集发生前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了 WeakReference 类来实现弱引用。
 4. 虚引用 Phantom Reference
 一个对象是否有虚引用,完全不会对其生存时间够成影响,也无法通过虚引用来取得一个对象实例。为一个对象关联虚引用的唯一目的,就是希望在这个对象被收集器回收时,收到一个系统通知。提供了 PhantomReference 类来实现虚引用。
3.2.4 生存还是死亡
如果覆写了finalize()方法,则会被放到F-Queue里等待执行。对象可以实现一次救赎。
 同一个对象finalize()只能执行一次。
3.2.5 回收方法区
方法区主要回收废弃的常量和无用的类。
3.3 垃圾收集算法
3.3.1 标记-清除算法
分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
有什么缺点?
- 效率问题。标记和清除过程的效率都不高。
- 空间问题。标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致,程序分配较大对象时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。
3.3.2 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
优点:复制算法使得每次都是针对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:将内存缩小为原来的一半。在对象存活率较高时,需要执行较多的复制操作,效率会变低。

3.3.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
 
3.3.4 分代收集算法
根据对象的存活周期,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点,采用最适当的收集算法。
新生代:每次垃圾收集时会有大批对象死去,只有少量存活,所以选择复制算法,只需要少量存活对象的复制成本就可以完成收集。
老年代:对象存活率高、没有额外空间对它进行分配担保,必须使用“标记-清理”或“标记-整理”算法进行回收。
Minor GC 和 Full GC有什么区别?
 Minor GC:新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般回收速度较快。
 Full GC:老年代 GC,也叫 Major GC,速度一般比 Minor GC 慢 10 倍以上。
3.4 分代收集算法
3.4.2 安全点
安全点的选择以程序“是否具有让程序长时间执行的特征”来选定,例如:方法调用、循环跳转、异常跳转等;另一个因素:如果在GC发生时让所有线程在安全点上暂停。方案:抢占式中断和主动式中断。普遍jvm采用抢断式。GC发生时,让所有线程中断,如果有线程中断的位置不在安全点,则恢复线程,待其执行到安全点中断。
3.4.3 安全区域
Safe Point 是对正在执行的线程设定的。
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。
因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。
线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。
3.5 垃圾搜集器
搜集算法是内存回收的理论,垃圾回收器就是内存回收的具体体现。
3.5.1 Serial收集器
Serial收集器是最基本、发展历史最为悠久的收集器。在曾经的JDK1.3之前是新生代收集的唯一选择。会暂停当前线程,进行内存回收
3.5.2 ParNew收集器
Serial收集器的多线程版本,能与CMS合作。可多线程收集垃圾,收集新生代,使用收集算法
3.5.3 Parallel Scavenge收集器
多线程收集垃圾,收集新生代,使用收集算法。Parallel收集器更关注系统的吞吐量,可以通过参数来打开自适应调节策略。
3.5.4 Serial Old收集器
是Serial 收集器的老年代版本,使用多线程和“标记-整理”算法。
3.5.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。jdk1.6开始提供。
3.5.6 CMS收集器
Concurrent Mark Sweep,采用标记-清除算法,用于老年代,常与ParNew协同工作。优点在于并发收集与低停顿。
 注:并行是指同一时刻同时做多件事情,而并发是指同一时间间隔内做多件事情
3.5.7 G1收集器
优势:并行(多核CPU)与并发;
    分代收集(新生代和老年代区分不明显);
    空间整合;
    限制收集范围,可预测的停顿。
 步骤:初始标记、并发标记、最终标记和筛选回收。
1999年随JDK1.3.1一起来的是串行方式的serialGc,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布·
Parallel GC在JDK6之后成为HotSpot默认GC。
2012年,在JDK1.7u4版本中,G1可用。
2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强zGC,自动返回未用堆内存给操作系统。
2020年3月,JDK14发布。删除cMs垃圾回收器。扩展zGC在macos和Windows上的应用
3.6 内存分配与回收策略
Java体系的自动内存分配管理可以总结为给对象分配内存以及回收分配给对象的内存。
-  对象优先在Eden分配 
 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
-  大对象直接进入老年代 
 所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组.
-  长期存活的对象将进入老年代 
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
-  动态对象年龄判定 
 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
-  空间分配担保 
 下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了
 内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
堆内存分为哪几块?
 Young Generation Space 新生区(也称新生代)
 Tenure Generation Space养老区(也称旧生代)
 Permanent Space 永久存储区
Young Generation Space (采用复制算法)
 主要用来存储新创建的对象,内存较小,垃圾回收频繁。这个区又分为三个区域:一个 Eden Space 和两个 Survivor Space。
- 当对象在堆创建时,将进入年轻代的Eden Space。
- 垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space,如果B Suvivor Space已经满,则复制 Old Gen
- 扫描A Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个Old对象,则将其移到Old Gen。
- 扫描完毕后,JVM将Eden Space和A Suvivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Suvivor Space。
Tenure Generation Space(采用标记-整理算法)
 主要用来存储长时间被引用的对象。它里面存放的是经过几次在 Young Generation Space 进行扫描判断过仍存活的对象,内存较大,垃圾回收频率较小。
Permanent Space
 存储不变的类定义、字节码和常量等。
虚拟机性能监控与故障处理工具
4.2 JDK的命令行工具

- jps:虚拟机进程状况工具
 可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。
- jstat:虚拟机统计信息监视工具
 jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。
 jstat -gcutil 3344 1000 10 每隔1000毫秒执行一次 10次
- -jinfo:Java配置信息工具
 jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。
- jmap:Java内存映像工具
 jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。
- jhat:虚拟机堆转储快照分析工具
 Sun JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。
- jstack:Java堆栈跟踪工具
 jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。
在JDK 1.5中,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机
中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的
大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看
线程堆栈
- HSDIS:JIT生成代码反汇编
 HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机的源码之中,但没有提供编译后的程序。
4.3 JDK的可视化工具
JDK中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole和
 VisualVM,这两个工具是JDK的正式成员,没有被贴上“unsupported and experimental”的标签。
4.3.1 JConsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。
4.3.2 VisualVM:多合一故障处理工具

调优案例分析与实战
类文件结构
6.2 无关性的基石

6.3 Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地
 排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前 [1] 的方式分割成若干个8位字节进行存储。
6.3.1 魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件
 是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(MinorVersion),第7和第8个字节是主版本号(Major Version)。
6.3.2 常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,
 它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
6.3.3 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类
 型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
6.3.4 类索引、父类索引与接口索引集合
6.3.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以
 及实例级变量,但不包括在方法内部声明的局部变量。
虚拟机类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
7.2 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载
 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化
 (Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图7-1所示。
 
 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字
实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果入常 量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化, 则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父 类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那类),虚拟机会先初始化这个主类。
5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种场景中的行为,成为对一个类进行主动引用,除此之外,所有引用类的方式都不会被初始化,称为被动引用。
7.3 类加载的过程
-  加载 
 在加载阶段,虚拟机需要完成以下3件事情:
 1)通过一个类的全限定名来获取定义此类的二进制字节流。
 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-  验证 
 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-  准备 
 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
-  解析 
 析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
-  初始化 
 类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
 初始化是执行()方法的过程。
7.4 类加载器
类与类加载器
类加载器的作用是什么?
 类加载器实现类的加载动作,同时用于确定一个类。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等。
类加载器有哪些?
- 启动类加载器(Bootstrap ClassLoader):使用C++实现(仅限于HotSpot),是虚拟机自身的一部分。负责将存放在\lib目录中的类库加载到虚拟机中。其无法被Java程序直接引用。
- 扩展类加载器(Extention ClassLoader)由ExtClassLoader实现,负责加载\lib\ext目录中的所有类库,开发者可以直接使用。
- 应用程序类加载器(ApplicationClassLoader):由APPClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。
/**
 * 类加载器与instanceof 关键字
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = classLoader.loadClass("com.company.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof ClassLoaderTest);
    }
}
7.4.2 双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现 [1] ,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
 
 什么是双亲委派(上溯委托)模型?
 双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外,其余加载器都应当有自己的父类加载器。类加载器之间的父子关系,通过组合关系复用。
 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有到父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么要使用双亲委派模型,组织类加载器之间的关系?
 Java类随着它的类加载器一起具备了一种带优先级的层次关系。比如java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中,都是同一个类。
如果没有使用双亲委派模型,让各个类加载器自己去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
7.4.3 破坏双亲委派模型
第一次破坏:
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。
第二次破坏:
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。
如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,
 它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
第三次破坏:
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
 OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
 1)将java.*开头的类委派给父类加载器加载。
 2)否则,将委派列表名单内的类委派给父类加载器加载。
 3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
 4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
 5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
 6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
 7)否则,类加载器失败。
双亲委派模型破坏举例(JDBC)
 原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
虚拟机字节码执行引擎
所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果,本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
8.2 运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack) [1] 的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

8.2.1 局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
 局部变量没有类变量的“准备阶段”,没有赋值无法使用。
8.2.2 操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
8.2.3 动态连接
每个栈帧都包含一个指向运行时常量池 [1] 中该栈帧所属方法的引用,持有这个引用是为
 了支持方法调用过程中的动态连接(Dynamic Linking)。
8.2.4 方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到
 任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得
 到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异
 常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出
 方法的方式称为异常完成出口。
8.3 方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本
 (即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
8.3.1 解析
调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
8.3.2 分派
1.静态分派
 在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。
 静态分派的典型应用是方法重载。
2.动态分派
 由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调
 用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
 3.单分派与多分派
 4.虚拟机动态分派的实现
8.3.3 动态类型语言支持
随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言”(Dynamically Typed Language)支持而进行的改进之一。
 什么是动态类型语言 [1] ?动态类型语言的关键特征是它的类型检查的主体过程是在运行
 期而不是编译期,满足这个特征的语言有很多。相对的,在编译期就进行类型检查过程的语言(如C++和Java等)就是最常用的静态类型语
 言。
8.4 基于栈的字节码解释执行引擎
8.4.1 解释执行
Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍
 历语法树生成线性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,
 而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。
8.4.2 基于栈的指令集与基于寄存器的指令集
Java编译器输出的指令流,基本上 [1] 是一种基于栈的指令集架构(Instruction Set
 Architecture,ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。
8.4.3 基于栈的解释器执行过程
类加载及执行子系统的案例与实战
早期(编译期)优化
10.3 Java语法糖的味道
10.3.1 泛型与类型擦除
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已
 经替换.
10.3.2 自动装箱、拆箱与遍历循环
/**
 * 自动装箱的陷阱
 * @author wx
 *
 */
public class test {
	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));
	}
}
-128至127是最常用的Integer对象,会被Integer缓存。
10.3.3 条件编译
Java语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量值的真假,编
 译器将会把分支中不成立的代码块消除掉,这一工作将在编译器解除语法糖阶段
 (com.sun.tools.javac.comp.Lower类中)完成。
晚期(运行期)
11.1 概述
在部分的商用虚拟机(Sun HotSpot、IBM J9)中,Java程序最初是通过(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。
11.2 HotSpot虚拟机内的即时编译器
11.2.1 解释器与编译器
解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
11.2.2 编译对象与触发条件
上文中提到过,在运行过程中会被即时编译器编译的“热点代码”有两类,即:
 被多次调用的方法。
 被多次执行的循环体。
11.2.3 编译过程
Java内存模型与线程
12.2 硬件的效率与一致性
运算任务,除了需要处理器计算之外,还需要与内存交互,如读取运算数据、存储运算结果等(不能仅靠寄存器来解决)。
 计算机的存储设备和处理器的运算速度差了几个数量级,所以不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache),作为内存与处理器之间的缓冲:将运算需要的数据复制到缓存中,让运算快速运行。当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了。
 基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存时,可能导致各自的缓存数据不一致。
 为了解决一致性的问题,需要各个处理器访问缓存时遵循缓存一致性协议。同时为了使得处理器充分被利用,处理器可能会对输出代码进行乱序执行优化。Java虚拟机的即时编译器也有类似的指令重排序优化。
12.3 Java内存模型
Java虚拟机规范中试图定义一种Java内存模型 [1] (Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
12.3.1 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的 [3] ,不会被共享,自然就不会存在竞争问题。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝 [4] ,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量 [5] 。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图12-2所示。
 
12.3.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
 
12.3.3 对于volatile型变量的特殊规则
关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,具备两种特性:
- 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。
- 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
这句指令中的“addl $0x0,(%esp)”(把ESP寄存器的值加0)显然是一个空操作,所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
为什么使用volatile?
 在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字),但是由于虚拟机对锁实行的许多消除和优化,所以并不是很快。
volatile变量读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
12.3.4 对于long和double型变量的特殊规则
Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这4个操作的原子性,这点就是所谓的long和double的非原子性协定(Nonatomic Treatment ofdouble andlong Variables)。
12.3.5 原子性、可见性与有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的,我们逐个来看一下哪些操作实现了这3个特性。
原子性(Atomicity):对基本数据类型的访问和读写是具备原子性的。对于更大范围的原子性保证,可以使用字节码指令monitorenter和monitorexit来隐式使用lock和unlock操作。这两个字节码指令反映到Java代码中就是同步块——synchronized关键字。因此synchronized块之间的操作也具有原子性。
可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。synchronized和final也能实现可见性。final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。
有序性(Ordering):J有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了synchronized和volatile两个关键字来保证线程之间的操作的有序性。
12.3.6 先行发生(happens-before)原则
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
- 程序次序原则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:unlock解锁操作先行发生于后面对同一个锁的lock加锁操作(synchronized)
- volatile变量规则:对一个变量的写操作先行发生于后面对这个线程的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
12.4 Java与线程
什么是线程?
 线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O),又可以独立调度(线程是CPU调度的最基本单位)。
 什么是进程?
 进程是操作系统分配资源的最小单元;进程有独立的地址空间,互不影响,而线程只是进程不同的执行路径;进程的切换比线程的切换开销大;
12.4.1 线程的实现
实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
12.4.2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
12.4.3 状态转换
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别如下。
- 新建(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):已终止线程的线程状态,线程已经结束执行。

几个方法的比较
- Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
- t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
- obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
线程安全与锁优化
13.2 线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
13.2.1 Java语言中的线程安全
我们 可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
- 不可变
 Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,如果读者还没想明白这句话,不妨想一想java.lang.String类的对象,它是一个典型的不可变对象,我们调用它的substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
- 绝对线程安全
 Vector是一个线程安全的容器,如果不在方法调用端做额外的同步措施的话,仍然会不安全。
- 相对线程安全
 相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
- 线程兼容
 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。
 Java API中大部分的类都是属于线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
5.线程对立
13.2.2 线程安全的实现方法
- 互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。悲观并发策略。
 在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
 除了synchronized之外,我们还可以使用java.util.concurrent(下文称J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。
 不过,相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。
- 非阻塞同步 互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。1.5以后基于冲突检测的乐观并发策略,需要硬件指令集支持。常用的指令有:
 测试并设置(Test and Set)
 获取并增加(Fetch and Increment)
 交换(Swap)
 比较并交换(Compare and Swap
CAS 存在“ABA”问题
A→B→A 值已经被修改 ,但是实际检测没有变化。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。。
-  无同步方案 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数 据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步 措施去保证正确性,因此会有一些代码天生就是线程安全的。 可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。 线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 
13.3 锁优化
-  自旋锁与自适应自旋 
 自旋锁 :为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
 自适应自旋:自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
-  锁消除 
 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
-  锁粗化 
 如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
-  轻量级锁 
 对象头状态01尝试cas获取锁,获取成功变为00,获取失败变为10,膨胀为重量级锁。
-  偏向锁 
 对象头信息设置的另一种锁
 偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号