jvm
java内存区域
程序计数器Program Counter Register
一块很小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,
每个线程都有一个独立的程序计数器
tips:如果正在执行一个Java方法,这个计数器的值为正在执行的虚拟机字节码指令的地址,如果正在执行的是Native方法,这个计数器的值为空。
本地方法栈
和虚拟机栈发挥的作用相同,虚拟机运行的是Java方法,本地方法栈则是虚拟机使用到的Native方法。
Java堆
是Java虚拟机管理的内存中最大的一块,此内存区域的唯一目的就是存放对象的实例,
各个线程共享的内存区域
方法区
Method Area 于Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码
运行时常量池
是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口信息描述外,还有运行时常量池,用于存放编译时期生成的各种字面量和符号的引用,这部分内容将在类加载后进入方法区的运行时常量池存放
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这块内存被频繁地使用,而且也可能导致Out Of Memory Error异常出现
它可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作
对象探究
对象创建
- 检查类是否已经加载,解析,初始化;如果没有,必须先执行相应的类加载过程.
- 为对象分配内存,内存分配的大小在类加载完成后便可完全确定;[把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为 本地线程分配缓冲[Thread Local Allocation Buffer, TLAB].哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定. ]
- 初始化0值; 内存分配的空间都初始化为0,如果使用TLAB,这一工作提前到TLAB分配时进行
- 显示初始化
对象内存布局
对象在内存中存储的布局分为3块区域:对象头,实例数据,对齐填充
对象头(32位的HotSpot)[ Mark Word ]: 25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0
实例数据:程序代码中所定义的各种类型的字段内容
对齐填充:对象的大小必须是8字节的整数倍,而且对象头部正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全.
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象;
对象的访问方式也是取决于虚拟机实现而定,句柄式访问:Java堆中会分配一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;如果使用直接指针访问,Java堆对象的布局就必须考虑如何放置访问类型数据的相关信息.
outofmemoryerror异常
Java堆溢出
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
显示结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at HeapOOM.main(HeapOOM.java:16)
虚拟机栈和本地方法栈溢出
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:6)
方法区和运行时常量池溢出
因为运行时常量池属于方法区的一部分,
例子: /**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author ren
* when it run on the jvm 1.7/1.8,
* 运行很长时间
* 关于String 常量池的实现问题,1.7之后,当new 出来的字符串需要进入运行时常量池时,常量池只会记录首次出现的实例的引用,而不会再复制实例.这个程序会造成堆内存溢出
*
*/
import java.util.List;
import java.util.ArrayList;
public class RuntimeConstantPoolOOM{
public static void main(String[]args) throws Throwable{
List<String> list = new ArrayList<>();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
/*
//计算机软件是第一次出现
String str1 = new StringBuilder("计数机").append("软件").toString();
System.out.println(str1.intern() == str1);
//因为java这个字符串在之前已经出现过
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
//如果是 静态的字面量,会直接入 常量池
String s5 = "a" + "b";
System.out.println(new String("ab").intern() == s5);//true
*/
}
本机直接内存溢出
明显特征:heap Dump文件中不会看到明显的异常[堆使用情况正常],程序直接或者间接使用了NIO......
垃圾收集器与内存分配策略
对象已经”死”了吗
[1] 引用计数器算法,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加一,每当一个引用失效时,计数器减一,任何时刻计数器为0的对象是不可能在被使用的
【问题】如果两个已经无法再访问的对象相互引用着,它们的计数器就不会为0?
[2]可达性分析算法
判断一个对象是否存活,通过一系列的称为GC Roots 的对象为起始点,从这些节点开始向下搜索,如果一个对象到GC Roots没有相连,或者说是不可达,则此对象是不可用的.
tips:在Java语言中,可以作为GC Root的对象包括:
方法区中的静态属性引用的对象; 虚拟机栈中引用的对象;方法区中常量引用的对象,本地方法栈中JNI引用的对象
GC Root :一组必须活跃的引用
再谈引用
JDK1.2之后:对象不只仅有 被引用和不被引用两种状态,有一些对象在内存空间足够时希望能保留于系统,空间不足时,则可以被清除;
引用分为:强引用 软引用 弱引用 虚引用
强引用: 类似于Object obj = new Object();这类引用,只要强引用还在,GC永远都不会回收掉被引用的对象
软引用:一些有用但并非需要的对象. 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进可回收范围之中进行第二次回收。如果这次回收之后还没有足够的内存,才会抛出内存溢出异常,JDK1.2之后,提供了SoftReference类来实现软引用
弱引用:弱引用关联的对象 只能生存到下一次垃圾收集器发生之前,JDK1.2之后提供了WeakReference 类来实现弱引用
虚引用:它是最弱的一种引用关系。一个对象是否拥有虚引用完全不会对其生存时间构成影响,也无法根据虚引用来获取这个对象的实例,为一个对象设置虚引用的唯一目的是在这个对象被GC回收时收到一个系统通知。
生存还是死亡
要真正判断一个对象的死亡,至少要经历2次标记过程。
如果对象进行可达性分析后发现没有与GC Roots 相连接的引用链,它将被标记一次并进行一次筛选,条件是该对象是否有必要执行finalize()方法,如果该方法没有被覆盖或者已经调用过该方法,则没有必要执行; 对于有必要执行finalize()的对象放入F-Queue队列之中有虚拟机创建优先级低的线程区’执行”这个方法,并不保证执行完全/结束; 稍后,GC对F-Queue中的对象进行第二次小规模的标记,如果对象在最后时刻成功拯救自己,就不会被回收.
回收方法区
JVM规范中没有规定 要求虚拟机在方法区实现垃圾收集,而在方法区进行垃圾回收性价比比较低,永久代的垃圾回收主要回收两部分内容 = 废弃常量和无用的类.....
以常量池中字面量的回收为例.【1】这个字符串没有一个String对象是叫做”ABC”,如果这时候回收,有必要的话,这个”ABC”将会被回收. 常量池中的其他类,方法,字段符号引用也与此类似.....
【2】一个类是否废弃:1. 该类的所有实例已经被回收 2. 加载该类的ClassLoader已经被回收3.该类所对应的java.lang.Class对象没有在任何地方被引用,也没有任何地方通过反射访问该类的方法
垃圾收集算法
标记-清除算法
算法分为 标记 清除 两个阶段,标记阶段如上述所说,;;;;
不足之处:[1] 标记和清除效率不高 [2] 空间问题,标记清除后产生大量的碎片空间,空间碎片太多导致以后如果需要分配大的对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾回收动作;
复制算法
- 为了解决标记-清除算法的效率问题.......
把内存按照容量划分为大小相等的两块,只使用其中一块,当这一块用完时,将当前可用的所有对象复制到另一块中并按照顺序放置,回收可回收的实例......
优点:保证了不会造成大量的内存碎片,
tips:实际可能是把内存分为 一块较大的Eden空间和两块小的Survivor空间,,,当然如果回收的新生代对象超过10%, 就需要依赖其他内存(老年代)进行分配担保...
(分配担保:后详述)
标记-整理算法
对于老年代对象的特点,与标记-清除算法不同的是 不是直接对可回收对象进行清除,而是让存活对象向一端移动,然后直接清理掉端边界以外的内存,,,,,,
分代收集算法
将Java对象分为新生代和老年代,,根据每个年代的特点选用最合适的收集算法,
在新生代,大批对象’死去’,少数存活就选用复制算法,老年代少量对象死去,大部分存活就必须选用标记-[清除/整理]算法
hotspot的算法实现
枚举根节点:
hotSpot实现中,通过OopMap数据结构在类加载完成时候,HotSpot把对象内什么偏移量上是什么类型的数据计算出来.这样GC在进行扫描时就能直接得知这些信息.
总之:通过OopMap,GC停顿时虚拟机可以通过OopMap这样的一个映射表知道对象内的是什么类型的数据,
安全点:
在OopMap协助下,HotSpot可以快速且准确的完成GC Roots枚举,但是如果引用关系变化,如果为每一条指令都生成OopMap,占用的空间太大了......
实际上程序只有在特定的SafePoint才能暂停,
问题:[1] 安全点的选定? 是否具有让程序长时间执行的特征
[2] GC 发生时,所有线程如何跑到最近的安全点?
抢先式中断:GC发生时,所有线程全部中断,如果有线程不在安全点,恢复线程让它 运行至安全点.(现在几乎不用)
主动式中断:不直接对线程操作,需要暂停线程时,(待研究......
垃圾收集器
理解GC日志
33.125: [GC |DefNew: 3324K->152K(3712K), 0-0025925 ses] 3324k->152K(11904k), 0-0031680 secs]
GC发生时间+ (GC:垃圾收集的停顿类型)+(DefNew :GC发生范围)
3324k->152K(3712K)表示GC前盖内存区域已经用容量-->GC后使用量(该内存区域总容量)
内存分配与回收策略
- 对象主要分配在新生代的Eden区上,如果启用了本地线程分配缓冲,将按线程优先在TLAB上分配.少数情况也会分配在老年代中.
- 大对象直接进入老年代,因为新生代采用复制算法,大对象直接进入老年代可以避免在Eden区及两个Survivor区之间发生大量的内存复制;虚拟机提供-XX:PretenureSizeThreshold 参数,大于此值的对象直接分配在老年代...
- 长期村后的对象将进入老年代: 虚拟机给每一个对象定义了一个对象年龄(Age)计数器 ,当对象在Eden出生并经历了第一次GC 后仍然存活,如果这个对象能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄为1. 当它的年龄到达一定程度(默认15岁) ,将会被晋升到老年代中. 对于晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
动态对象年龄判定: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代.....
空间分配担保
在Minor GC(新生代GC)之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,Minor GC 可以确保是安全的...如果不成立,则虚拟机查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么 会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC(tips:猜测:如果历次晋升的平均大小比较小,可能老年代有可能可以承受这一次对象晋升到老年代,也有可能失败);;;;;如果小于,或者不允许担保失败,那这时也要改为进行一次Full GC...
第4章 虚拟机性能监控与故障处理工具
名称 | 主要作用 |
jps | JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 |
jstat | JVM Statistics Monitoring Tool, 用于收集HotSpot虚拟机各方面的运行数据 |
jinfo | Configuration Info for Java. 显示虚拟机配置信息 |
jmap | Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件) |
jhat | JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上分析结果 |
jstack | Stack Trace for Java..显示虚拟机的线程快照 |
JDK的命令行工具
jps(JVM Process Status Tool):虚拟机进程状况工具
命令格式: jps [ options ] [hostid ] 可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)......
jstat:虚拟机统计信息监视工具
jstat (JVM Statistics-Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具.它可以显示本地或者远程虚拟机进程中的类装载,内存,垃圾收集,JIT编译等运行数据......
命令格式: jstat [ option vmid [ interval [s | ms | [count]] ]
VMID 与 LVMID : 如果是本地虚拟机进程,VMID与LVMID是一致的,如果是远程虚拟机进程,VMID的格式应当是:
lprotocol://lvmid[@hostname[:port]/servername}
option的各个选项略
-class 监视类装载,卸载数量,总空间以及类装载所耗费的时间
jinfo :Java配置信息工具
实时地查看和调用虚拟机各项参数,使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,
命令格式: jinfo [ option ] pid
jmap:Java 内存映像工具
...
jstack:Java 堆栈跟踪工具
......
JConsole:Java 监视与管理控制台
jvisualvm ......
第5章 调优案例分析与实战
[1]集群之间同步导致内存溢出
[2]堆外内存导致内存溢出
[3]外部命令导致内存溢出
[4]服务器jvm进程崩溃
[5]不恰当的数据结构导致内存崩溃
[6]有windows虚拟内存导致的长时间停顿
Eclipse运行速度调优
[1]类加载时间优化 : 通过 -Xverify:none 禁止掉字节码验证过程
[2]编译时间(Compile Time)和垃圾收集时间(GC -Time), Java代码编译出来的字节码文件是虚拟机通过解释方式执行,速度比执行二进制代码慢......
JDK1.2之后,虚拟机内置了2个运行时编译器,如果一段Java方法被调用的次数达到一定程序,就会被判定成热代码交给JIT编译器编译成本地代码,甚至运行时动态编译比C/C++静态编译出来的代码更优秀,甚至采用一些激进的优化手段,在优化条件不成立的条件下在逆优化回来. Java 运行时编译 最大缺点:需要消耗程序正常运行时间,这也是所说的”编译时间”
tips:JVM提供一个-Xint参数禁止编译器运作,强制虚拟机对字节码采用纯解释的方式执行.这个参数禁止修改,优化是必须的......[这个参数现在的最大作用是让用户怀念一下JDK1.2之前的那令人心碎的运行时间和心碎的运行速度.]
[3]和解释执行相对于的另一个方面,虚拟机有两个选择: 虚拟机运行在-client模式时,使用的是一个代号为C1的轻量级编译器;;;;;虚拟机运行在-server模式时,使用的是一个代号为C2的重量级编译器(提供了更多的优化措施),如果用户长时间不关闭Eclipse, C2编译器所消耗的额外的编译时间将会在运行速度的提升中赚回来
[4][*****]由于GC是随着程序运行而不断的运行,它对性能的影响才显得尤为重要...
-Xms 和 -XX:PermSize参数值设置为-Xmx 和 -XX:MaxPermSize 参数值一样,这样就把老年代和永久代容量固定下来了,
第六章 类文件结构
6.3 Class 类文件的结构
类型 | 名称 | 数量 |
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
6.3.1 魔数与class文件的版本
0xCAFEBABE : class类文件结构开头四字节内容,用于身份识别
5,6字节为次版本号, 7,8字节为主版本号 (jdk 1.7 : 0x 00 00 00 33) (jdk 1.8 : 0x 00 00 00 34) 每过一个JDK版本,主版本号加一......
6.3.2 常量池
常量池主要存放:字面量和符号引用,字面量比如文本字符串,声明为final的常量值;符号引用则属于编译原理方面的概念,包括了:1.类和接口的全限定名,2字段的名称和描述符,3方法名称和描述符
常量池中的每一项常量又是一个表,JDK1.7之前有11种类型,1.7时添加3种..
共同特性: 每一个表开始第一位都是一个u1类型标志位(fag,其取值代表那种类型的常量)
6.3.3访问标志
常量池结束以后,紧接着两个字节为访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括这个class是类还是接口,是否public,是否为abstract,是否声明为final.
6.3.4 类索引,父类索引与接口索引
u2 类型数据
6.3.5字段表集合
字段表结构包括:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descripion_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
第七章 虚拟机类加载机制
虚拟机把描述 类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.
7.2类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading), 验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading), 七个阶段......其中验证,准备,解析3个部分统称为连接(Linking)......
7.3类加载的过程
加载:[1] 通过一个类的全限定名来获取定义此类的二进制字节流
[2]将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
[3]在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证:1. 文件格式验证2.元数据验证 3.字节码验证4.符号引用验证
准备: 准备阶段是正式为类变量分配内存并设置类变量初始值(0)的阶段,这些变量所使用的内存都将在在方法区中进行分配...(类变量:被static 修饰的变量),其次,这里所说的初始值通常情况下是数据类型的零值,假设一个类变量public static int value = 23;
value在准备阶段过后初始值为0而不是123
tips:如果public static final int value = 123;这时类字段属性表中就会存在ConstantValue属性,准备阶段变量value就会被初始化为123
解析:虚拟机将常量池内的符号引用替换为直接引用的过程...
符号引用Symbolic References:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可.....
直接引用:直接引用是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄.
解析动作主要针对:类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行,,
初始化:类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全有虚拟机主导和控制.到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器(<clinit>()方法)的过程.
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的;
tips:静态语句块static{} 可以访问和定义在它之前的类变量,不能访问在静态语句块之后的变量,在后面定义的类变量可以赋值无法访问......(1.8这个结论不成立)
7.4类加载器
虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
== 比较两个类是否相等(equals() isAssignableFrom() isInstance() instanceof),只有在这两个类是由同一个类加载器加载的前提下才有意义.
[1]启动类加载器(Bootstrap ClassLoader):负责将<JAVA_HOME>/lib 目录中的,或者被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库加载到虚拟机内存中,,,
[2]扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库
[3]应用程序类加载器(Application ClassLoader):这个类加载器有sun.misc.Launcher$App-ClassLoader实现。这个类加载器是CLassLoader中的getSystemClassLoader()方法实现的返回值,由于这个类加载其是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器,负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器
破坏类加载器,在OSGI环境下,类加载器不再是双亲委托模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGI将按照下面的顺序进行类搜索:[1]将以java.*开头的类委托给父类加载器加载
[2]否则,将委托列表名单内的类委派给父类加载器加载
[3]否则;将Import列表中的类委派给Export这个类的Bundle的类加载器加载
[4]否则,查找当前的Bundile的classpath,使用自己的类加载器加载
[5]否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
[6]否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
[7]否则,类查找失败
第八章 虚拟机字节码执行引擎
8.2 运行时栈帧结构
栈帧(Stack Frame)用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机(Virtual Machine Stack)的栈元素。
栈帧存储了方法的局部变量表,操作数栈, 动态连接 和方法返回地址等信息.
[8.2.1 局部变量表]
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只有很有导向性地说到每个Slot都应该能存放一个boolean,byte, char, short, int ,float, reference 或returnAddress类型的数据;;;
[8.2.2] 操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(Last In First Out,LIFO)栈。操作数栈的每一个元素可以是任意Java数据类型,包括Long和Double......
32为数据类型所占栈容量为1,64则占用栈容量2。。 iadd指令为例:它在执行时最接近栈顶的两个元素的数据类型必须是Int型.
[8.2.3] 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用, 持有这个引用是为了支持方法调用过程中动态连接(Dynamic Linking).
[8.2.4 方法返回地址]
当一个方法执行时,有两种方式退出这个方法。第一种方法:执行引擎遇到任意一个方法返回的字节码指令,这时候会有返回值传递给上层方法调用者(正常完成出口Normal Method Invocation Completion);;;
第二种方法:执行过程遇到了异常,并且这个异常没有在方法体内得到处理,没有匹配的异常处理器,方法退出(异常完成出口Abrupt Method Invocation Completion).异常退出不会给它的上层调用者产生任何返回值.
方法退出的过程实际上就等同于把当前栈帧出栈,恢复上层方法的局部变量表,返回值压入调用者栈帧的操作数栈,pc计数器指向方法调用指令后面的一条指令
8.3 方法调用
方法调用阶段唯一任务就是确定被调用方法版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
8.3.1 解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,类加载过程中,会将其中一部分符号引用转化为直接引用,这种解析成立条件:方法在程序真正运行之前就有一个可确定的调用版本,并且方法调用版本在运行期不变,
这类方法的调用称为解析;
符合“编译器可知,运行期不变” 这个要求的方法,包括静态方法和私有方法,
解析是一个静态过程,类装载解析阶段就会把涉及的符号引用全部转变为可确定的直接引用..
8.3.2 分派
1.静态分派(******)
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载.
重载时是通过参数的静态类型而不是实际类型作出判断依据的.
编译器虽然能够确定出方法重载版本,但在大多数情况下这个重载版本并不是唯一的,往往只能确定一个更加合适的版本,这种模糊的结论由0和1构成的计算机世界中算是比较稀罕的事情,产生这种模糊结论的主要原因是字面量不需要定义,字面量没有明显的静态类型. 所以字面量没有显示的静态类型,静态类型只能通过语言上的规则去理解和推断
char -> int -> long -> float -> double
8.3.3 动态类型语言支持
动态类型语言:动态类型语言的关键特征是它的类型检查的主题过程是在运行期而不是编译器......
动态类型语言特点:变量无类型而变量值才有类型.
JDK1.7(JSR-292)中Invokedynamic指令以及java.lang.invoke包出现的技术背景:在虚拟机层面提供动态类型的直接支持就成为了Java平台的发展趋势之一
8.4 基于栈的字节码解释执行引擎
指令流中的指令大部分都是零地址指令,他们依赖操作数栈进行工作
虚拟机执行的概念模型:
第九章 类加载及执行子系统的案例与实战
9.2.1 tomcat 类加载器架构
放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用
放置在/server目录中:类库可被Tomcat使用,对所有的Web应用进程都不可见
放置在/shared目录中:类库可被所有的Web引用进程共同使用,但对tomcat自己不可见
放置在/WebApp/WEB-INF目录下,类库仅仅可以被此Web应用程序使用,tomcat和其他Web应用程序都不可见.
9.2.2 osgi:灵活的类加载器架构
以java.*开头的类,委派给父类加载器加载
否则委派列表名单内的类,委派给父类加载器加载
否则Import列表中的类,委派给Export这个类的Bundle的类加载器加载
否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
否则,查找是否在自己的Fragment Bundle 中,如果是,则委派个Fragment Bundle的类加载器加载
否则,查找Dynamic Import列表的Bundle,委派个对应的Bundle的类加载器加载.
否则,类查找失败
9.2.3 字节码生成技术
9.2.4 retrotranslator :跨越JDK版本
将高版本的jdk1.5编译出来的Class文件转变为可以在低版本JDK1.4,上部署的版本
9.3 自己动手实现远程执行功能
需求:我们需要在Java服务器运行时(服务时)执行一段临时代码的需求
JDK1.6 提供Compiler API 但是不使用,为了不依赖JDK版本
不依赖第三方类库
不侵入原有服务端程序部署
[假设已经编译好形成了.class 文件, 每一次调用执行都根据新传入的byte[]数组决定,且程序所有的system.out 输入 或者 err 输出都会给到一个printStream对象(通过字节码替换方式)]
最后:成功替换System.out 为 PrintStream
第十章 早期(编译器)优化
Java语言的”编译期”其实是一段”不确定”的操作过程,应为它可能是指一个前端编译器把*.java -> *.class 的过程,也可能是指 把*.class ->机器码的过程 ,也可能是指把 .java->机器码的过程;这三种情况分别为:
[1]前端编译器:Sun 的 Javac, Eclipse JDT 中的增量式编译器(ECJ)
[2]JIT编译器:HotSpot VM 的C1,C2编译器
[3]AOT编译器:GNU Compiler for the Java(GCJ), Excelsior JET
10.2 javac编译器
10.2.2 解析与填充符号表
1.词法,语法分析
根据Token序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形结构.
2.填充符号表
语法解析和词法分析之后,就是填充符号表的过程。符号表是一组符号地址和符号信息构成的表格,可以想象成k-v键值对的形式,符号表所登记的信息在编译的不同阶段都要用到. 例如,在语法分析中,符号表所登记的内容将用于语义检查和产生中间代码.
10.2.3 注解处理器
JDK1.5之后,Java语言对注解Annotation的支持.jdk1.6 提供了一组插入式注解处理器标准API在编译期对注解进行处理,在这些插件里面,可以读取,修改,添加抽象语法树中的任意元素.
插入式注解处理器:初始化过程在 initPorcessAnnotations()方法中,执行过程在processAnnotations()方法中执行;
10.2.4 语义分析与字节码生成
语法树:表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的.语义分析的主要任务是对结构上3正确的源程序进行上下文有关性质的审查.
- 标注检查
语义分析过程中分为标注检查以及数据及控制流分析两个步骤,分别为attribute() flow()方法完成..标注检查步骤中,一个重要的动作称为常量折叠,int a = 1+2; 相比于int a=3;
不会增加运行期cpu运算量.
- 数据及控制流分析
对程序上下文逻辑进一步验证,进程注诸如程序局部变量在使用前是否有赋值,方法的每条路径是否都有返回值,是否所有的受查异常都被正确处理.
编译时期数据及控制流分析 和 类加载时的数据及控制流分析目的基本上是一致的,但范围不同,
3解语法糖
语法糖(Syntactic Sugar),糖衣服语法,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用.
虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖.
4字节码生成
是Javac编译过程中最后一个阶段,在JAVAC源码里面由com.sun.tools.javac.jvm.Gen类来完成.字节码生成阶段不仅仅把前面各个步骤所生成的信息转化为字节码写到磁盘,编译器还进行少量代码添加和转移工作...
10.3 java语法糖的味道
[1]泛型与类型擦除:java的泛型只在源代码层面存在,编译后的字节码文件,就已经替换为原生类型,对于运行期的java, ArrayList<Integer>与ArrayList<String>是同一个类,
所以泛型技术是Java的一颗语法糖.
[2]自动装箱,拆箱与遍历循环
//List<Integer> list = Arrays.asList(1, 2, 3, 4);
//JDK1.7中,语法糖:List<Integer> list = [1, 2, 3,4];
自动装箱:Integer.valueOf() 和 Integer.intValue()方法
[3]条件编译
条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量池的真假,编译器将会把分支中不成立的代码块消除掉,这一工作
第十二章 Java内存模型与线程
12.2 硬件效率与一致性
绝大多数的计算任务不可能只靠处理器”计算”就能完成.处理器至少要和内存交互.由于计算机的存储设备与处理器的运算速度有几个数量级别的差距,现代计算机系统不得不加入一层读写速度尽可能接近处理器速度的高速缓存(Cache)来作为内存与处理器之间的缓冲. 这样运算需要使用到的数据会复制到缓存中,计算结束后再从缓存同步到内存;
这样处理器就不需要等待缓慢的内存读写了.
导致的问题: 缓存一致性(Cache Coherence)
每一个处理器都有自己的高速缓存,他们又共享同一个主内存 当多个处理器的运算任务涉及到同一块主内存时,讲可能导致各自的缓存数据不一致的情况, 这时需要各个处理器访问缓存时遵循一些协议,在读写时根据协议来进行操作,这类协议有MSI, MESI(ILLinois Protocol),MOSI,Synapse,Firefly及Dragon,Protocol等,
内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
处理器对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序结果重组,保证该结果与顺序执行的结果一致,因此如果一个操作依赖与另一个计算的中间结果,那么其顺序性并不能靠代码的先后顺序来保证...Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化
12.3 Java内存模型
12.3.1 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,,,即虚拟机将变量存储到内存和从内存中取出变量这样的底层细节...此处的变量(Variables)与Java编程中所说的变量有所区别,它包括(实例字段,静态字段,构成数组对象的元素),但不包苦线程私有的变量例如局部变量和方法参数
tips:主内存对应着物理内存, (优化措施下)工作内存优先存储与寄存器和高速缓存中
12.3.2 内存间的交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步会主内存之类的实现细节,Java内存模型定义了以下8中操作来完成,虚拟机实现必须保证下面提及的每一种操作都是原子性的,不可再分的.
[1]lock
(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占状态
[2]unlock(解锁):作用于主内存的变量,它把一个变量从lock状态释放出来,释放后的变量可以被其他线程锁定
[3]read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作
[4]load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中.
[5]use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
[6]assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个变量赋值的字节码指令时执行这个操作
[7]store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write操作使用.
[8]write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
*** Java内存模型只保证两个操作必须按顺序执行
12.3.3 对于volatile型变量的特殊规则
volatile两种特性:第一是保证此变量对所有线程的可见性,(这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的)
这里的一致性并不是各个线程的工作内存中的值都是一致的,而是在获取主内存中volatile变量时总是最新的值,在回写volatile变量时,保证了先写后读
第二个特性:禁止指令重排序优化,普通变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不保证变量赋值操作的顺序与程序代码中的执行顺序一致.这也就是Java内存模型中描述的所谓的”线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics).
从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元执行结果. lock addI $0x0($esp)指令修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了”指令重排序无法越过内存屏障”的效果.
tips:volatile 与 synchronized 对比: 大多数情况volatile比锁开销更加低,volatile的读操作与普通变量性能消耗差距不大,写操作则可能慢一些,因为他需要在本地代码中插入许多内存屏障指令保证处理器不发生乱序执行. 我在volatile与锁之间选择的唯一依据仅仅是volatile的语义能否满足场景的需求.
12.3.4 对于long和double型变量的特殊规则
虚拟机实现可以不保证64位数据类型的load,store,read,和write这4个操作的原子性;
如果有多个线程共享一个并未声明volatile的long或double 变量,并同时进行读写操作,可能某些线程会读取到一个”半个变量”的值;但是在大多数商业虚拟机实现中都保证了64为数据类型读写操作的原子性
12.3.5 原子性,可见性和有序性
可见性:一个线程修改了共享变量的值,其他线程能够立即得知这个修改.
(volatile, final, synchronized)
有序性:线程内表现为串行的语义, 如果在一个线程观察另一个线程,所有操作都是无序的==>指令重排序和工作内存与主内存同步延迟
12.3.6 先行发生原则
天然的先行发生关系,无序任何同步就已经存在
[1]程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生与书写在后面的操作.
[2]管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面的对同一个锁的Lock操作.
[3]volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于读操作
[4]线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生与对此线程的终止检测
[5]线程启动规则(Thread Start Rule):Thread 对象的start()方法先行发生于此线程的每一个动作.
[6]线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生
[7]线程终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始.
[8]传递性(Transitivity):如果操作A先行发生与操作B,操作B 先行发生与操作C,那就可以得出操作A先行发生与操作C 的结论.
12.4 Java与线程
线程的实现主要有3种方式:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现.
- 使用内核线程实现
内核线程(Kernel-Level Thread, KLT) 就是直接由操作系统内核支持的线程, 这种线程由内核来完成线程切换.
Java 使用的线程调度方式是抢占式...
12.4.3 状态转换
线程状态:
[1]新建(new):创建后尚未启动的线程处于这种状态
[2]运行(Runable):Runable包括了操作系统线程状态中的Running 和 Ready,也就是出于此状态的线程有可能在运行也有可能正在等待CPU为他分配执行时间.
[3]无期限等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,他们要等待被其他线程显示地唤醒,以下方法会让线程陷入无期限的等待状态:
没有设置Timeout参数的 Object.wait()
没有设置Timeout参数的Thread.join()方法
LockSupport.park()方法
[4]期限等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其他线程显示地唤醒,在一定时间之后它们会由系统自动唤醒,
thread.sleep();方法
设置了TimeOut参数的Object.wait()方法
设置了Timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法
[5]阻塞(Blocked):线程被阻塞了,”阻塞”和”等待”区别:阻塞状态 在等待着获取 到一个排它锁, 这个事件将在另一个线程放弃这个锁的时候发生;而等待则是等待一段时间.或者唤醒动作的发生.
[6]结束(Terminated):已终止线程的线程状态.
第十三章 线程安全与锁优化
13.2.1 Java语言中的线程安全
[1]不可变对象
[2]绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施,通常需要付出很大的,甚至有时候是不切实际的代价
[3]相对线程安全
它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保证措施,但对于一些特定顺序的连续调用,就可能需要在调用段使用额外的同步手段来保证调用的正确性
[4]线程兼容
对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说一个类不是线程安全的,绝大多数时候指的就是这一种情况,例如Vector 和 HashTable所对应的ArrayList和HashMap
[5]线程对立
无论调用端是否采取了同步措施,都无法在多线程并发使用的代码..
13.2.2 线程安全的实现方法
[1]互斥同步
指多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用,互斥是实现同步的一种手段,临界区,互斥量,和信号量都是主要的互斥实现方式...因此在这里互斥是因,同步是果, 互斥是方法,同步是目的
等待可中断,公平锁/非公平锁, 锁绑定多个条件
[2]非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步(Blocking Synchronization),从处理问题的方式上说,互斥同步属于一种悲观锁...
乐观并发策略:先进行操作,如果没有其他线程争用共享数据则操作成功,有争用时,再采用其他补偿措施..这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)
[3]无同步方案
可重入代码:在代码执行任何时刻中断它并转而执行另一端段代码,而在控制权返回后,原来的程序不会出现任何错误.
线程本地存储:\
13.3 锁优化
13.3.1 自旋锁与自适应自旋
如果自旋等待时间短,可以减小线程切换的开销;如果等待的时间长,反而自旋的线程白白浪费处理器的资源.
自适应自旋:如果某个对象通过自旋获得了某个锁,那么可能再次成功,从而它将被允许自旋等待时间更长.反之亦然.