再看JVM第二篇:深入理解JVM运行机制与GC机制
上一篇:再看JVM第一篇:了解class文件是深入理解JVM与Java的好方式
这是我第二次研究JVM的原理性知识,这次一共分为两篇内容。花了不少的精力,其实我还想再深入些,但我必须遏制自己的好奇心了。作为Android 应用层开发者在JVM上花太多时间是没必要的,一是这样不带问题进入一个深层次领域ROI很低,第二个是有更多的应用层的更实用的东西需要学习。不过收获还是蛮大的,至少以后有机会碰到需要深入研究JVM时应该能快速找到上手路径,不至于毫无头绪。
下面这两篇oracle 官方文档是最好的学习资料,空的时候还是有很大兴趣当看书一样进行细读。
The Structure of the Java Virtual Machine
Java Garbage Collection Basics - Oracle
hencoderPlus的JVM课程也很棒,很好的帮助我跨过了JVM前期知识门槛
在上一篇中分析了class字节码文件与执行指令解读,算是只见树木不见森林的一种探索。这一篇将在上一篇的基础上,结合JVM 整体结构与执行流程来记录我对JVM的学习。
虚拟机结构
运行时数据区域(叫JVM 内存模型也可以)
按oracle的虚拟机规范介绍可以大致分为下面几类:
程序计数器(pc register)、Java虚拟机堆栈、堆、方法区、运行时常量池、本机方法堆栈(native方法堆栈)
JVM中寄存器用于存储程序计数器的值,就是每个字节码指令左侧的数字。看到寄存器我还蛮亲切的,上学时在实验室里没少“扣”寄存器,那真是用本子记再用机器“扣”着读。那时还是写汇编(现在全忘了),代码向一个寄存器地址load一个1,move一个1在寄存器调试器上都可以立马看到,非常有意思。
最后这个本机方法堆栈其实也并不陌生,它会产生两个JVM的异常
- 如果线程中的计算需要的堆栈大小比允许本机方法堆栈更大,Java 虚拟机将抛出一个
StackOverflowError
. - 如果本机方法堆栈可以动态扩展并且尝试本机方法堆栈扩展但可用内存不足,或者如果可用内存不足以为新线程创建初始本机方法堆栈,Java 虚拟机将抛出一个
OutOfMemoryError
栈帧
这里指虚拟机在安排方法执行时栈帧,一个方法进入执行前都会产生一个栈帧,多个方法则按照LIFO的模式执行。
在结构上栈帧包含本地变量表、操作栈两个可具象化的块儿。除此之外,在栈帧中还需要引用运行时常量池中的变量,方法,但他们在常量池中都是以符号形式存在,这就需要依赖 动态链接将它们转化为具体引用来供栈帧读取。
我还是更喜欢用图形来记忆:
从对象的创建分析JVM执行流程
准备一个简单的java文件,实现创建一个对象的代码:
- 1.在class加载前,先启动虚拟机 。虚拟机启动后,堆,方法区(及常量池)都会创建完成
- 2.创建一个线程栈来执行方法
- 3.外部需要一个类加载器,来加载Class到方法区
- 4.执行main方法前,将押入方法栈帧,设置好操作数栈,本地变量表大小。
完成准备工作后就像下图的样子:
这里的 Hello 就是类名字符串,其引用将通过动态链接引入到栈帧
- 5.执行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类加载过程:(装载、校验、准备、解析、初始化、使用、卸载)
- 1 创建阶段(Created)
- 2 应用阶段(In Use)
- 3 不可见阶段(Invisible)
- 4 不可达阶段(Unreachable)
- 5 收集阶段(Collected)
- 6 终结阶段(Finalized)
- 7 对象空间重分配阶段(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