深入理解 JVM 运行机制与 GC 机制
虚拟机结构
运行时数据区域(叫 JVM 内存模型也可以)
按 oracle 的虚拟机规范介绍可以大致分为下面几类:
程序计数器(pc register)、Java 虚拟机堆栈、堆、方法区、运行时常量池、本机方法堆栈(native 方法堆栈)
JVM 中寄存器用于存储程序计数器的值,就是每个字节码指令左侧的数字。看到寄存器我还蛮亲切的,上学时在实验室里没少“扣”寄存器,那真是用本子记再用机器“扣”着读。那时还是写汇编(现在全忘了),代码向一个寄存器地址 load 一个 1,move 一个 1 在寄存器调试器上都可以立马看到,非常有意思。
最后这个本机方法堆栈其实也并不陌生,它会产生两个 JVM 的异常:
如果线程中的计算需要的堆栈大小比允许本机方法堆栈更大,Java 虚拟机将抛出一个 StackOverflowError.
如果本机方法堆栈可以动态扩展并且尝试本机方法堆栈扩展但可用内存不足,或者如果可用内存不足以为新线程创建初始本机方法堆栈,Java 虚拟机将抛出一个 OutOfMemoryError
栈帧
这里指虚拟机在安排方法执行时栈帧,一个方法进入执行前都会产生一个栈帧,多个方法则按照 LIFO 的模式执行。
在结构上栈帧包含本地变量表、操作栈两个可具象化的块儿。除此之外,在栈帧中还需要引用运行时常量池中的变量,方法,但他们在常量池中都是以符号形式存在,这就需要依赖 动态链接将它们转化为具体引用来供栈帧读取。
我还是更喜欢用图形来记忆:
从对象的创建分析 JVM 执行流程
准备一个简单的 java 文件,实现创建一个对象的代码:
在 class 加载前,先启动虚拟机 。虚拟机启动后,堆,方法区(及常量池)都会创建完成
创建一个线程栈来执行方法
外部需要一个类加载器,来加载 Class 到方法区
执行 main 方法前,将押入方法栈帧,设置好操作数栈,本地变量表大小。
完成准备工作后就像下图的样子:
这里的 Hello 就是类名字符串,其引用将通过动态链接引入到栈帧
执行 main 方法 需要一个外部的执行引擎,来修改程序计数器 以第一个指令 new 作图 计数器 0: new : 创建一个对象,并将其引用值压入栈顶
在堆区创建一个对象[分配内存],将其引用入栈 :
后续就不用图了因为准备工作都完成了,直接看指令
计数器 3: dup: 复制栈顶数值并将复制值压入栈顶
计数器 4: invokespecial: 调用超类构造方法,实例初始化方法,私有方法 这里会进入到构造器执行,也就需要创建新的栈帧。
从构造方法指令可以看到得构造方法的 栈容积 1,本地变量表大小 1 ,押入新栈帧:
0: aload_0 :将第 0 个引用类型本地变量推送至栈顶 1: invokespecial :调用超类构造方法 4: return :出栈
7: astore_1: 将栈顶引用型数值存入第 1 位本地变量 这里将栈的引用存入 1 位变量中 到这里才完成了 hello = new Hello() 这行代码的执行
看来创建一个对象最少需要 4 步指令,如果要赋值给另一个变量还需要额外增加 2 步
Class 生命周期与对象生命周期(粗略了解)
JVM 类加载过程:(装载、校验、准备、解析、初始化、使用、卸载)
Java 对象在 JVM 中的生命周期:
创建阶段(Created)
应用阶段(In Use)
不可见阶段(Invisible)
不可达阶段(Unreachable)
收集阶段(Collected)
终结阶段(Finalized)
对象空间重分配阶段(De-allocated)
GC(Automatic Garbage Collection)
垃圾回收设计原理的官方介绍文档:Java Garbage Collection Basics
GC root 的种类:Garbage Collection Roots
重点关注:
系统 Class
Thread Block:被存活线程引用的对象
Thread:已启动但未停止的线程
Finalizable:在终结器队列中的对象
Busy Monitor:作为同步监视器或调用 wait() 或 notify() 的对象
Java Local:被线程栈引用的方法内的局部变量
在 Heap 中的对象,其组成结构中包含一个 age 块,用于标记其存活年龄。
Heap 分区
整个垃圾回收机制要集合 Heap 分区与不同的 GC 算法来一起看。先看 heap 分区结构
从左往右分别是:新生代、老年代与永久代。YG 区域又细分为 eden,与两个 survivor space 区域,这里比较好理解。
eden 区都是新创建的对象,在 eden 内存区满时会触发小型 GC,幸存的对象将移至 S0,根据 age 累计不同逐渐移动到 S1,当判定为长期存活的对象将移动到老年代。
小型 GC 往往会很快完成,但小型 GC 也是“Stop the world Event”即阻塞全部线程的执行。
老年代为主要垃圾回收,也是“Stop the world Event”,其影响时间会更长,主要由老年代的垃圾收集器决定。
永久代包含 JVM 所需的元数据,描述应用程序中使用的类,方法,跟随 JVM 运行时需要引入的类进行递增填充。简单可以理解为永久代是专门给 JVM 存储类与方法的描述信息用的,比如 ClassLoader 加载的类信息应该就在这里吧。这很容易误以为永久代就是方法区(永久代是 Heap 中的块,而方法区是与 Heap 同级的)。
永久代也会参与垃圾回收,如果 JVM 发现不再需要这些类并且其他类可能需要空间,则这些类可能会被收集(卸载)。
与 Heap 分区对应的 GC 算法
优秀的个人文章:图解 Java 垃圾回收算法及详细过程
新生代取整体采用的是标记+复制清理法:
Eden GC 后将幸存者复制到 S0,Eden 再次 GC 后 如果 S0 满了,则将 Eden 存活的与 S0 存活的一起复制到 S1 区域,同时清空 Eden 与 S0 区域。
新建对象通常量比较小,所以 Eden 小型 GC 开销相对较小,采用复制清理可以释放出整块的内存空间,避免内存碎片化。
老年代采取标记+压缩算法(或者叫标记+整理更好理解)
在回收完被标记的对象后后产生较多的碎片内存,导致无法释放出整块的内存用于创建大的对象。此时采取压缩算法对存活的对象进行重排,释放出整块的内存区域。
开销:当内存中存活对象多,并且都是一些微小对象,而垃圾对象少时,要移动大量的存活对象才能换取少量的内存空间,从而引发频繁的 GC,进而造成应用卡顿。
到这里,此次对 JVM 知识的回顾就结束了,一共花了整整两天时间。相比较上一次这次信息视野变大了很多,查看了更多 oracle 的官方文档,从原始资料上获得了更多更深入的理解,配合一起的笔记,课程吸收更快,发现了不少以前模棱两可的理解。
这种从里往外学习技术本质的感觉着实让人上瘾!就像武侠小说里的高级心法一样,往往是心法修炼到一定级别就会进入快速上升期,随之对外在招式也是一看就会。
另一个体会,英语已经逐渐成为我的学习的明显阻碍了,直接读英文理解会更到位,但是由于各种长句潜逃与生词读起来很慢。阅读机翻的话往往会感到每个字都认识但就是看的不是特别明白,又不得不重新读原文。
等新工作定好,要在英语学习上加大力了。其实自信心还是很足的,我从来不会觉得我学英语有困难,在 Tandem 上面经常跟老外聊日常是远远不足的,一个是因为时差等因素不能保证时长,其次是聊天的质量也不太能保证,想来想去还是配合 GPT 来做定时学习吧。
作者:橘子没
链接:https://juejin.cn/post/7217054630295371813
来源:稀土掘金
评论