《深入理解Java虚拟机》笔记
自动内存管理机制
程序计数器
- 通过改变这个计数器的指决定执行的下一条指令
- 线程私有 每条线程独立
- 不会 oom
Java 虚拟机栈
存放基本类型和引用和 returnAddress类型(指向了一条字节码指令的地址)
描述 Java 方法执行的内存模型Java 堆
- 存放对象实例
- 线程共享,内存最大
- GC 的主要区域
- 基于分代回收,大致分为新生代和老年代
方法区
- 线程共享
- 类信息,常量,静态变量
虚拟机对象
对象的创建
new指令的参数是否能在常量池中定位到一个类的符号引用->是否已经加载,解析初始化->分配内存(指针碰撞,空闲列表)->初始化为零值->为对象进行必要设置->< init > 方法。(ps:new 关键字其实为new和init两个方法)
对象的内存分布
- 对象头
1.分用于存储对象自身的运行时数据,e.g.哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等
2.类型指针,对象指向它的类元数据的指针,虚拟机通过这个指 针来确定这个对象是哪个类的实例. - 实例数据
- 对其填充
对象的访问定位
使用栈上的引用去操作堆上的实例。
- 句柄
在堆中有一块内存区域作为句柄池,栈中的引用存储的是对象的句柄地址,句柄中包括对象的实例数据(实例池)和类型数据(方法区)。
好处:GC 时只会改变句柄中实例指针,不会影响栈中的指向。- 直接指针
栈中的引用直接指向对象地址。
好处:速度快,省一次指针定向
- 直接指针
ps: String.intern
的问题 点这里
垃圾收集器与内存分配策略
垃圾收集算法
标记-清除
先标记需要回收的对象,然后统一回收。缺点是效率不高,会产生内存碎片问题。
复制算法
1:1分割内存,只用一半,回收时把存活的复制到另一半,清除原来一半。
实际使用在新生代,适合回收频率高的。朝生夕死。每次清理都有大量对象死亡。HotSpot
默认为8:1:1,浪费10%的内存空间,使用8和1,当回收时候,复制到另一个1中。空间不够时候,需要依赖老年代。
标记-整理
先标记,把存活的移动到一端,清除。适合老年代
HotSpot的算法实现
枚举根节点
通过使用一组OopMap
的数据结构知道哪些地方存放对象引用。在类加载完成时,HotSpot
就把对象内什么偏移量上是什么类型的数据计算出来,在JIT
编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,这样GC扫描时就可以直接得知。
安全点
上面特定的位置是安全点,只有在安全点处暂停。主要使用主动式中断让所有线程都跑到安全点再停顿。不直接对线程操作,设置一个标志,各个线程主动去轮训,发现中断标志true就自己中断挂起。
安全区域
安全区域用来解决在程序没有CPU时间,例如线程sleep或者block的时候进入GC。安全区域是指在一段代码之后,引用关系不会发生变化,在这个区域任何地方GC都是安全的。当程序执行到安全区域,首先标识进入,然后GC可以随时进行。当程序要离开安全区域时,先去检查GC是否完成,是->继续执行,否->等待完成。
垃圾收集器
Serial收集器
最基本最久远的收集器,单线程,STW。没有线程切换的开销,简单。复制算法
ParNew收集器
Serial收集器的多线程版本。
Parallel Scavenge收集器
和ParNew收集器类似,关注吞吐量
Serial Old收集器
Serial收集器的的老年代版本。标记-整理
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,标记-整理。
CMS收集器
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
1,3步骤STW。1只标记GC roots能直接关联对象,很快。并发标记阶段就是进行GC RootsTracing 的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变 动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远 比并发标记的时间短。整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作
优点:
并发收集,低停顿
缺点:
对CPU资源敏感,默认回收线程(cpu数量+3)/4
cms无法处理”浮动垃圾”(当并发收集同时产生的垃圾)
标记-清除算法内存碎片问题
G1收集器
特点:
- 分代收集
- 并行与并发
- 空间整合
- 可预测的停顿
将整个Java堆划分 为多个大小相等的独立区域(Region)
大致过程:- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
4->筛选回收阶段首先对各个Region的回收价值和成本进行排序, 根据用户所期望的GC停顿时间来制定回收计划
内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟 机将发起一次Minor GC
大对象直接进入老年代
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对 象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被 Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中 每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就 将会被晋升到老年代中。
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总 和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等 到MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有 对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机 会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代 最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行 一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置 不允许冒险,那这时也要改为进行一次Full GC。
虚拟机性能监控与故障处理工具
略
调优案例分析与实战
略
虚拟机执行子系统
类文件结构
Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
无符号数属于基本类型,有u1,u2,u4,u8表示几个字节,可以用来描述数字,索引引用,数据值或者按照UTF-8编码的字符串
表是由多个无符号数或者其他表作为数据结构的复合数据结构,表习惯以_info结尾。
整个Class本质上就是一个表
Class文件格式:
魔数 u4
0xCAFFBABE
次版本号 u2
主版本号 u2
常量池
u2的常量池大小 index从1开始,第0项空出
常量池中主要存放两大类常亮:字面量和符号引用
字面量类似Java的常量
符号引用包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项都是一个表(都对应一个Class)。一共14种表,表开始的第一位u1代表当前这个常量属于哪种常量类型
具体如下:
访问标志 u2
用于标识一些类或者接口成次的访问信息
具体如下:
类索引、父类索引与接口索引集合 u2,u2,List
字段表集合
用于描述接口或类中的变量
字段包括类级变量(static)和实例级变量 不包括方法内部的局部变量
方法表集合
对方法的描述
属性表集合
用于描述某些场景专用信息。下面的必备的9个属性:
Code属性
简单理解就是方法体,具体的结构如下:
Code属性是Class文件中最重要的属性,如果把Java程序中信息分为代码(Code)和元数据(类,字段,方法定有以及其他信息)两部分。Code属性属于描述代码,所有的其他数据项目都用于描述元数据
字节码指令简介
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
Tload -> 将一个局部变量加载到操作栈
Tstore -> 将一个数值从操作数栈存储到局部变量表
Tipush -> 将一个变量加载到操作数栈
wide -> 扩充局部变量表的访问索引指令
运算指令
用于对两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作栈顶。
算术运算大致分为对整形数据进行运算的指令和对浮点数进行运算的指令
加法指令 :Tadd
减法指令: Tsub
乘法指令: Tmul
除法指令:Tdiv
求余指令:Trem
取反指令:Tneg
位移指令:Tshl,Tshr,Tushr
按位或指令:Tor
按位与指令:Tand
按位异或指令:Txor
局部变量自增指令:Tinc
比较指令:Tcmpg,Rcmpl
类型转换指令
用于将两种不同的数值类型进行相互转换,一般用于实现用户代码中的显式类型转换或者用于处理字节码指令集中数据类型相关指令无法一一对应的问题
i2b,i2c…
对象创建和访问指令
Java虚拟机对类实例和数组的创建于操作用了不同的字节码指令。
创建类实例 -> new
创建数组 -> newarray,anewarray,multianewarray
访问类字段和实例字段 -> getfield,putfield,getstatic,putstatic
把一个数组元素加载到操作数栈 -> Taload
将一个操作数栈的值存储到数组元素中 ->Tastore
取数组长度 -> arraylength
检查类实例类型 -> instanceof,checkcast
操作数栈管理指令
直接操作操作数栈的指令
将操作数栈的栈顶一个或两个元素出栈 ->pop,pop2
复制栈顶两个数值并将双份复制值从新压入栈顶 -> dup2,dup2_x1,dup2_x2
栈顶两个数值交换 -> swap
控制转移指令
条件分支,复合条件分支,无条件分支
各种类型的比较最终都会转化成int类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最丰富和强大的
方法调用和返回指令
invokevirtual ->调用实例方法,最常见的
invokeinterface ->调用接口方法,会在运行时搜索一个实现了这个接口方法的对象,找到合适的方法进行调用
invokespecial -> 调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
invokestatic -> 调用类方法
invokedynamic -> 在运行时动态解析出调用点限定符所引用的方法,并执行该方法????
方法调用指令与数据类型无关,返回指令是根据返回值的类型区分的.Treturn
异常处理指令
显示抛出异常(throw)都是由athrow指令实现。
处理异常不是由字节码指令实现的,采用异常表完成。
同步指令
支持方法级同步和方法内部一段指令序列的同步,是使用管程(Monitor)支持。
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作 之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个 方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线 程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出 了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同 步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的 指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现 synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。