JVM学习

发布于:2024-05-01 ⋅ 阅读:(35) ⋅ 点赞:(0)

背景

JVM和对象分不开,今天就说说JVM和对象之间的关系

步骤

请添加图片描述

首先分析这张图,我们从对象的生命周期入手,从最最宏观来说,对象的生命周期是出生,工作,再死亡,我们要知道对象存在的意义是为了完成我们的逻辑,那对象什么时候出生又在什么时候消亡呢,让我们来看看吧。

是什么

Java 程序运行的时候,编译器会将 Java 源代码(.java)编译成平台无关的 Java 字节码文件(.class),接下来对应平台的 JVM 会对字节码文件进行解释,翻译成对应平台的机器指令并运行。

在这里插入图片描述

由什么构成

JVM 大致可以划分为三个部门:类加载器、运行时数据区和执行引擎。
在这里插入图片描述

① 类加载器
负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。

#② 运行时数据区
JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域主要包括方法区、堆、栈、程序计数器和本地方法栈。

#③ 执行引擎
执行引擎是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器,还包括即时编译器(JIT Compiler)和垃圾回收器(Garbage Collector)。

从这张图里边也能大概看出来对象的加载过程:首先加载Class文件,然后验证(只有符合JVM字节码规范的才能被JVM正确执行)准备(static 分配内存并初始化0 ,null 等)解析(针对类或接口,字段,类接口方法等)初始化(赋值为期望的值)

加载

负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。

分配内存

1、分配的时候按照是否规整分为两种情况指针碰撞和空闲列表

2、按照内存划分:堆,栈,方法区,程序计数器

目的

访问对象:使用句柄访问和使用直接引用访问
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。

HotSpot 虚拟机主要使用直接指针来进行对象访问。

构成

关于堆我有话要说,首先堆由什么构成:对象头,实例数据,对齐补充 (为什么这么划分?举个例子,我们做为一个人,要有姓名,id等信息作为唯一标识(自身运行数据+类型指针+数组长度),这就是对象头,然后就是实例数据,我这个人有胳膊有腿,还有对齐补充,这意味着这一个人会长个,长到某种程度)
在这里插入图片描述

分代

新生代和老年代(这个在垃圾回收的时候还会提到)

问题

内存溢出和内存泄露

内存溢出:申请不到合适的内存
内存泄露::内存被占用无法释放也就意味着无法被使用
这里包括但是不限于:静态集合类,单例模式对象被持有外部引用,IO迟迟不能被释放,ThreadLocal迟不能被释放。

JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的
在这里插入图片描述

本地方法栈和虚拟机栈

方法区

Class对象
常量
静态变量

内存空间初始化

设置对象头和设置构造方法

销毁

垃圾 :判断是否还活着

引用计数法

在这里插入图片描述

可达性分析算法

在这里插入图片描述

