第一章 略
第二章 Java内存区域和内存溢出异常
运行时数据区域
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区
- 运行时常量池(方法区的一部分)
- 直接内存(并不属于数据区域,只是方便讲解)
程序计数器:
当前线程所执行的字节码的行号指示器
对于多线程来讲,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
Java虚拟机栈:
Java方法执行的内存模型,存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
本地方法栈
为虚拟机使用到的Native
方法服务,而虚拟机栈为虚拟机执行Java
方法(也就是字节码)服务
Java堆
内存中最大的一块。存放对象实例,而且是垃圾收集器管理的主要区域。
方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
运行时常量池
方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
对象创建的过程
- 类加载检查
- 分配内存,并将内存空间初始化为零值
- 对象设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等
- 执行
init
方法
OutOfMemoryError异常
- Java堆溢出:Java堆用于存储对象实例,只要不断地创建对象,并且保证
GC Roots
到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制会就会产生内存溢出异常 - 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出:经常动态生成大量Class,比如
cglib
,大量jsp
或动态产生jsp
文件等 - 本机直接内存溢出
第三章 垃圾回收器与内存分配策略
概述:GC关注的主要是Java堆和方法区的内存
引用计数算法
主流虚拟机不选用该方法来管理内存。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
缺点:很难解决对象之间相互循环引用的问题。
可达性分析算法
定义:通过一系列的称为“GC Roots
”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots
没有任何引用链时,则证明此对象是不可用的。
GC Roots
的对象包括以下几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2. 方法区中类静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI
(即一般说的Native
方法)引用的对象
方法区的主要回收两部分内容:废弃常量和无用的类
垃圾收集方法
- 标记-清理算法
- 复制算法:
Eden
和Survivor
比例为8:1 - 标记-整理算法
- 分代收集算法:新生代使用复制算法,老生代使用标记-清理或者标记-整理算法
垃圾收集器:CMS和G1
回收策略
Minor GC
和Major GC
的区别:前者发生在新生代,后者发生在老生代- 大对象(很长的字符串以及数组)直接进入老生代
- 长期存活的对象将进入老生代
- 动态对象年龄判定
- 空间分配担保
第四章 虚拟机性能监控与故障处理工具
JDK主要命令行监控工具:
- jps:显示指定系统内所有的HotSpot虚拟机进程
- jstat:用于收集HotSpot虚拟机各方面的运行数据
- jinfo:显示虚拟机配置信息
- jmap:生成虚拟机内存转储快照(headdump文件)
- jhat:用于分析headdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
- jstack:显示虚拟机的线程快照
JDK可视化工具
- JConsole:基于JMX的可视化监视、管理工具
- VisualVM:多合一故障处理工具
第五章 调优案例分析与实战
略
第六章 类文件结构
Class类文件的结构
两种数据类型:
- 无符号数:用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,以”_info”结尾
Class文件
- 前4个字节:0xCAFEBABE
- 第5和第6个字节:次版本号
- 第7和第8个字节是主版本号
- 常量池
- 访问标志:用于识别一些类或者接口层次的访问信息
- 类索引、父类索引与接口索引集合
- 字段表集合:用于描述接口或者类中声明的变量
- 方法表集合
- 属性表集合(Class文件、字段表、方法表中都可以携带)
常量池主要存放两大类常量:
1. 字面量
- 文本字符串
- 声明为final
的常量值
2. 符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Oprands)而构成。
注意:大部分的指令没有支持整数类型byte
、char
和short
,和boolean
,编译期会在编译期或运行期将byte
和short
类型的数据带符号扩展为相应的int
类型数据,将boolean
和char
类型数据零位扩展为相应的int
类型数据。
第七章 虚拟机类加载机制
概念:
类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
注意:类型的加载、连接和初始化过程都是在程序运行期间完成的。
类的生命周期:总共7个阶段,如图
必须立即对类进行初始化的有且仅有的5种情况:
1. 使用new
实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法
2. 对类进行反射调用
3. 当初始化一个类的时候,其父类还没被初始化的时候
4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()
方法的那个类)
5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化
类加载的过程:
- 加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
- 验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- 准备:正式为类变量分配内存并设置类变量初始值的阶段,内存都将在方法区中进行分配
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化:执行类构造器
<clinit>()
方法的过程(包括类变量的赋值和静态语句块)
类加载器
注意:比较两个类是否“相等”,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派模型
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
第八章 虚拟机字节码执行引擎
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构。如下图所示
- 局部变量表:一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
- 操作数栈:后入先出(LIFO)
- 动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
- 方法返回地址
方法调用
- 解析:在编译期间确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用。
- 分派
- 动态类型语言支持:JDK 7新增
invokedynamic
字节码指令
第九章 类加载及执行子系统的案例与实战
略
第十章 早期(编译期)优化
三类编译器
- 前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
- JIT编译器(后端编译器):HotSpot VM的C1、C2编译器
- AOT编译器:GNU Compiler for the Java(GCJ)、Excelsior JET
Javac编译过程
- 编译与填充符号过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生成过程
Java语法糖的味道
- 泛型和类型擦除:Java中的泛型实现方法称为类型擦除,也称伪泛型
- 自动装箱、拆箱与遍历循环
- 条件编译:使用条件为常量的if语句
第十一章 晚期(运行期)优化
概述:
即时编译器:
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。
第12章 Java内存模型与线程
Java内存模型
线程、主内存、工作内存三者的交互关系:
内存间交互操作
- lock(锁定)
- unlock(解锁)
- read(读取)
- load(载入)
- use(使用)
- assign(赋值)
- store(存储)
- write(写入)
volatile关键字
两种特性:
1. 保证此变量对所有线程具有可见性
2. 禁止指令重排序优化
注意:volatile
保证此变量对所有线程具有可见性,但是变量的运算在并发下一样是不安全的
使用场景:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
三种特性
原子性
- 基本数据类型的访问读写具有原子性(
long
和double
除外) - 使用
lock
和unlock
满足更大范围的原子性,虚拟机提供了monitorenter
和moniterexit
字节码指令,反应到Java代码中就是同步块——synchronized
关键字
- 基本数据类型的访问读写具有原子性(
可见性
- 通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性
volatile
、synchronized
和final
都能实现可见性
有序性
- 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
- 前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”的现象。
先行发生原则
指的是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
线程
线程的实现
- 使用内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级线程混合实现
- Java线程的实现
Java线程调度
- 协同式线程调度:线程的执行时间由线程本身来控制
- 抢占式线程调度:由系统来分配执行时间(Java使用的方式)
状态转换
- 新建(New)
- 运行(Runnable)
- 无限期等待(Waiting):需要等待被其他线程显式地唤醒
- 限期等待(Timed Waiting):一定时间后会被系统自动唤醒
- 阻塞(Blocked)
- 结束(Terminated)
第13章 线程安全与锁优化
各种操作共享的五类数据
- 不可变:基本数据类型声明为
final
即可;将对象中带有状态的变量都声明为final
- 绝对线程安全:不管运行环境如何,调用者都不需要任何额外的同步措施
- 相对线程安全:对于一些特定顺序的连续调用,可能需要在调用端使用额外的同步手段来保证调用的正确性。例如
Vector
、HashTable
、Collections
的synchronizedCollection()
方法包装的集合。 - 线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。例如
ArrayList
和HashMap
。 - 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。例如
Thread
类的suspend()
和resume()
方法。
线程安全的实现方法
- 互斥同步:
synchronized
关键字和ReentrantLock
ReentrantLock
增加的三个功能:
- 等待可中断
- 可使用公平锁
- 锁绑定多个条件
- 非阻塞同步:CAS操作,例如J.U.C包里面的整数原子类,其中的
compareAndSet()
和getAndIncrement()
等方法都是用了Unsafe
类的CAS操作 - 无同步方案:如果一个方法本来就不涉及共享数据,就无须任何同步措施
- 可重入代码
- 线程本地存储:如果共享数据的代码能保证在同一个线程中执行,就可以把共享数据的可见范围限制在同一个线程之内。通过
java.lang.ThreadLocal
类来实现线程本地存储的功能。
锁优化
- 自旋锁与自适应锁:让后面请求锁的线程等待,执行一个忙循环(自旋)。避免了线程切换的开销,但是只适用于锁被占用时间很短的情况。
- 锁消除:指的是虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。例如对
String
字符串的拼接操作。 - 锁粗化:如果一系列的连续操作都是对同一个对象进行加锁和解锁,就会把加锁同步的范围扩展到整个操作序列的外部。
- 轻量级锁:在无竞争的情况下使用CAS操作去消除同步使用的互斥量。
- 偏向锁:在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。