首页 Jvm⚙️
文章
取消

Jvm⚙️

JVM虚拟机(内存区域、垃圾回收机制、类加载机制等)

参考书籍📚:《深入理解Java虚拟机》

一.内存区域

1.程序计数器:记录当前线程在class类文件中运行到第几行
2.栈:

3.堆:
4.方法区:

二.垃圾回收机制

1.什么是垃圾?
没有被引用的对象 即死亡对象 就叫做垃圾
怎么判定为对象是否被引用?

​ -1.引用计数算法 ​ 每次对象被引用计数器就加一 取消引用就减一 虽然实现方法很简单 但是无法解决==循环引用==的问题 ​ ​ -2.可达性分析算法 ​ 每次去清理垃圾时候 从一个叫做GC Root的==根对象==作为起始结点开始向下遍历 如果GC Root 下没有相关联的引用或者说是==引用链== 则对象不可达 是垃圾 需要清理 反之 可达 不是垃圾 ​ 🌟可以作为GC Root的对象: ​ ·虚拟机栈的引用对象:栈帧中的本地变量表:参数、局部变量、本地变量 ​ ·本地方法栈中JNI引用对象:本地方法 ​ ·方法区中 静态变量引用对象:java类的引用类型静态变量 ​ ·方法区中 常量引用对象:字符串常量池中的引用 ​ ·虚拟机内部的引用对象:基本数据类型对应的Class对象、常驻异常对象(空指针、OOM)、系统类加载器 ​ ·所有同步锁持有的对象:Synchronized ​ ​ 三色标记法 ​ 把遍历GC Root过程中遇到的对象 按照是否访问过这个条件标记成以下三个颜色 ​ 白色:在可达性分析完毕后 如果是白色 则说明垃圾收集器没有访问过 说明是对象不可达的 是垃圾 ​ 黑色:表示对象已经被垃圾收集器访问过 并且该对象的所有引用都被扫描过了 说明对象是可达的 不是垃圾 黑色对象不能直接指向某个白色的对象 ​ 灰色:表示对象被垃圾收集器访问过了 但是该对象的有一些引用还没被扫描过 ​ 问题:黑色对象被误认为白色对象 ​ 解决:增量更新和原始快照 ​

对象真正的死亡💀

​ 当被标记为不可达对象时候 不一定是对象真正的死亡 还需要进行一次判断才能死亡宣告 ​ ​ 1.进行可达性分析后 GC Root下没有 引用链 则说明对象不可达 对象被第一次标记 ​ -可达性分析后 对象不可达 ​ 2.如果对象的finalized()方法==没有==被覆盖或者调用 则进入 一个F-Queue队列中 ​ 随后通过一个Finalizer线程执行他们的finalize()方法 ​ 如果此时对象==没有==和引用链上任意一个对象产生关联 那么就被真的判定为死亡对象 ​ 对象没有覆盖或者使用finalized()方法 并且 随后 没有和引用链上的对象产生关联 则对象真正死亡

2.引用的分类

​ -强引用 ​ Object obj = new Object(); ​ 即使堆内存空间不足、宁愿报异常也不不会被GC清理

​ -软引用 ​ 堆内存空间足够大时候 不会被清理 当内存紧张的时候 就会被清理

​ -弱引用 ​ 对象引用只会存活到下一次GC垃圾回收之前

​ -虚引用 ​ 任何时候都可能被垃圾回收

2.垃圾回收算法📒
·标记清除算法

​ 区域: ​ 概述:先在GC区域进行通过可达性分析 进行 ==存活对象的标记== 统一清除回收♻️未标记的对象 ​ 优点:提高系统吞吐量 ​ 缺点: 1.执行效率不稳定 如果堆中有大量对象 而且对象都需要被回收 执行效率就很低下
​ 2.标记-清除后有大量内存碎片🧩 导致大对象得不到分配空间而进行垃圾回收 ​ 细述:🈚️

·标记复制算法

​ 区域:针对堆中==年轻代==的算法 因为年轻代中的对象 来的多 ==但是死的快 最后剩下的对象很少== 每次复制的量少 ​ 概述:先在GC区域进行通过可达性分析 进行 ==存活对象的标记== 然后 ==复制存活对象另一块年轻代内存中== 最后清理之前的那一块年轻代区域 ​ 优点:不需要担心内存碎片的问题 ​ 缺点:浪费空间太大了 ​ 详细: ​ 把年轻代分为:Eden、Survivor From、Survivor To 三个区域(8:2:2) ​ 第一步 在Eden 区域进行 对象的收集和标记 ==MiniorGC== 然后 存活对象复制到 SF 之后清理 Eden 最后 对象 age +1 ​ 第二步 在Eden和SF区进行 对象收集和标记 ==MiniorGC== 然后 存活对象 复制到 ST 之后 清理 Eden和SF 对象age +1 ​ 第三步 把SF和ST区域 进行对换 然后 从第二步开始一直循环 ​ TIP:==空间分配担保== 当一次 MiniorGC 后 复制到对应 survivor区域 空间不足 ​ 具体步骤: ​ 1.进行MiniorGC之前 虚拟机对==老年代最大可用空间==进行判断 判断其==连续可用空间是否 大于新生代所有对象总大小== ​ 2.如果大于说明装的下 则这次MinorGC是安全的 否则是不安全的 ​ 3.如果不安全 则查看 虚拟机 ==是否允许担保失败== ​ 4.如果允许 担保失败 则查看 ==老年代最大连续可用空间== 是否 大于 ==年轻代晋升到老年代的平均对象大小== ​ 5.如果大于 说明 平均条件下 是安全的 ==则进行一次MiniorGC 否则 进行Full GC==