目前 Java 虚拟机的主流垃圾回收器采取的是可达性分析算法。这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(Gc Root Set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。

GC Roots 的引用
虚拟机栈中的引用(方法的参数、局部变量等)
本地方法栈中 JNI 的引用
类静态变量
运行时常量池中的常量(String 或 Class 类型)

引用分类

分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。

在这里插入图片描述

弱引用主要用于描述非必需对象,并在垃圾收集时可能被回收;而虚引用则主要用于跟踪对象的回收活动,并获取相关通知。

这里和ThreadLocal内存泄漏有点关系

垃圾回收

收集
收集器

想象一下,你有一堆用过的废纸、瓶子和其他垃圾,你需要找个方式来清理它们,以免家里变得乱七八糟。在虚拟机中,这些“垃圾”就是不再使用的对象,而“清理垃圾”的工作就交给了垃圾收集器来完成。

Serial GC(串行垃圾收集器):最早的时候,虚拟机有一个简单的垃圾收集器,就像你用手一张一张地捡起废纸一样,它逐个检查对象是否还在使用。这种方式虽然简单,但效率不高,特别是当垃圾很多时,会花费很多时间。

ParNew垃圾收集器或Parallel GC:后来,人们发现可以同时用多只手来捡垃圾,这样就能更快地清理完。于是,并行垃圾收集器出现了,它就像有好几只手同时工作,大大提高了清理速度。标记-复制算法实现

Concurrent Mark Sweep GC(CMS):但有时候,你并不想因为清理垃圾而停下来做其他事情。所以,人们又发明了并发垃圾收集器,它在清理垃圾的同时,你还可以继续做一些其他的事情,这样就不会因为清理垃圾而打断你的工作了。标记-清除算法

G1垃圾收集器:再后来,人们发现有些垃圾虽然现在不用,但可能以后还会用,如果直接扔掉太可惜了。于是,出现了可以预测哪些垃圾以后可能还要用的垃圾收集器,它只清理那些确定不再需要的垃圾,这样就能更好地利用空间。

CMS

先标记,再并发找到所有对象,再标记一次,清除。

初始标记(CMS initial mark):
这个阶段是垃圾回收过程的开始。在这个阶段,垃圾回收器会暂停所有的应用线程(也就是“Stop The World”),然后标记出那些直接被GC Roots(垃圾回收的根对象)引用的对象。GC Roots通常是一些静态变量或者活跃线程栈中的对象引用。这个过程是单线程的,并且需要暂停应用线程,但它通常很快完成。

并发标记(CMS concurrent mark):
这个阶段是CMS算法并发性的体现。在这个阶段,垃圾回收器和应用线程是并发执行的,也就是说,应用线程还在正常工作,而垃圾回收器则在后台标记从GC Roots开始可以到达的所有对象。这样,垃圾回收器就不会影响应用线程的执行,从而减少了程序的暂停时间。

重新标记(CMS remark):
在并发标记阶段,应用线程可能还在创建新的对象或者改变对象的引用关系。因此,这个阶段的任务是重新扫描并标记那些在并发标记阶段发生变化的对象,以确保标记的准确性。同样,这个阶段也需要暂停应用线程,但通常这个暂停时间会比初始标记阶段稍长一些。

并发清除(CMS concurrent sweep):
最后这个阶段,垃圾回收器再次和应用线程并发执行。它会清理掉那些在标记阶段被标记为不再使用的对象,释放它们占用的内存。这个阶段同样不会影响应用线程的执行。

G1

先并发标记,再混合收集,有预测的停顿STW

解决了垃圾碎片的问题。

①、并发标记,G1 通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。

②、混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。

选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。

③、可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

垃圾收集算法

主要有三种,分别是标记-清除算法、标记-复制算法和标记-整理算法
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
标记-清除算法:
想象一下,你的书桌上堆满了各种纸张,有些是你还需要的,有些是可以扔掉的。标记-清除算法就像是你拿起每一张纸,判断它是不是“垃圾”(即不再使用的对象),如果是,就把它放到一边准备扔掉。这个过程就是“标记”和“清除”。但这样做有个问题,就是扔完垃圾后,书桌上可能还是乱糟糟的,纸张之间有很多空隙,这就是内存碎片。

标记-复制算法:
为了解决标记-清除算法导致的内存碎片问题,标记-复制算法想出了一个新办法。它把书桌分成两半,平时只在其中一半放纸张。当需要清理垃圾时,它会把这一半中还需要的纸张都搬到另一半去,然后直接把这一半清空。这样,每次清理完垃圾后,书桌都是整整齐齐的,没有碎片。但这种方法也有个缺点,就是书桌的空间利用率不高,因为总有一半是空的。

标记-整理算法:
标记-整理算法则是对标记-清除算法的改进。它不只是简单地标记和清除垃圾,还会把还需要的纸张都移动到书桌的一边,然后清理掉另一边的垃圾。这样,清理完后书桌也是整齐的,而且空间利用率也比标记-复制算法高。

这三种算法各有优缺点,适用于不同的场景。比如,标记-复制算法适用于新生代对象,因为它们大部分都是朝生夕灭的;而标记-整理算法则更适用于老年代对象或整个堆的垃圾回收阶段,因为它能够解决内存碎片问题。总的来说,它们都是为了更有效地管理内存,确保程序能够顺畅运行。

逃逸分析

对象一定分配在堆中吗? 不一定的。

当一个对象被 new 出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸
在这里插入图片描述

好处

栈上分配
如果确定一个对象不会逃逸到线程之外,那么久可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样一来,垃圾收集的压力就降低很多。

同步消除
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。

标量替换
如果一个数据是基本数据类型,不可拆分,它就被称之为标量。把一个 Java 对象拆散,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写。

对象出现问题

JVM 的常见参数配置

-Xms:初始堆大小
-Xms:最大堆大小
-XX:NewSize=n:设置年轻代大小
。。。。

JVM调优

https://blog.csdn.net/weixin_45706856/article/details/129438426?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171411206216800180690664%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171411206216800180690664&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-8-129438426-null-null.nonecase&utm_term=%E5%A0%86&spm=1018.2226.3001.4450

总结

我们沿着对象创建的整个流程就可以基本上梳理出来JVM学习的整个路线,学习不是一蹴而就的。请添加图片描述


网站公告

今日签到

点亮在社区的每一天
去签到