走进 - JVM - b
走进 - JVM - b
参考书籍:《深入理解JVM原理》
1.内存自动管理机制
1.1 运行时数据区
1.1.1 程序计数器
-
较小内存空间,字节码解释器执行时基于计数器的值选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能。
-
Java虚拟机的多线程通过线程轮流切换并分配处理器执行时间实现,故任何一个确定时刻一个处理器(多核处理器的一个内核)只会执行一条线程中的指令。为保证其切换后的正确性,每个线程有独立的计数器。
-
线程正在执行Java方法,则计数器记录正在执行的指令地址,若为本地(Native)方法,则计数器值为空(Undeifined)。
-
Java虚拟机规范中唯一没有任何OutOfMemoryError的区域
1.1.2 虚拟机栈
-
线程私有,生命周期同线程
-
描述Java方法执行的内存模型,Java 方法执行同时会创建栈帧,存放存储局部变量表,操作数栈,动态链接,方法出入口等信息,方法的调用直至执行----一个栈帧入栈到出栈的过程
-
局部变量表:
-
存放编译期可知的各种数据类型:boolean, byte, char, short, int, long, double, 对象引用(referience类型)和returnAddress类型(指向字节码指令的地址),其中64位long和double占两个slot局部变量空间,其余数据类型占一个
-
内存空间编译器分配完毕,方法运行期不改变
-
异常:
StackOverflowError:线程请求栈深度大于虚拟机允许深度
OutOfMemoryError:动态扩展获取不到足够内存时抛出
-
1.1.3 本地方法栈
-
为虚拟机使用到的Native方法服务
-
异常:
StackOverflowError:线程请求栈深度大于虚拟机允许深度
OutOfMemoryError:动态扩展获取不到足够内存时抛出
1.1.4 Java堆
- 线程共享,虚拟机启动时创建,存放对象实例(几乎所有)
- GC器主要管理区域,也称:GC堆(Garbage Colleced Heap)
- Java虚拟机规范要求其内存逻辑上连续即可,一般可扩展,若实例分配未完成,也无法扩展,抛出OutOfmemoryError
1.1.5 方法区
-
线程共享
-
存储已被虚拟机加载的类信、常量、静态变量、即时编译器编译后的代码等,又称Non-Heap(非堆)
-
可设定固定或可扩展大小,也可不实现GC
-
此区域GC目标,常量池的回收和对类型的卸载
-
无法实现内存分配需求,抛出OutOfMemoryError
-
运行时常量池(Runtime Constant Pool)
- 方法区的一部分
- Class文件除了基本数据信息外还有,常量池(Constant Pool Table)存放编译期的字面量和符号引用,类加载后进入方法区的运行时常量池
- 动态性,运行期间同样可以将新的常量放入池中
- 无法再申请内存时抛出OutOfMemoryError
1.2 直接内存(Direct Memory)
- 不属于Java虚拟机定义的内存区域
- JDK1.4加入NIO(New Input/Output)类,基于通道(Channel)和缓冲区(Buffer)的 I/O 方式,可使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块区域的引用
2.对象
2.1 对象的创建
2.1.1 步骤
-
遇到new指令
检查其参数能否在常量池中定位到一个类的符号引用
检查其引用带表的类是否已被加载,解析,初始化过,若没有执行相应的类加载过程
-
为对象分配内存
- “指针碰撞”:内存规整,指针为标记,一边为空闲区,一边为使用区
- 代表:Serial, ParNew,带Compact过程的收集器
- “空闲列表”:内存不规整,空闲列表记录空闲区,使用时依据空闲表分配内存
- 代表:CMS,基于Mark-Sweep算法的收集器
- 解决线程分配安全:
- 分配内存动作同步处理
- CAS+失败重试保证更新操作的原子性
- 内存分配动作按照线程划分到不同空间中进行(线程在Java堆中分配小块内存,本地分配缓冲(Thread Local Allocation Buffer, TLAB)),-XX:+/-UseTLAB设定是否使用分配内存
- 分配内存动作同步处理
- “指针碰撞”:内存规整,指针为标记,一边为空闲区,一边为使用区
-
初始化
- 分配到的内存空间初始化为零值(除对象头以外),使用TLAB时,此步骤可提前至TLAB分配时进行
-
设定对象头(虚拟机视角,对象创建完成)
-
new 执行—><init>执行(程序员视角,对象创建完成)
2.2 对象内存模型
对象头(Header) + 实例数据(Instance Data) + 对齐填充(Padding)
2.2.1 对象头
-
运行时数据(32/64w位系统(未开启指针压缩)中分别为32bit,64bit),Make Word
设计为非固定数据结构,可依据对象状态复用自己存储空间
-
类型指针(指向类元数据)(并非所有虚拟机实现均有此)
2.2.2 实例数据
-
程序中定义的各种字段内容,父类继承,子类中定义
-
存储顺序由: 虚拟机分配策略(FeildsAllocationStyle) + 字段在Java源码中定义顺序
-
HotSpot默认分配策略:
longs/doubles, ints, shorts/chars, bytes/booleans, opps(Ordinary Object Pointers)
CompactFilds = true时,子类小变量可插入父类变量空隙内存片段中
-
-
HotStop VM 对象起始地址为8的倍数
2.3 对象访问定位
- 通过栈上的reference数据操作堆上的对象
- 主流对象访问方式:
- 句柄:Java堆中会划分出一块内存用于句柄池
- Java栈中存放的是句柄的地址,句柄中存放到对象实例(实例池中)数据的的指针+到对象类型的指针
- 优:reference中存储稳定的句柄地址对象被移动时只需修改句柄地址
- 直接指针:Java栈中指向的变量是Java堆中的对象实例数据,对象实例中是到对象类型的实例数据
- 优:访问速度快
- 句柄:Java堆中会划分出一块内存用于句柄池
3.垃圾收集及分配策略
3.1 判断对象已死
3.1.1 引用计数算法(Reference Counting)
- 算法:对象中添加计数器(计数器的值就是当前被引用的数量),计数器=0,对象已死
- 缺点:解决不了相互引用
3.1.2 可达性分析算法(Reachabiliy Analysis)
- 以“GC Roots"的对象作为起点,对象不可达时对象已死,搜索走过的路径为引用链(Reference China)
3.2 引用
- 强引用(Strong Reference)
- 代码中普遍存在,类似Object obj = new Object()
- 软引用(Soft Reference)
- 有用但非必须
- 弱引用(Weak Reference)
- 存活至下次GC之前
- 虚引用(Phantom Reference)
- 无法通过虚引用获取对象实例,唯一目的,被GC时收到系统通知
3.3 对象的真正去留
kill对象至少标记两次
- 第一次标记:筛选条件是否有必要执行finalize()方法
- 没必要执行:对象未覆盖finalize()方法或者已被调用过
- 有必要执行:放置F-Queue中,由虚拟机自动建立、低优先级的finalize()执行(只触发,并不承诺等待其执行完成),任何对象的finalize()方法只会被系统自动调用一次
- 第二次标记:F-Queue中小规模标记,无引用则回收
- 相比于使用finalize()的运行代价高昂,不确定性大,try{} catch{}更能胜任
3.4 回收方法区(HotSpot中的永久代)
- 回收目标:废弃常量+无用的类
- 废弃常量回收:
- 判断策略:常量不在被引用
- 回收:发生垃圾回收,有必要时回收
- 无用的类回收:
- 判断策略:
- 该类的实例都已被回收
- 该类的ClassLoader已被回收
- 该类对对应的java.lang.Class对象没有任何的引用,无法通过反射在任何地方访问该类的方法
- 回收:判断无用后不一定必被回收,HotSpot提供:-Xnoclassgc参数控制
- 判断策略:
- 频繁自定义ClassLoader的场景需要虚拟机具备类卸载功能
- 废弃常量回收:
3.5 垃圾收集算法
3.5.1 Mark-Sweep
- 最基础的收集算法
- 缺点:
- Mark和Sweep效率都不高
- 处理后有大量不连续内存碎片
3.5.2 Copying
- 内存对半分,每次使用一半,内存快用完时,将存活对象转移至另一半,随后对原半区进行内存回收
- 缺点:
- 代价高了点,可用内存直接减半
- 改进:
- 内存仍然划分:Eden + Survivor +Survivor
- 使用:Eden + Survivor,另外一个用作这两个区幸存者的逃生地,回收时对使用的Eden+Survivor回收
- 例外:Eden和Survivor中的幸存者超过Survivor的承载能力,这时需要老年代(Handle Promotion)担保(存放Survivor存放不了的对象)
3.5.3 Mark-Compact
- 标记对象,存活对象向一端移动,随后清理边界意外的内存
3.5.4 Generational Collection
- 内存空间依据对象存活周期不同划分,Java堆一般分新生代和老年代
- 策略:对不同区采用不同的回收算法,对象存活少—复制算法,对象存活率高—Mark-Sweep或Mark-Collpact
3.6HotSpot算法实现
-
枚举根节点
- 可达性分析:
- 背景:
- 方法区内存大,逐个检查消耗时间。
- 对执行时间敏感,GC停顿
- 为保证可达性分析的正确性,GC时停止所有Java进程(Stop The World)
- 处理:GC 停顿是必要的,但虚拟机必须有方法知道哪些地方有对象引用
- HotSpot使用一组OopMap(),类加载时将对象偏移量上的类型数据计算出来,JIT编译过程中会在特定位置记录栈和寄存器中哪些位置是引用
- 背景:
- 可达性分析:
-
安全点:
-
特定位置记录OopMap,这些记录了OopMap的位置称为安全点(Safepoint)
-
程序执行时只能到安全点才能停
-
安全点的选定:
- 是否具有让程序长时间执行的特征,指令复用
-
线程(执行JNI调用的线程)到达最近的安全点:
-
抢占式中断:直接终止所有线程,选取不在安全点的线程启动,到达安全点后终止(几乎不再使用)
-
主动式中断:设定标志,线程自动轮询问,标志为真自动中断挂起
-
-
-
安全区:
- 安全区的任何时刻GC都是安全的
- 线程快离开安全区时,判断系统是否完成了根节点枚举,若没有则这个线程等到可以安全离开(Safe Region)的信号为止