Java代码执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-smm9bR4f-1628844016582)(JVM.assets/image-20210805171335842.png)]
任何一个环节失败都不能正确生成字节码文件。
JVM的生命周期
- 虚拟机的启动
- 是通过引导类加载器创建一个初始类来完成的。
- 虚拟机的执行
- 程序开始就运行,程序结束就停止
- 执行一个java程序时,实际上执行的是java虚拟机的进程
- 虚拟机的退出
- 程序正常执行结束
- 遇到异常或者错误
- 操作系统的错误
- 调用Runtime类或者System类exit方法
手写一个JVM的需要类加载器和执行引擎
类加载器
java字节码文件起始的[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APaVDV21-1628844016584)(JVM.assets/image-20210805194224618.png)]
类加载的过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GMM7vVxd-1628844016589)(JVM.assets/image-20210805193039442.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aj9nRtx7-1628844016591)(JVM.assets/image-20210805193901572.png)]
类加载器的分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9bsCaxHG-1628844016593)(JVM.assets/image-20210805203313401.png)]
- 引导类加载器(Bootstrap Class Loader)
- C/C++实现,嵌套在JVM内部
- 加载核心类库
- 出于安全考虑只加载Java、Javax、Sun等开头的类
- 扩展类加载器(Extension Class Loader)
- 派生于Class Loader类
- 系统类加载器(App Class Loader)
- 派生于Class Loader类
- 负责加载环境变量classpath或系统属性
- 该加载器时程序中默认的加载器,Java引用类都是由它完成
- 自定义加载器(User-Defined Class Loader)
- 直接或者间接继承了Bootstrap Class Loader
- 扩展类加载器和系统类加载器都是自定义加载类
- 不是继承关系,并且一般的类不能获取到引导类加载器
- Java的核心类库都是使用引导类加载器加载的
类加载过程
- 加载阶段
- 通过一个类的全限定名获取此类二进制字节流
- 将这个字节流所代表静态存储结构转化为方法区的运行时数据结构
- 在内存中生成Class对象,作为方法区这个类的各种数据的访问入口
- 链接阶段
- 验证(Verify)
- 确保Class文件的正确性
- 四种验证(文件格式验证、元数据验证、字节码验证、符号引用验证)
- 准备(Prepare)
- 分配内存、赋初始值(零值或者null)
- 不包含final修饰的static,因为final在编译时就分配了值(显式初始化)
- 解析(Resolve)
- 将常量池中的符号引用转化为直接引用的过程
- 验证(Verify)
- 初始化阶段
- 初始化阶段就是执行类构造器方法<clinit>()的过程
- 此方法不需要定义,自动收集类中所有类变量的赋值动作和静态代码块中的语句
- 任何一个类声明以后,内部的至少有一个构造器<init>()
双亲委派机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohifaCGu-1628844016594)(JVM.assets/image-20210805210156482.png)]
- 类加载器收到类加载请求,它并不会自己区加载,而是把这个请求委托给父类加载器去执行
- 如果父类加载器还有父类,继续委托达到引导类加载器
- 随后,若当前加载器能加载完成则返回,若不能,则让子类加载器去完成。
**沙箱安全机制:**不能在java.lang包自定义类,或者重写其他类
运行时数据区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-81hrEMNe-1628844016594)(JVM.assets/image-20210806092834925.png)]
PC寄存器
存储运行的下一条指令
问:使用PC寄存器字节码指令地址有什么用?
答:因为CPU需要不停的切换线程,切换回来的时候就知道接着从哪里继续执行
问:为什么使用PC寄存器记录当前线程的执行地址?
答:JVM的字节码编译器就需要通过改变PC寄存器的值来明确下一条字节码指令
JVM栈
栈帧包含(局部变量表,操作数栈,动态链接,方法返回地址,附加信息)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPYqbw2B-1628844016596)(JVM.assets/image-20210806102046692.png)]
局部变量表(数字数组):方法参数,局部变量,数据基本类型,对象引用。
操作数栈(主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间)
动态链接(链接到本地方法栈)
栈是不存在垃圾回收的问题,但存在OOM(内存溢出)
-Xss 1024k(设置栈的大小)
问:举例栈溢出的情况?
答:StackOverflowError、OOM
问:调整栈大小,就能保证不出现溢出吗?
答:不能,如果使用递归没有终止的情况
问:分配的栈内存越大越好吗?
答:不是,挤占别的空间
问:垃圾回收会涉及到栈吗?
答:不会,因为只有进栈和出栈,不存在垃圾
问:方法中定义的局部变量是否线程安全?
答:具体问题具体分析(对象内部产生内部消亡那个的是安全的,否则不是)
本地方法栈
与JVM栈发挥的作用相似,区别:虚拟机栈为虚拟机执行java方法服务,而本地方法栈使用的是Native方法服务。
- 本地方法栈就是管理本地方法的
- 本地方法是使用C语言实现的
- 与本地方法库打交道
堆
- 所有的对象实例以及数组都应当运行时分配在堆上
- 数组和对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
- 方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 堆时GC执行垃圾回收的重点区域
- -Xms(起始内存)【年轻代和老年代】
- -Xmx(最大内存)
内存细分
- java7之前:新生区+养老区+永久区(方法区)
- java8之后:新生区+养老区+元空间
- 新生区=伊甸园区+s0+s1(比例8:1:1)
- 1/3新生代,2/3老年区
- 当伊甸园区满了触发MinorGC
- 当达到阈值15的时候,晋升(Promotion)到老年代
- 大对象直接进入老年代
MinorGC、MajorGC、FullGC
- MinorGC:只是新生代的垃圾回收
- 只有伊甸园区满了才会触发,幸存区满不会
- MajorGC:只是老年代的垃圾回收
- 执行之前至少执行一次MinorGC
- 目前只有CMS GC会有单独收集老年代的行为
- Mixd GC:收集整个新生代以及部分老年代的垃圾回收
- 目前,只有G1 GC会有这种行为
- Full GC:收集整个java堆和方法区的垃圾收集
方法区(原空间)
- 独立于堆的内存空间
- 方法区的大小决定了系统可以保存多少个类
- 加载大量的第三方jar包
- 部署工程过多
- 大量动态生成的反射类
- 以上都会造成方法区溢出
- 类型信息、运行时常量池、静态变量、即时编译后的代码缓存
- 运行时常量池
- 是方法区的一部分
- 常量池表存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 具备动态性
- 运行时常量池
- 回收
- 只要常量池中的常量没有被任何地方引用,就可以被回收
- 判断类是否可以被回收
- 该类的所有实例被回收
- 加载该类的加载器被回收(很难达成)
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
执行引擎
java是半编译半解释型语言
- 解释器(解析执行)
- 逐行执行翻译成机器指令,效率低下,但响应速度快
- 字节码解释器、模板解释器(与模板函数相关联)
- JIT编译器(编译执行)
- 字节码编译程机器指令(翻译成机器指令耗时)
- 将热点代码缓存
- 栈上替换
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qAwnMf6h-1628844016597)(JVM.assets/image-20210807103553597.png)]
- 热点代码缓存在元空间,在jdk8以后元空间使用的是本地内存,故本地内存也可以
- 热度衰减:超过一定时间限度,这个方法的调用计数器就会被减少一半
本地方法库
- 与java环境外的交互
- 与操作系统交互
- Sun’s Java
- native关键字
对象实例化
- 创建对象的方式
- new
- Class的newInstance():jdk8能用,jdk9过时(使用起来苛刻,只能调用无参构造,权限必须是public)
- Constructor的newInstance(XXX):反射,可以调用无参构造和有参构造,权限没有要求
- 使用clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone方法
- 使用反序列化:从文件或者网络中获取对象的二进制流
- 第三方库Objenesis
- 创建对象的步骤
- 判断对象对应的类是否被加载、链接、初始化
- 为对象分配内存
- 处理并发安全问题
- 初始化分配到的空间
- 设置对象头
- 执行init方法进行初始化
四大垃圾回收算法
标记阶段
- 引用计数算法
- 有引用计数器就+1,引用失效计数器就-1,只要计数器为0就回收
- 没法处理循环引用的情况,导致java的垃圾回收器没用使用这一算法
- 可达性分析算法
- 以根对象(GC Roots)集合为起始点,从上之下搜索对象是否可达,不可达的就是垃圾
- GC Roots:
- 虚拟机栈中引用的对象
- 本地方法栈内引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 解决了循环引用的问题
清除阶段
- 标记-清除算法(Mark-Sweep)
- 停止整个程序(STW stop the world),需要停顿,所以体验不好,效率也不高
- 标记:从根节点出发,标记到非垃圾对象
- 清除:对堆内存从头到尾进行遍历,如果发现某个对象在其Header中没有被标记,将其回收
- 复制算法(Copying)
- 准备两块相同的空间AB,A存数据,清除的时候把有用的复制到B,随后清除A,然后交换
- 运行高效,没有碎片
- 但是需要两倍的内存空间,开销大
- 适用于年轻代的垃圾回收
- 标记-压缩算法(Mark-Compact)
- 从根节点出发,标记可用对象
- 将存活对象压缩到内存的一段,按顺序存放(连续存储)
- 清除其余空间的所有无用对象
- 适用于老年代的垃圾回收
- 效率比较低
- 分代收集算法
- 新生代采用复制算法
- 老年代采用标记-清除和标记-压缩算法
七大垃圾回收器
- Serial单线程垃圾回收器
- 要停止所有工作线程
- 默认新生代收集器
- ParNew垃圾收集器
- 是Serial的多线程版
- 新生代
- Parallel Scavenge并行垃圾回收器
- 新生代收集器
- 使用的复制算法
- 多线程
- Serial Old垃圾回收器
- 老年代收集器
- 单线程
- 标记-压缩算法
- Parallel Old垃圾回收器
- 老年代收集器
- 多线程
- 标记-压缩算法
- CMS垃圾回收器
- 标记-清除算法
- 并发收集、低停顿
- G1垃圾收集器
- 标记-压缩算法
- 精确的控制停顿,明确指定一个时间段内执行
rial的多线程版 - 新生代
- Parallel Scavenge并行垃圾回收器
- 新生代收集器
- 使用的复制算法
- 多线程
- Serial Old垃圾回收器
- 老年代收集器
- 单线程
- 标记-压缩算法
- Parallel Old垃圾回收器
- 老年代收集器
- 多线程
- 标记-压缩算法
- CMS垃圾回收器
- 标记-清除算法
- 并发收集、低停顿
- G1垃圾收集器
- 标记-压缩算法
- 精确的控制停顿,明确指定一个时间段内执行