·标记整理算法

​ 区域:针对堆中==老年代==的算法 因为老年代存在大量的对象 标记复制算法效率太低下 ​ 概述:先在GC区域进行通过可达性分析 进行 ==存活对象的标记== 然后 让存活的对象向内存空间一端移动 然后清理掉内存边界以外的内存 ​ 优点:提高系统吞吐量 ​ 缺点:每次移动对象 是一个开销很大的操作 需要暂停用户线程进行Stop the word ​ 细述:🈚️

3.垃圾收集器📒

​ 基于分代收集理论: ​ 年轻代:对象存活少 采用复制算法➕serial(单线程) / parNew(多线程并行) / parallel scavange(多线程并发 吞吐量高 但响应时间慢)

​ 老年代:对象存活多 采用标记-清除算法 ➕ serial old / parallel old / CMS(多线程 吞吐量低 但 响应时间快)

​ 老年代、年轻代:G1垃圾收集器

==年轻代==
·Serial

​ 区域:年轻代 ​ 算法:复制算法 ​ 概述:单线程的垃圾收集器 实现方式简单 但是单线程的方式 会导致在垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好 ​ 优点:实现方式简单高效 没有线程之间的切换 并且它的所有收集器中额外内存消耗最小的 ​ 缺点:垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好

·ParNew

​ 区域:年轻代 ​ 算法:复制算法 ​ 概述:Serai的多线程的并行版本 GC线程使用多个线程 进行垃圾回收 垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好 ​ 优点:GC线程使用多线程 能在垃圾收集的时候 使用多条线程去开启任务 ​ 缺点:垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好

·Parallel Scavenge

​ 区域:年轻代 ​ 算法:复制算法 ​ 概述:多线程并行 可控制的吞吐量 吞吐量优先 ​ 优点:GC线程使用多线程 能在垃圾收集的时候 使用多条线程去开启任务 ​ 缺点:垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好

==老年代==
·Serial Old

​ 区域:老年代 ​ 算法:标志整理算法 ​ 概述:serial 的老年代版本 单线程的垃圾收集器 实现方式简单 单线程的方式 会导致在垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好 ​ 优点:实现方式简单高效 没有线程之间的切换 并且它的所有收集器中额外内存消耗最小的 ​ 缺点:垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好

·Parallel Old

​ 区域:老年代 ​ 算法:标记整理算法 ​ 概述:Parallel Scavenge 老年代版本 多线程并发收集 可控制的吞吐量 吞吐量优先 ​ 优点:GC线程使用多线程 能在垃圾收集的时候 使用多条线程去开启任务 ​ 缺点:垃圾收集的时候必须暂停用户线程 stop the word 用户体验感不好

·🌟CMS(Concurrent Mark Sweep)

​ 区域:老年代 ​ 算法:标记清除算法 ​ 概述:一种追求最短回收停顿时间 低停顿的垃圾回收器 ​ 优点:最短的回收停顿时间 并发收集 ​ 缺点: ​ 1.采用并发清除算法 会产生内存碎片 不好清除 可能导致提前的Full GC ​ 2.面向并发设计 对cpu处理器资源比较敏感 会占用一部分线程 导致应用程序变慢 ==降低总吞吐量== ​ 3.无法处理浮动垃圾(并发标记和清除 时gc和用户线程 同时运行 此时用户线程可能有新的垃圾对象 只能等到下一次再清理) 可能导致一次Full GC

​ 详述: ​ 初始标记:暂停用户线程 开始GC线程 去标记GC Root能直接关联到的对象 ​ 并发标记:GC线程开启的同时 开启用户线程 去遍历整个GC Root引用链 ​ 重新标记:用户线程暂停 重新去标记产生变化的引用 由于用户线程的使用 导致之前标记的引用发生变化 ​ 并发清除:开启用户线程的同时 同时开启GC线程去清理不可达对象

·🌟G1(Garbage First)

​ 区域:年轻代和老年代 ​ 算法:标记清除算法 ​ 概述:不再进行分区的垃圾收集 面向堆内存任何部分组成回收集 把堆内存划分为大小相等的独立区域Region 每个Region都可以是Eden SF ST 或者老年代 ​ G1收集器 去跟踪每个Region里垃圾的价值大小 把那些最有价值的内存区域进行回收 (价值即:回收空间的大小以及回收所需要的时间) ​ 优点:工作线程 并行收集 ==用户可以指定期望停顿时间==(分析每个Region的花费成本 得出平均值、标准偏差 通过这些信息去预测停顿时间) ​ 缺点: ​ 1.Region数量比传统的收集器要多 因此G1收集器比其他传统的垃圾收集器有更多的内存占用 ​ 详述: ​ 初始标记:暂停用户线程 开始GC线程 去标记GC Root能直接关联到的对象 ​ 并发标记:GC线程开启的同时 开启用户线程 去遍历整个GC Root引用链 找出要回收的对象 遍历完成后 还要处理在并发时有引用变化的对象 ​ 最终标记:用户线程暂停 用于处理并发标记后 仍遗留下来的那最后少量的记录 ​ 筛选回收:用户线程暂停 对Region的价值和成本进行排序根据用户期望停顿时间进行定制回收计划 由多条线程并行完成

