深入理解Java虚拟机
2019-08-21 14:00:34 3 举报
AI智能生成
登录查看完整内容
《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记。把图看完,书基本上不用看了。
作者其他创作
大纲/内容
深入理解Java虚拟机
四、程序编译与代码优化
10、早期(编译期)优化
10.1、概述
编译期是一个不确定的过程
1、前端编译器,把*.java转成*.class
sun的Javac
Eclipse JDT中的增量式编译器(ECJ)
对代码运行效率没有任何优化措施
只是提供了语法糖提升编程便利性
2、虚拟机的后端运行期编译器(JIT),把字节码转成机器码
HotSpot VM的C1、C2编译器
3、使用静态提前编译器(AOT)直接把*.java编译成本地机器码
GNU Compiler for the Java(GCJ)、Excelsior JET
10.2、Javac编译器
10.2.1、Javac的源码与调试
10.2.2、解析与填充符合表
1、词法、语法分析
词法分析,是将源代码的字符流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,标记是编译过程的最小元素,关键字、变量名、字面量、运算符都可以是标记
语法分析,根据Token序列构造抽象语法树的过程,抽象语法树是用来描述程序代码语法结构的树形表示方式,一个节点代表程序代码中的一个语法结构
2、填充符号表
符号表,一组符号地址和符号信息构成的表格,k-v形式
10.2.3、注解处理器
10.2.4、语义分析与字节码生成
语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查
1、标注检查
变量使用前是否被声明、变量与赋值之间的数据类型是否匹配
常量折叠
int a=1+2
int a=3
2、数据及控制流分析
对程序上下文逻辑更进一步的验证
和类加载时的数据及控制流分析的目的基本一致,但校验范围不同
如,局部变量声明为final,对运行期没有影响,变量的不变形仅由编译器在编译期间保障
3、解语法糖
语法糖,计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用
增加程序可读性,从而减少代码出错的机会
包括:泛型、变长参数、自动装箱/拆箱
4、字节码生成
进行少量代码添加、转换,将源码转换成字节码
生成构造器
将字符串的加操作替换为StringBuffer、StringBuilder的append()
10.3、Java语法糖的味道
10.3.1、泛型与类型擦除
方法重载要求方法具备不同的特征签名,返回值不包含在方法的特征签名之中,所以返回值不参与重载选择
但是,Class文件格式中,只要描述符不是完全一致的两个方法就可以共存
擦除,仅仅是方法的Code属性中的字节码进行擦除,实际上元数据中保留了泛型的信息,这是能够通过反射取得参数化类型的根本依据
10.3.2、自动装箱、拆箱与遍历循环
包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,equals()方法不处理数据转型的关系
10.3.3、条件编译
只有使用条件为常量的if语句才能触发条件编译
方法体内部,根据布尔值的真假,把分支中不成立的代码块消除
10.4、实战:插入式注解处理器
类似CheckStyle实现
11、晚期(运行期)优化
11.1、概述
即时编译器
Java最初是解释执行的,当虚拟机发现某个方法或代码块运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提升执行效率,运行时,虚拟机会把这些代码编译成本地平台相关的机器码,并进行各层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,JIT)
JVM规范对这块没有约束
11.2、HotSpot虚拟机内的即时编译器
疑问
为何HotSpot虚拟机要使用解释器和编译器并存的架构?
为何HotSpot虚拟机要实现两个不同的即时编译器?
程序何时使用解释器执行?何时使用编译器执行?
哪些程序代码会被编译为本地代码?如何编译为本地代码?
如何从外部观察即时编译器的编译过程和编译结果?
11.2.1、解释器与编译器
优劣
程序需要迅速启动和执行时,解释器首先发挥作用,省去编译时间,立即执行
程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,从而获取更高的执行效率
解释执行可以节约内存,编译执行可以提升效率
两个即时编译器
Client Compiler(C1)
获取更高的编译速度
Server Compiler(C2)
获取更高的编译质量
分层编译策略
1.6出现,1.7默认,为了在程序启动响应速度和运行效率之间达到平衡
第0层,解释执行,不开启性能监控,可触发第1层编译
第1层,也叫C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要,会加入性能监控
第2层,也叫C2编译,将字节码编译为本地代码,会启用一些编译耗时较长的优化,甚至会根据性能监控进行一些不可靠的激进优化
11.2.2、编译对象与触发条件
热点代码
被多次调用的方法
由方法调用触发的编译
编译器以整个方法作为编译对象
标准的JIT编译方式
被多次执行的循环体
编译动作由循环体触发
编译器仍然以整个方法作为编译对象
编译发生在方法执行过程中,这种编译称为“栈上替换”(On Stack Replacement,OSR编译,方法栈帧还在栈上,方法就被替换了)
什么是热点代码?
判定代码是不是热点代码,需不需要触发即时编译,这样的行为称为“热点探测”(Hot Spot Detection)
热点探测方式
基于采样的热点探测
周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那这个方法就是热点方法
简单、高效,容易获取方法调用关系
很难精确地确认一个方法的热度,容易受到线程阻塞或其他因素的影响
基于计数器的热点探测(HotSpot采用)
为每个方法建立计数器,统计方法的执行次数,超过阈值就认为是热点代码
HotSpot基于计算器的热点探测方法
方法调用计数器
方法调用计算器热度的衰减(方法计数默认统计的不是方法调用的绝对次数,而是一个相对执行频率)
一段时间内方法被调用的次数
超过一定时间限度,方法调用计数就会减半
可以使用-XX:UseCounterDecay来关闭热度衰减,统计绝对次数,保证程序运行最后都会被编译为本地代码
-XX:CounterHalfLifeTime,半衰周期,单位是秒
回边计数器
统计一个方法中循环体代码执行的次数
字节码遇到控制流向后跳转的指令称为“回边”
通过-XX:OnStackReplacePercentage间接调整
流程
虚拟机遇到回边指令时,先查找将要执行的代码片段是否有编译好的版本
有,就执行
否则,就+1,然后判断方法调用计数器与回边计数器的和是否超过回边阈值
如果超过阈值,将提交一个OSR编译请求,并且把回边计数器的值降低一些,以便继续在解释器中执行循环,等待编译器输出结果
调用次数阈值通过-XX:CompileThreshold设定,Client默认1500,Server默认10000
11.2.3、编译过程
默认设置下,编译过程是在后台执行的,当编译未完成时,虚拟机仍然按照解释方式执行,通过-XX:-BackgroundCompilation禁止后台编译
Client Compiler
简单快速的三段式编译器
第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR)。在此之前,编译器会在字节码上完成一部分基础优化,如:方法内联、常量传播
HIR使用静态单分配的形式来代表代码值,可以使得一些在HIR的构造过程之中和之后进行的优化动作更易实现
第二阶段,一个平台相关的后端从HIR中产生低级中间代码表示(LIR)。在此之前,会在HIR上完成一些优化,如:空值检查消除、范围检查消除等
第三阶段,在平台相关的后端使用线性扫描算法在LIR上分配寄存器,并在LIR上做窥孔优化,然后产生机器代码
Server Compiler
专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器
会执行所有经典优化动作,如:无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等
还会实施一些与Java特性相关的优化,如:范围检查消除、空值检查消除
还可能根据解释器或Client Compiler提供的性能监控信息,做一些不稳定的激进优化,如:守护内联、分支频率预测等
11.2.4、查看及分析即时编译结果
需要Debug或FastDebg版虚拟机
-XX:+PrintCompilation,使得虚拟机在即时编译时将被编译成本地代码的方法名打出来
-XX:+PrintInlining,输出方法内联信息
11.3、编译优化技术
1.3之后,虚拟机设计团队的所有代码优化都集中在即时编译器之中
11.3.1、优化技术概览
例子
方法内联
冗余访问消除
复写传播
无用代码消除
语言无关的经典优化技术
公共子表达式消除
语言相关的经典优化技术
数组范围检查消除
最重要的优化技术
最前沿的优化技术
逃逸分析
11.3.2、公共子表达式消除
含义:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式
初始:int d = (c*b)*12 + a + (a+b*c)
即时编译:int d = E*12 + a + (a+E)
进一步代数化简:int d = E*13 + a*2
11.3.3、数组边界检查消除
思路1:尽可能把运行期检查提到编译期完成
思路2:隐式异常处理
先裸奔,出事之后再转换
根据运行期收集到的Profile信息选择是否如此
11.3.4、方法内联
消除方法调用成本,为其他优化手段打基础
激进优化
引进:类型继承关系分析
11.3.5、逃逸分析
当一个对象在方法中被定义后,可能被外部方法引用,如作为调用参数传递到其他方法中,称为方法逃逸
甚至可能被外部线程访问到,称为线程逃逸
如果没有逃逸,可以进行高效优化
栈上分配
将对象分配在栈上内存,空间随栈帧出栈销毁
同步消除
变量不会发生线程逃逸,就不会有竞争,对应的同步措施可以消除掉
标量替换
标量,一个数据已经无法再分解成更小数据来表示,如原始数据
变量可以在栈上分配和读写
可以分解的数据称为聚合量
将对象拆散,根据程序访问的情况,将其使用到的成员变量恢复成原始类型来访问,就叫标量替换
使用-XX:+DoEscapeAnalysis开启逃逸分析,使用-XX:+PrintEscapeAnalysis查看分析结果,使用-XX:+EliminateAlloations开启标量替换,使用-XX:+EliminateLocks开启同步消除,使用-XX:+PrintEliminateAllocations查看标量替换情况
11.4、Java与C/C++的编译器对比
即时编译器和静态编译器的对比
即时编译器运行占用的是用户程序的运行时间,具有时间压力,优化手段受制于编译成本
Java语言是动态的类型安全语言,需要由虚拟机确保程序不违反语言语义或访问非结构化内存
即虚拟机必须频繁进行动态检查
如实例方法访问时检查空指针
数组元素访问时上下界检查
Java中使用虚方法频率较大
Java语言可以动态扩展,编译器需要随着类型变化而在运行时撤销或重新优化
需要垃圾回收
五、高效并发
12、Java内存模型与线程
12.1、概述
尽力压榨处理器的运算能力
12.2、硬件的效率与一致性
大部分运算任务都不可能只依赖处理器计算就能完成,至少需要与内存交互
CPU和内存之间增加高速缓存
引入缓存一致性问题
内存模型
在特定操作协议下,对特定内存或高速缓存进行读写访问的过程抽象
12.3、Java内存模型
屏蔽硬件和操作系统的内存访问差异,使得Java程序在各平台下达到一致的内存访问效果
12.3.1、主内存与工作内存
JMM主要目标:定义程序中各个变量的访问规则
所有变量存储在主内存
类似于物理机的内存
每条线程有自己的工作内存
类似于物理机的高速缓存
线程对变量的所有操作(读取、赋值)都必须在工作内存进行,不能直接读写主内存中的变量
不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成
12.3.2、内存间交互操作
8种原子的、不可再分的
lock(锁定)
作用于主内存变量
它把一个变量标识为一条线程独占的状态
unlock(解锁)
它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取)
它把一个变量的值从主内存传输到线程的工作内存中,以便后续的load动作使用
load(载入)
作用于工作内存变量
它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用)
它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时会执行这个操作
assign(赋值)
它把一个从执行引擎接收到的值赋给工作内存的变量
store(存储)
它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入)
它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM要求
把一个变量从主内存复制到工作内存
顺序地执行read和load操作
把变量从工作内存同步会主内存
顺序地执行store和write操作
12.3.3、对于volatile型变量的特殊规则
关键字volatile是java提供的最轻量级同步机制
修改的新值立即同步到主内存
每次使用前立即从主内存刷新
两个特性
1、保证此变量对所有线程的可见性
一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
2、禁止指令重排序优化
普通变量无法保证变量赋值操作的顺序与程序代码中的执行顺序一致
因为“线程内表现为串行的语义”,所以单线程的时候指令重排不会影响程序执行结果
12.3.4、对于long和double型变量的特殊规则
虚拟机规范没有强制这类操作实现为原子性,但是强烈建议实现为原子性
商业虚拟机目前都实现为原子性
12.3.5、原子性、可见性与有序性
JMM是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特性来建立的
原子性
通过synchronized来实现monitorenter和monitorexit的操作
可见性
一个线程修改了共享变量的值,其他线程能够立即得知这个修改
JMM实现:在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现的
普通变量和volatile变量都是这样
volatile的特殊规则保证新值能立即同步到主内存,以及每次使用前立即从主内存刷新
实现可见性的还有
synchronized
通过对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store、write操作),实现的
final
被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去,那在其他线程中就能看到final字段的值
有序性
天然的有序性
如果在本线程内观察,所有的操作都是有序的
线程内表现为串行的语义
如果在一个线程中观察另一个线程,所有的操作都是无序的
指令重排现象和工作内存与主内存同步延迟现象
Java提供了volatile和synchronized两个关键字保证线程之间的有序性
volatile
禁止指令重排
一个变量同一个时刻只允许一条线程对其进行lock操作
12.3.6、先行发生原则
先行发生是JMM中定义的两项操作之间的偏序关系
如果操作A先行发生于操作B,那么操作A产生的影响能被操作B观察到
天然的“先行发生关系”
程序次序规则
一个线程内,按照程序代码顺序,写在前面的先行发生于写在后面的
管程锁定规则
一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量规则
对一个volatile变量的写操作先行发生于后面(时间先后)对这个变量的读操作
线程启动规则
Thread对象的start()方法先行发生于此线程的每一个动作
线程终止规则
线程中的所有操作都先行发生于对此线程的终止检测
线程中断规则
对线程interrupte()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C
时间上的先发生不等于先行发生
先行发生也不等于时间上的先发生
如指令重排序
12.4、Java与线程
概述
并发不一定要依赖多线程,如多进程。但是Java的并发依赖多线程
12.4.1、线程的实现
线程是CPU调度的基本单位
实现线程的3种方式
1、使用内核线程实现
由操作系统内核支持的线程
线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并映射到各个处理器上
创建数量受限制
使用内核线程的一种高级接口-轻量级进程(熟知的进程)实现的
2、使用用户线程实现
优势:不需要系统内核支持
劣势:所有线程操纵都需要用户程序自己处理,具体实现较复杂
3、使用用户线程加轻量级进程混合实现
用户线程完全建立在用户空间,线程的创建、切换操纵成本低,且支持大规模用户线程的并发
可以实现内核提供的线程调度功能和处理器映射,用户线程的系统调用通过轻量级线程来完成,大大降低了整个进程完全阻塞的风险
4、Java线程的实现
实现依赖于操作系统支持怎样的线程模型
12.4.2、Java线程调度
线程调度是指系统为线程分配处理器使用权的过程
两种调度方式
协同式线程调度
线程执行时间由线程本身来控制
实现简单,没有线程同步问题
但是,线程执行时间不可控制
抢占式线程调度
线程执行时间由系统来分配,线程切换由系统决定
Java采用的
Java中可以主动让出执行时间,(Thread.yield()),但是无法主动获取执行时间
程序可以“建议”系统给某些线程多分配执行时间,某些线程少分配执行时间
通过设置线程优先级来完成
但是不靠谱,因为Java线程是映射到系统的原生线程上实现的,系统的线程优先级不一定和Java优先级一一对应
而且,优先级还可能被系统改变
Windows系统中有“优先级推进器”,如果一个线程执行得特别“勤奋努力”,可能会为它分配越多执行时间
12.4.3、状态转换
线程的5种状态
新建(New)
创建后尚未启动的线程处于这个状态
运行(Runable)
包含操作系统线程状态的Running和Ready
无限期等待(Waiting)
线程不会被分配CPU执行时间,需要等待其他线程显式地唤醒
3个方法会让线程进入此状态
未设置Timeout参数的Object.wait()方法
未设置Timeout参数的Thread.join()方法
LockSupport.park()方法
限期等待(Thread Waiting)
线程不会被分配CPU执行时间,但是不需要等待其他线程的显式唤醒,在一定时间之后会由系统自动唤醒
5种方法让线程进入此状态
Thread.sleep()方法
设置了Timeout参数的Object.wait()方法
设置了Timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法
阻塞(Blocked)
线程被阻塞了,和等待的区别是,阻塞状态在等待着获取到一个排他锁
结束(Terminated)
线程已经结束执行
13、线程安全与锁优化
13.1、概述
虚拟机如何实现“高效”
虚拟机对我们写的并发代码提供了什么样的优化手段
面向过程
把数据和工程分开考虑
数据代表问题空间中的客体
代码用于处理这些数据
面向对象
站在现实世界的角度抽象和解决问题
把数据和行为都看做是对象的一部分
高效并发
首先保证并发的正确性
然后在此基础上实现高效
13.2、线程安全
定义
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
13.2.1、Java语言中的线程安全
讨论线程安全的前提是,多个线程之间存在共享数据访问
Java中操作共享数据分类
1、不可变
不可变的对象一定是线程安全的
2、绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施
3、相对线程安全
通常所讲的线程安全
如Vector、HashTable
4、线程兼容
对象本身不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中的安全使用
如ArrayList、HashMap
5、线程对立
无论调用端是否采取同步措施,都无法多线程环境中并发使用的代码
如System.setIn()、System.setOut()和System.runFinalizersOnExit()
13.2.2、线程安全的实现方法
1、互斥同步
同步指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用
互斥是实现同步的一种手段
互斥是因,同步是果;互斥是方法,同步是目的
Java中基本的手段是synchronized,synchronized经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令
这两个指令需要一个reference类型的参数指明要锁定和解锁的对象
synchronized对同一条线程来说是可重入的
还可以通过重入锁ReentrantLock实现同步
相比synchronized多了几个高级特性
特性
等待可中断
当持有锁的线程长期不释放时,正在等待的线程可以放弃等待
可实现公平锁
多个线程等待同一个锁时,须按照申请锁的时间顺序依次获得锁
synchronized是非公平锁,ReentrantLock默认也是非公平的,但是可以设置
锁可以绑定多个条件
一个ReentrantLock对象可以同时绑定多个Condition对象
1.6之后,synchronized和ReentrantLock的性能基本一样
2、非阻塞同步
阻塞同步
互斥同步最主要问题是进行线程阻塞和唤醒所带来的性能问题
先操作,有冲突再采取其他补偿措施(如不断重试)。乐观的并发策略可以不把线程挂起,因此称为非阻塞同步
硬件保证一个从语义上看需要多次操作的行为只通过一条处理器指令就能完成
常用指令
测试并设置
获取并增加
交换
比较并交换
3个参数
内存位置
旧的预期值
新值
无论是否更新值,都会返回该内存位置旧值
加载链接/条件存储
3、无同步方案
保证线程安全,不是一定要进行同步
同步只是保证共享数据争用时的正确性手段
可重入代码
不依赖堆上的数据和共用的系统资源、用到的状态量都由参赛数中传入、不调用非可重入方法
线程本地存储
ThreadLocalStorage
java.lang.ThreadLocal
实现线程本地存储的功能
每一个线程的Thread对象中都有一个ThreadLocalMap对象
13.3、锁优化
13.3.1、自旋锁与自适应自旋
出现自旋锁的背景
挂起和恢复线程的操作需要转入内核态完成,这些操作给系统并发性能带来较大压力
共享数据的锁状态通常只会持续很短时间,为了这段时间去挂起和恢复线程不值得
基于以上背景,所以想让线程等待一会,又不放弃CPU执行时间,看看锁是否会被很快释放。为了让线程等待,让线程执行一个忙循环(自旋),这叫做自旋锁
自旋锁默认开启,默认次数是10,使用-XX:PreBlockSpin修改
1.6引入了自适应自旋锁
自旋时间不固定,根据上次在同一锁的自旋时间和锁的拥有者的状态决定等待时间
13.3.2、锁消除
在虚拟机即时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
判断依据来源于逃逸分析技术
13.3.3、锁粗化
如果有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部
13.3.4、轻量级锁
HotSpot虚拟机的对象头里的Mark Word
轻量级锁能提升程序同步性能的依据是“对于大部分的锁,在整个同步周期内是不存在竞争的”
如果有两条以上的线程竞争同一个锁,轻量级锁就会膨胀为重量级锁
目的:在无竞争的情况下使用CAS操作消除同步使用的互斥量
13.3.5、偏向锁
在无竞争的情况下把整个同步都消除掉
通过-XX:-UseBiasedLocking禁止偏向锁优化
六、附录
编译jdk
虚拟机字节码指令表
HotSpot虚拟机主要参数表
虚拟机有许多非稳定参数,即以-XX:开头的参数
使用-XX:+PrintFlagsFinal可以输出所有参数的名称及默认值
默认不包括Diagnostic和Experimental的参数
可以配合-XX:+UnlockDiagnosticVMOptions/-XX:UnlockExperimentalVMOptions使用
参数使用方式
-XX:+<option>开启option参数
-XX:-<option>关闭option参数
-XX:<option>=<value>将optioin参数的值设为value
对象查询语言
水木桶整理
一、走近Java
Java技术体系
Java发展史
虚拟机发展史
展望
模块化
OSGi
混合语言
基于JVM的语言
多核并行
Fork/Join
利用多核进行计算
类似CUDA的异构计算
语法更丰富
Lambda表达式
64位虚拟机
自己编译jdk
二、自动化内存管理机制
2、Java内存区域与内存溢出异常
2.2、运行时数据区域
程序计数器
当前线程执行字节码的行号指示器
如果执行的是Native方法,则计算器值是空
线程私有
唯一没有规定OOM的区域
Java虚拟机栈
生命周期和线程相同
每个方法在执行时都会创建一个栈帧
存储局部变量表、操作数栈、动态链接、方法出口等信息
内存溢出
StackOverflowError
线程请求栈深度大于虚拟机允许的深度
OutOfMemoryError
扩展时无法申请到足够内存
本地方法栈
为使用到的Native方法服务
异常和Java虚拟机栈类似
Java堆
Java虚拟机所管理的内存中最大的一块
被所有线程共享
用于存储对象实例
细分
新生代
Eden
From Survivor
To Survivor
老年代
方法区
线程共享
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
这个区域的内存回收主要针对:常量池和类型的卸载
运行时常量池
属于方法区的一部分
常量可以在运行时产生
直接内存
不属于虚拟机运行时数据区的一部分
会产生OOM
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
可以避免数据在Java堆和Native堆的来回复制,从而提高性能
默认与堆内存大小一致
通过MaxDirectMemorySize设置
2.3、HotSpot虚拟机对象探秘
2.3.1、对象的创建
如果内存规整
指针碰撞分配
如果内存不规整
虚拟机需要维护一个列表
空闲列表分配
堆是否规整是由垃圾收集器是否带有压缩整理功能决定的
分配内存时可能存在并发
采用CAS配上失败重试保证更新的原子性
分配新块时再同步锁定
分配内存时内存空间初始化为0,所以对象实例字段在不赋初始值时可以直接使用
对象头设置
2.3.2、对象的内存布局
对象头
包括两部分信息
对象自身运行时数据
哈希码
gc分代年龄
锁状态标志
线程持有的锁
偏向线程id
偏向时间戳
类型指针
如果是数组,头部还存放数组长度
实例数据
前提:相同宽度的字段总是被分配在一起
父类中定义的变量会出现在子类之前
如果CompactFields参数为true(默认为true)
子类的窄变量也可能插入到父类变量的空隙
对齐填充
占位符,不是必然存在
因为HotSpot VM要求对象起始地址必须是8字节的整数倍
也就是对象大小必须是8字节的整数倍
因为对象头正好是8字节的整数倍
2.3.3、对象的访问定位
建立对象是为了使用它
通过栈上的reference访问
对象访问方式主要有两种
句柄
实现
Java堆中将划出一块内存作为句柄池
reference中存储的是对象的句柄地址
句柄中包含对象实例数据和类型数据各自的具体地址信息
好处
reference中存储的是稳定的句柄地址
对象被移动时只需改变句柄中的实例数据指针即可
指针(HotSpot采用)
reference中存储的是对象地址
Java堆对象需要考虑如何放置访问类型数据的相关信息
速度快,节省了一次指针定位
2.4、实战OOM异常
3、垃圾收集器与内存分配策略
3.1、概述
哪些内存需要回收
程序计数器、虚拟机栈、本地方法栈随线程而生、而灭,内存回收具备确定性
Java堆、方法区不同
什么时候回收
如何回收
3.2、对象已死?
3.2.1、引用计数算法
优点
1、实现简单
2、判定效率高
缺点
无法解决循环引用
总结
Java虚拟机没有用过
3.2.2、可达性分析
通过可达性分析判定对象是否存活。
通过GC Roots遍历搜索
GC Roots
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
常量引用的对象,是啥意思?类变量
本地方法栈中JNI引用的对象
3.2.3、再谈引用
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,则这块内存就代表引用
引用分类
强引用
普遍存在的引用
软引用
有用但并非必需的对象
在系统要发生内存溢出异常之前,将会把这些对象列到回收范围之中进行第二次回收
通过SoftReference类实现
弱引用
关联的对象只能生存到下一次垃圾收集发生之前
通过WeakReference实现
虚引用
又叫幽灵引用、幻影引用
无法通过虚引用来取得一个对象的实例
设置虚引用的唯一目的
这个对象被收集器回收时收到一个系统通知
通过PhantomReference实现
3.2.4、生存还是死亡
经过可达性分析不可达,不一定“非死不可”
暂时处于“缓刑”
一个对象死亡,至少经历两次标记过程
没有与GC Roots相连
被第一次标记
并且进行一次筛选
筛选的条件是此对象是否有必要执行finalize()
如果对象没有覆盖finalize()
如果该方法已经被调用过
则视为“没有必要执行”
一个对象的finalize()方法只会被系统执行一次
如果有必要执行,则进入F-Queue队列
等待虚拟机的低优先级线程Finalizer执行
不承诺等待运行结束
如果对象在finalize()中拯救了自己,即重新与引用链上的对象建立关联
在第二次标记时则逃出“即将回收”的集合
如果未拯救,则被回收
3.2.5、回收方法区
主要回收部分
废弃常量
和回收堆类似
无用的类
必备条件
该类所有的实例都已经被回收,即堆中不存在该类的实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
3.3、垃圾收集算法
3.3.1、标记-清除算法
算法分为标记和清除两个阶段
首先标记处所有需要回收的对象
标记完成后,统一回收所有被标记的对象
不足
效率问题
标记-清除过程效率不高
空间问题
标记清除后产生不连续的内存碎片,不利于后期产生较大对象时的连续空间分配,提前触发另一次垃圾收集
3.3.2、复制算法
为了解决效率问题,将内存分为两块,来回复制存活的对象
优缺点
实现简单、运行高效
内存使用率只有50%
常用来回收朝生夕死的新生代
依赖内存的分配担保
如果新的Survivor空间没有足够空间,则将对象分配到老年代
3.3.3、标记-整理算法
标记过程和“标记-清除”一样
针对老年代提出的
标记完成后,让所有存活对象移向一端,然后直接清理掉端边界以外的内存
3.3.4、分代收集算法
当前商业虚拟机采用的回收算法
把Java堆分为新生代和老年代,根据各个年代特点采用适当的收集算法
一般情况
新生代(依赖分配担保)
复制算法
老年代(无额外空间进行分配担保)
标记-清理
标记-整理
3.4、HotSpot的算法实现(虚拟机如何发起内存回收)
3.4.1、枚举根节点
枚举根节点会导致所有Java执行线程停顿
分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间整个执行系统像被冻结在某个时间点
如果分析过程中对象引用关系在不断变化,则分析结果的准确性无法保证
准确式内存管理
虚拟机可以知道内存中某个位置的数据的具体类型(到底是引用还是整数)
执行系统停顿时,不需要遍历检查所有执行上下文和全局引用位置。虚拟机可以直接得知哪些地方存放着对象引用
使用一组OopMap来管理
Ordinary Object Pointer
3.4.2、安全点
GC可能导致引用变化
或者OopMap内存变化指令非常多
所以HotSpot不能为每条指令生成OopMap,只在特定位置记录这些信息
程序执行时,并不是所有地方都能停顿下来开始GC,只有在到达安全点才能暂停
安全点根据“是否具有让程序长时间执行的特征”为标准
长时间执行:指令序列复用
方法调用
循环跳转
异常跳转
如何在GC发生时所有检查都跑到最近的安全点停顿下来
抢先式中断
首先中断所有线程
如果还有线程不在安全点,就恢复线程,让线程跑到安全点
目前没有虚拟机采用了
主动式中断
设置一个标志
各个线程主动轮询这个标志
如果中断标志为真时就自己中断挂起
轮询标志的地方和安全点是重合的
所有线程都停顿,那发生时岂不是很恐怖?
3.4.3、安全区域
安全点保证了线程在执行时会进入安全点,但是,当线程不执行(处于sleep或者Blocked状态)时,如何进入安全点?JVM不太可能等待线程重新分配cpu时间
安全区域指在一段代码片段中,引用关系不会发生变化
这个区域任意地方开始GC都是安全的
可以看作是被扩展了的安全点
线程进入安全区域时,标识自己进入了安全区域。此期间可以进行GC。线程离开安全区域时,要检查系统是否已经完成根节点枚举(或者整个GC过程),只有完成之后线程才能继续执行
3.5、垃圾收集器(虚拟机如何具体进行内存回收动作)
观点
目前还没有最好的收集器
也没有万能的收集器
只有对具体应用最合适的收集器
3.5.1、Serial收集器
单线程、复制算法收集器
收集时必须暂停其他所有的工作线程
简单、高效
Client模型下默认的新生代收集器
3.5.2、ParNew收集器
Serial收集器的多线程版本
Server模型下虚拟机中首选的新生代收集器
能与CMS配合工作
并发与并行
并行:多条垃圾收集线程并行工作,但此时用户线程处于等待状态
并发:用户线程与垃圾收集器同时执行,用户程序在继续运行,垃圾收集程序运行于另一个CPU上
3.5.3、Parallel Scavenge收集器
新生代收集器,使用复制算法收集,并行的
目标不同:达到一个可控制的吞吐量
CMS关注:尽可能地缩短垃圾收集时用户线程的停顿时间
具备GC自适应调节策略
开关控制自动调节各个区的大小、晋升老年代对象年龄
3.5.4、Serial Old收集器
老年代、单线程、标记-整理收集、Client模式下使用
3.5.5、Parallel Old收集器
老年代、多线程、标记-整理算法
为什么有些收集器不能混用?
能够混用是基于一个分代式GC框架
不基于这个框架就不能混用
3.5.6、CMS收集器
目标:获取最短回收停顿时间为目标
基于标记-清除
初始标记
会导致STW
只标记一下GC Roots能直接关联到的对象
并发标记
进行GC RootsTracing
重新标记
修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
并发清除
并发收集、低停顿
对CPU资源非常敏感
多线程并发收集,占用CPU资源
提供“增量式并发收集器”
和用户线程交替运行,以减少GC线程独占资源的时间
但是会导致GC时间变长
已抛弃
因为垃圾回收时用户线程没有停止
不能等待空间几乎被填满再进行收集
需要预留一部分空间供并发收集时使用
内存占用达到阈值时进行收集,如果预留内存无法满足程序需要,就会出现Concurrent Model Failure失败,启动planB:Serial Old重新收集老年代
所以需要预留空间给用户线程使用
CMS运行期间如果预留内存不够用
出现Concurrent Mode Failure失败
临时启用Serial Old收集器
无法处理浮动垃圾
用户线程未停止,会持续产生垃圾
基于标记-清除,会产生碎片
-XX:_UseCMSCompactAtFullCollection
在CMS顶不住进行FullGC时开启内存碎片整理
-XX:CMSFullGCsBeforeCompaction
设置执行多少次不压缩的FullGC后进行一次压缩
默认为0,表示每次FullGC时进行碎片整理
3.5.7、G1收集器
面向服务端的
并行与并发
分代收集
有分代的概念,但是不需要其他收集器配合
能够采取不同方式处理:新创建对象、已经存活一段时间的对象、熬过多次GC的旧对象
获取更好的收集效果
空间整合
整体看:标记-整理
局部看:复制算法
收集后不会产生碎片,有利于程序长时间运行
可预测的停顿
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,然后根据允许的收集时间,优先回收价值最大的Region
使用Region划分内存空间以及有优先级的区域回收方式,保证G1收集器在有限时间内可以获取尽可能高的收集效率
将Java堆划分为多个大小相等的独立区域
保留新生代和老年代的概念
但是不再是物理隔离
虚拟机使用Remembered Set来避免全堆扫描
每个Region都有一个对应的Remembered Set
程序进行写操作时,检查Reference引用的对象是否处于不同Region,是的话,就通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中
收集过程
标记GC Roots能直接关联到的对象
修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region中创建对象
需要停顿线程
从GC Root开始对堆中对象进行可达性分析,找出存活对象
耗时较长
可与用户程序并发执行
最终标记
修正用户程序继续运转而导致标记产生变动的那一部分标记记录
需要停顿线程,可并行执行
筛选回收
对各个Region的回收价值和成本进行排序,根据用户期望停顿时间制定回收计划
3.5.8、理解GC日志
33.125:代表GC发生的时间,是虚拟机启动以来经过的秒数
GC、Full GC:说明垃圾收集的停顿类型,有Full,说明GC发生了Stop-The-World
System.gc()导致:Full GC (System)
DefNew:表示GC发生的区域,名称和使用的收集器相关
3324K->152K(3712K):GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
3324K->152K(11904K):GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)
0.0025925secs:该内存区域GC所占用的时间
有的收集器会给出具体的时间,因为多核的原因,时间总和可能超过real时间
3.5.9、垃圾收集器参数总结
3.6、内存分配与回收策略
自动内存管理
给对象分配内存
回收分配给对象的内存
对象的内存分配在堆上分配
主要分配在新生代的Eden区
少数情况会分配在老年代
3.6.1、对象优先在Eden分配
对象在新生代Eden分配
Eden没有足够空间分配,将发起一次Minor GC
3.6.2、大对象提前进入老年代
大对象
需要大量连续内存空间的Java对象
长字符串
数组
XX:pretenureSizeThreshold参数控制大于设置值的对象直接在老年代分配
可以避免大对象在Eden、Survivor之间复制
新生代采用复制算法收集内存
3.6.3、长期存活的对象将进入老年代
3.6.4、动态对象年龄判定
3.6.5、空间分配担保
4、虚拟机性能监控与故障处理工具
4.1、概述
定位系统问题
关键基础
知识
经验
依据
数据
运行日志
异常堆栈
GC日志
线程快照
堆转储快照
工具
运用知识处理数据的手段
4.2、JDK的命令行工具
4.2.1、jps:虚拟机进程状况工具
4.2.2、jstat:虚拟机统计信息监视工具
4.2.3、jinfo:Java配置信息工具
4.2.4、jmap:Java内存映象工具
4.2.5、jhat:虚拟机堆转储快照分析工具
4.2.6、jstack:Java堆栈跟踪工具
生成虚拟机当前时刻的线程快照
5、调优案例分析与实战
5.2、案例分析
5.3、实战:Eclipse运行速度调优
三、虚拟机执行子系统
6、类文件结构
6.1、概述
计算机只认识0和1
6.2、无关系基石
字节码
虚拟机和字节码存储格式是实现语言无关性的基础
6.3、Class类文件的结构
Class文件格式
无符号数
基本数据类型
u1
1个字节
u2
2个字节
u4
u8
可用来描述数字、索引引用、数量值或者UTF08编码构成的字符串值
表
由多个无符号数或者其他表作为数据项构成
习惯以“_info”结尾
整个Class文件其实是一张表
6.3.1、魔数与Class文件的版本
Class文件的头4个字节称为魔数
用于缺点这个文件是否为一个能被虚拟机接受的Class文件
紧接着的4个字节是Class文件的版本号
5、6是次版本号
7、8是主版本号
Java版本号是从45开始
6.3.2、常量池
Class文件之中的资源仓库
紧挨着主次版本号之后
存放
字面量
接近Java层面的常量概念
符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
javap -verbose TestClass
查看字节码内容
6.3.3、访问标志
常量池之后的两个字节代表访问标志
信息包括
这个Class是类还是接口
是否定义为public类型
是否定义为abstract
如果是类,是否被声明为final
6.3.4、类索引、父类索引与接口索引集合
6.3.5、字段表集合
用于描述接口或者类中声明的变量
6.3.6、方法表集合
6.3.6、属性表集合
7、虚拟机类加载机制
7.1、概述
Class文件需要加载到虚拟机中才能运行和使用
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
Java的动态扩展特性是依赖运行期动态加载和动态连接特点实现
如,一个接口可以在运行时再指定实际的实现类
7.2、类加载的时机
类的生命周期
1、加载(标数字表示这几个顺序是固定)
连接
2、验证
3、准备
解析(不标数字表示顺序不确定)
可以发生在初始化之后,为了支持运行时绑定
4、初始化
虚拟机规范规定有且只有5种情况必须立即进行初始化
1、遇到new、getstatic、putstatic、invokestatic这4条字节码指令时
使用new关键字实例化对象
读取或者设置一个类的静态字段(被final修饰、在编译期把结果放入常量池的静态字段的除外)
调用一个类的静态方法
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果累没有进行过初始化
3、初始化一个类的时候,如果它的父类没有进行过初始化
4、虚拟机启动时,用户需指定一个要执行的主类,虚拟机会先初始化这个主类
5、使用jdk7的动态语言时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法句柄对应的类没有进行初始化
这5种场景中的行为称为对一个类进行主动引用
使用
5、卸载
7.3、类加载的过程
7.3.1、加载
加载节点,虚拟机要完成3件事情
1、通过类的全限定名来获取定义此类的二进制字节流
2、将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
7.3.2、验证
目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
检验动作
1、文件格式验证
验证字节流是否符合Class文件格式的规范
只有符合验证的字节流才能进入内存的方法区进行存储
2、元数据验证
目的:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
对字节码描述的信息进行语义分析,保证描述的信息符合Java语义规范
3、字节码验证
目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
确保被校验类的方法在运行时不会危害虚拟机安全
如在栈放了一个int,使用时安装long类型加载入本地变量表
通过StackMapTable优化
4、符号引用验证
对类自身以外的信息进行匹配性校验
对于类加载机制而言,验证阶段非常重要,但不一定必要。如果所运行的代码已经被反复使用、验证过,可以使用-Xverify:none参数来关闭大部分类验证
7.3.3、准备
正式为类变量分配内存并设置类变量初始值的阶段,分配在方法区
内存分配仅包括类变量(被static修饰的变量),不包括实例变量
通常初始值是0值,如果类字段属性存在常量属性,则会初始化为指定的值
7.3.4、解析
虚拟机将常量池内的符号引用替换为直接引用的过程
以一组符号来描述所引用的目标,符合可以是任何形式的字母量,只要使用时能无歧义地定位到目标即可
引用的目标不一定已经加载到内存中
直接引用
可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
引用的目标必定在内存中
7.3.5、初始化
执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的
7.4、类加载器
“通过类的全限定名来获取描述此类的二进制字节流”这个动作在Java虚拟机外部实现,应用程序自己决定如何获取所需的类。实现这个动作的代码模块称为“类加载器”
7.4.1、类与类加载器
每个类加载器都有一个独立的类名称空间
比较两个类是否相等,只有在两个类是由同一个类加载器加载的前提下才有意义
相等
类的Class对象的equals()方法
isAssignableFrom()方法
isInstance()方法
instanceof关键字判定
7.4.2、双亲委派模型
从Java虚拟机角度,只有两种类加载器
启动类加载器
属于虚拟机的一部分
所有其他的类加载器
由Java实现,继承自抽象类java.lang.ClassLoader
从开发人员的角度
启动类加载器Bootstrap ClassLoader
无法被Java程序直接调用
扩展类加载器Extension ClassLoader
开发者可用直接使用
应用程序类加载器Application ClassLoader
是ClassLoader.getSystenClassLoader()返回值,又称为系统类加载器
程序的默认类加载器
除了启动类加载器,其余的类加载器都应当有自己的父类加载器
类加载器的父子关系一般使用组合关系来复用父加载器的代码
非强制约束
工作流程
一个类收到了类加载的请求,它不会自己尝试加载这个类,而是把请求委派给父类加载器完成,每一个层次的类加载器都是这样,只有父加载器反馈无法加载时,子加载器才会尝试自己去加载
这样使得类随着类加载器一起具备了优先级的层次关系
可以保证一个类即使被不同的类加载器加载,最终都是由一个类加载器加载
代码集中在java.lang.ClassLoader的loadClass()方法中
7.4.3、破坏双亲委派模型
双亲委派模型主要被破坏过3次
1、双亲委派模型出现在JDK1.2
为了向前兼容
之前,用户继承ClassLoader就是为了重写loadClass()方法
不提倡覆盖loadClass()方法,应该写在findClass()方法中,loadClass()里如果父类加载失败,就会调用自己的findClass()方法来完成加载
2、设计缺陷导致
双亲委派解决了类加载器的基础类的统一问题
但是,基础类无法回调用户的代码
通过引入线程上下文类加载器(Thread Context ClassLoader),可以通过Thread.setContextClassLoader()设置类加载器
如果创建时未设置,就从父线程继承
如果完全没设置,默认是应用程序类加载器
父类加载器请求子类加载器加载类
3、用户对程序动态性的追求导致
如代码热替换、模块热部署
在OSGi环境下,类加载器是网状结构
OSGi加载流程
1、将以java.*开头的类委派给父类加载器加载
2、否则,将委派列表名单内的类委派给父类加载器加载
3、否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
4、否则,查找当前Bundle的ClassPath,使用资金的类加载器加载
5、否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
6、否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器加载
7、否则,类查找失败
8、虚拟机字节码执行引擎
8.1、概述
执行引擎:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果
8.2、运行时栈帧结构
栈帧用于支持虚拟机进行方法调用和方法执行的数据结构
虚拟机运行时数据区中的虚拟机栈的栈元素
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
栈帧中包含局部变量表、操作数栈、动态连接、方法返回地址等
8.2.1、局部变量表
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
局部变量表复用对垃圾回收的影响!
8.2.2、操作数栈
jvm解释执行引擎:基于“操作数栈”的执行引擎
8.2.3、动态连接
在类加载阶段或第一次使用时将符合引用转化为直接引用的过程
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
8.2.4、方法返回地址
方法开始执行后,只有两种方式可以退出这个方法
1、执行引擎遇到任意一个方法返回的字节码指令
2、方法执行过程中遇到异常,且这个异常没有被处理
方法退出的过程等于把当前栈帧出栈,退出时可能执行的操作
1、恢复上层方法的局部变量表和操作数栈
2、把返回值压入调用者栈帧的操作数栈中
3、调整PC计数器的值以指向方法调用指令后面的一条指令
8.2.5、附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中
实际中一般会把动态连接、方法返回地址和其他附加信息全部归为一类,称为栈帧信息
8.3、方法调用
方法调用不同于方法执行,唯一任务就是确定被调用方法的版本,即调用哪一个方法
一切方法调用在Class文件里存储的都是符号引用
给Java带来了动态扩展能力
但是,导致需要在类加载期间,甚至运行期间才能确定目标方法的直接引用
8.3.1、解析
类加载解析阶段,会将一部分符号引用转化为直接引用
前提:方法在程序运行之前就有一个可确定的调用版本,且这个方法的调用版本在运行期是不可改变的
即“编译期可知,运行期不可变”
静态方法
和类型直接关联
私有方法
在外部不可被访问
8.3.2、分派
“重载”和“重写”在JVM中的实现
1、静态分派
虚拟机(其实是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的
依赖静态类型来定位方法执行版本的分派动作称为静态分派
重载
2、动态分派
重写
在运行期根据实际类型确定方法执行版本的分派过程称为动态分派
3、单分派和多分派
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,将分派分为单分派和多分派
Java语言是静态多分派、动态单分派的语言
4、虚拟机动态分派的实现
虚方法表
8.3.3、动态类型语言支持
1、动态类型语言
类型检查的主体过程是在运行期而不是编译期
2、JDK1.7与动态类型
3、java.lang.invoke包
主要目的:在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,即MethodHandle
模拟方法调用
Reflection是模拟Java代码层次的方法调用,重量级,针对Java语言
MethodHandle是模拟字节码层次的方法调用,轻量级,针对jvm语言
4、invokedynamic指令
5、掌控方法分派规则
invokedynamic指令与前面4条invoke指令的最大差别就是分派逻辑是由程序员决定
8.4、基于栈的字节码解释执行引擎
8.4.1、解释执行
8.4.2、基于栈的指令集与基于寄存器的指令集
8.4.3、基于栈的解释器执行过程
9、类加载及执行子系统的案例与实战
9.1、概述
在Class文件格式与执行引擎这部分,用户程序直接影响的较少
能通过程序进行操作的主要有:字节码生成与类加载器
9.2、案例分析
9.2.1、Tomcat:正统的类加载器结构
主流web服务器需要解决的问题
1、部署在同一个服务器上的两个web应用程序所使用的Java类库可以实现相互隔离
2、部署在同一个服务器上的两个web应用程序所使用的Java类库可以互相共享
3、服务器需要尽可能保证自身的安全不受部署的Web应用程序影响
4、支持jsp应用的Web服务器,需要支持HotSwap功能
Tomcat目录结构
/common目录
类库可被Tomcat和所有的Web应用程序共同使用
/server目录
类库可被Tomcat使用,对所有的web应用不可见
/shared目录
类库可被所有的web应用共同使用,但对tomcat不可见
/WebApp/WEB-INF目录
类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序不可见
Tomcat类加载器
Common类加载器
Catalina类加载器
Shared类加载器
WebApp类加载器
Jsp类加载器
9.2.2、OSGi:灵活的类加载器架构
学习JEE规范,看JBoss源码;学习类加载器,看OSGi源码
OSGi,一个基于Java语音的动态模块化规范
OSGi加载器之间的网状结构,容易导致加载死锁
可以启用按单线程串行化的方式进行类加载动作
1.7之后从底层升级,避免了这种死锁
9.2.3、字节码生成技术与动态代理的实现
javac就是字节码生成技术
generateProxyClass()方法是根据Class文件的格式规范区拼装字节码
9.2.4、Retrotranslator:跨越JDK版本
把JDK1.5中编写的代码放到JDK1.4的环境中部署使用,“Java逆向移植”
JDK相关的改动
逆向工具支持
编译器层面的改动
自动装箱、拆箱,变长参数等语法糖
替换为普通类即可
针对枚举类
本质
继承自java.lang.Enum
自动生成values()和valueOf()方法的普通类
解决思路
将父类Enum替换为运行时类库中的*.java.lang.Enum_
将类和字段的访问标志中的ACC_ENUM标志位抹去
对Java API的代码增强
如并发包
单独引入相关包即可
不支持
需要在字节码中进行支持的改动
JVM新增一条invokedynamic字节码指令
虚拟机内部的改进
重新定义的JMM
9.3、实战:自己动手实现远程执行功能
期望:Groovy Console功能
1.6提供Compiler API可以让服务器执行临时代码
9.3.1、目标
在服务器执行临时代码
要求
不依赖JDK版本
不改变原有服务端程序的部署,不依赖任何第三方类库
不侵入原有程序,也不影响原有程序的运行
需要直接支持Java语音
具备足够的自由度,不需要依赖特定的类或接口
执行结果能返回到客户端
9.3.2、思路
如何编译提交到服务器的Java代码
1、使用tools.jar包中的com.sun.tools.javac.Main类编译
2、在客户端编译好,将字节码上传到服务端
如何执行编译后的Java代码
如何收集Java代码执行的结果
9.3.3、实现
9.4、小结
只有了解了虚拟机如何执行代码,才能更好地理解怎样写出优秀的代码???
类加载疑问
类的格式已经定了,为什么不同的类还需要不同的类加载器?
类加载器决定从哪个地方加载类
类加载器无法加载类?
在它的搜索范围中找不到所需的类
双亲委派
可以防止同名类的出现
为了安全
jdbc
mysqlDriver由系统类加载器加载
DriverManager由启动类加载器加载
DriverManager里加载mysqlDriver是调用mysqlDriver的类加载器加载的
mysqlDriver已经被加载了,为什么还要被再加载一次呢?
现象:DriverManager再加载一次,判断这个driver是不是和已经注册的驱动一致
jdk理由:为了保证调用方有权限加载这个驱动
个人理解:防止出现不同类加载器加载同一个类,目的和双亲委派类似
收藏
收藏
0 条评论
回复 删除
下一页