三.类加载机制

1.类加载机制的概述
·把class类文件 加载到内存 并且对数据进行校验 转换解析和初始化 最终形成可以被虚拟机直接使用的java类型
·把整个过程都是在程序的运行期间完成的
2.类加载机制的时机
·解析阶段不一定严格按照类加载的顺序来 某些情况下可以在初始化阶段后执行(Java的动态绑定)
·以下六种情况初始化必须立刻执行

1.遇到new、getstatic、putstatic、invokestatic 这四条字节码指令 ·new关键字实例化对象 ·读取一个非final的静态字段 ·调用静态方法 2.进行反射调用时候 3.初始化时 发现没有对父类进行初始化 必须立即对父类进行初始化 如果是类的话 父类没有初始化 那么必须先去初始化 如果是接口 父接口没有初始化 不要求其父接口都完成了初始化只有真正使用到的时候才会初始化

4.虚拟机启动时 主类必须进行初始化 5.当接口中定义了JDK8新加入的默认方法 如果实现类发生了初始化那么 接口要在它之前初始化

3.类加载机制的过程
·加载

​ ·在class类文件中方法区的常量池的符号引用找到代表该类的==全限定名== 将其转化为代表该类的二进制字节流 ​ ·把该字节流代表的静态数据结构转化成方法区的动态数据结构 ​ ·在内存生成一个代表该类的class对象 程序通过这个对象 可以访问 方法区中该类各个数据

·验证

​ ·class类文件格式验证:魔数号、主次版本号、常量池是否有不支持的类型 ​ ·元数据验证:这个类是否有父类、这个类的父类是否继承了不允许被继承的类 ​ ·字节码验证:对类的方法体进行校验 保证不会对虚拟机做出危害的行为 ​ ·符号引用验证:符号引用的类、字段、方法能否被当前类访问

·准备

​ ·通常情况下把非final的静态类型的变量进行内存的分配以及默认值的设置 ​ ·特殊情况下如果是final 修饰的静态变量 那么就不会进行默认值的分配 而是设置为指定的值 ​ -原理是 ==类字段的字段属性表中存在 ConstantValue属性== 在准备阶段就会被设置为ConstantValue的值

·解析

​ 把class类文件的中方法区的符号引用改为直接引用 ​ 符号引用:一组符号来描述所引用的目标 符号可以是任何形式的字面量 ​ 直接引用:直接指向目标的指针 间接定位到目标的句柄

·初始化

​ 使用构造器的clinit()方法 对非final修饰的变量进行类的初始值的赋值

4.类加载器

类加载到第一步加载:通过类的全限定名找到该类 并且把转化成对应的二进制字节流 这一步是在虚拟机外部去完成 去做这一步的东西叫做 类加载器

类加载器是加载类的

对于任意一个类都必须由加载这个类的加载器和类共同确认在虚拟机的唯一性

·启动类加载器

​ 把java home /lib 下的类加载到虚拟机 ​ ====启动类加载器 底层是c++实现的 虚拟机自身的一部分== ​ ==其余类加载器 底层是java实现的 存在于虚拟机外部 继承classLoader类==

·扩展加载器

​ 把java home /lib/ext 下的类加载到虚拟机

·应用程序类加载器

​ 把class path下的类加载到虚拟机 如果用户没有自定义类加载器 ==那么应用程序类加载器就是默认的类加载器==

·自定义类加载器

​ 继承classLoader类

5.双亲委派模型
·概述

当收到一个类加载的请求的时候 当前类加载器首先不会去处理而是去查看其父类加载器 如果有的话就交给上层的父类加载器 没有的话就自己处理

·为什么这么做

基础类都是被最上层的启动类加载器加载 在各个类加载器中保证是同一个类 保证java程序的安全和稳定 保护程序安全, 防止核心API被随意篡改

·双亲委派模型的打破

1.线程上下文类加载器 基础类都是由最上层的启动类加载器加载的 如果基础类型想调回用户代码 可以打破双亲委派模型

2.代码热部署 在OSGI的环境下 类加载器不再是双亲委派模型下的树型结构 而是更加复杂的网状结构

3.只要重写loadClass方法就可以破坏 例子:tomcat自定义了类加载器,重写loadClass方法使其优先加载自己目录下的class文件,来达到class私有的效果。

·缺点❌

顶层的ClassLoader 无法访问底层的 ClassLoader 所加载的类

本文由作者按照 CC BY 4.0 进行授权

Os Inputandoutput

Juc💎