JVM相关知识点

JVM相关知识点_第1张图片

目录

​编辑一、JVM 内存区域划分

1、栈 

2、堆

3、元数据区(方法区)

二、JVM 类加载机制

1、类加载机制介绍

2、双亲委派模型(经典问题)

三、JVM 垃圾回收机制 GC

1、了解 GC

2、GC 实际工作过程:

(1)找到垃圾 / 判定垃圾

(2)对象的释放


一、JVM 内存区域划分

JVM 也就是启动的时候,会申请到一整个很大的内存区域

JVM 是一个应用程序,要从操作系统这里申请内存(相当于租了个写字楼)

JVM 就根据需要,把整个空间,分成几个部分,每个部分各自有不同的功能作用

1、栈 

JVM相关知识点_第2张图片

2、堆

堆,是整个 JVM 空间最大的区域

new 出来的对象,都是在 堆 上,类的成员变量,也是在堆上面

堆是一个进程只有一份的,栈是每个线程有一份,一个进程有多个

堆,多个线程用的都是同一个堆,栈,每个线程用自己的栈

每个 jvm ,就是一个 java 进程,如果弄两个 java 进程,就是两个 jvm 了


3、元数据区(方法区)

JVM相关知识点_第3张图片

类对象,常量池,静态成员等都存放在这里


如何判断某个变量在哪个区域上?

1、局部变量:在 栈 上

2、普通成员变量:在 堆 上

3、静态成员变量:在 方法区 / 元数据区

二、JVM 类加载机制

1、类加载机制介绍

准确的来说,类加载就是 .class 文件,从文件(硬盘)被加载到内存中(元数据区)这样的一个过程

JVM相关知识点_第4张图片

加载:把 .class 文件找到(找的过程),打开文件,读文件,把文件内容读到内存中

验证:根据 jvm 虚拟机规范,检查下 .class 文件格式是否正确

准备:给类对象分配内存空间(现在元数据区占个位),此时内存初始化成全为0,也会使静态成员设置成 0 值

解析:初始化字符串常量,把符号引用转为直接引用

初始化:调用构造方法,进行成员的初始化,执行代码块,静态代码块,加载父类 .....

把符号引用转为直接引用:字符串常量,得有一块内存空间,来存这个字符的实际内容,还得有一个引用,来保存这个内存空间的起始地址

在类加载之前,字符串常量,此时是处在 .class 文件中的,此时 “引用” 记录的并非字符串常量的真正的地址,而是它在文件中的 “偏移量” 这个东西(或者是个占位符)

类加载之后,才真正把这个字符串常量给放到内存中,此时才有 “内存地址”,这个引用才能真正被赋值成指定内存地址(直接引用)

JVM相关知识点_第5张图片

类加载到底什么时候会触发?

不是 java 程序一运行,就把所有的类都加载的,而是真正用到才加载(懒汉模式)

1、构造 类 的实例

2、调用这个类的静态方法 / 使用静态属性

3、加载子类,就会先加载其父类

一旦加载过之后,后续再使用,就不必重复加载了


2、双亲委派模型(经典问题)

双亲委派模型,描述的是这个加载,找 .class 文件的基本过程

JVM 默认提供了三个类加载器,他们三个各有分工

1、BootstrapClassLoader ,负责加载标准库中的类(Java 规范,要求提供哪些类,无论是哪种 JVM 的实现,都会提供一样的类)

2、ExtensionClassLoader ,负责加载 JVM 扩展库中的内容(规范之外,由实现 JVM 的厂商 / 组织,提供的额外的功能)

3、ApplicationClassLoader ,负责加载用户提供的第三方库 / 用户项目代码中的类

上述三个类,存在 “父子关系”,(不是父类子类,相当于每个 classLoader 有一个 parent 属性,指向自己的父 类加载器

JVM相关知识点_第6张图片

上述三个类加载器如何配合工作?

首先,加载一个类的时候,是先从 ApplicationClassLoader 开始,但是 ApplicationClassLoader 会把加载任务交给父亲,让父亲去进行

于是 ExtensionClassLoader 要去加载了,但是也不是真加载,而是再委托自己的父亲

于是 BootstrapClassLoader 要去加载了,也是想委托自己的父亲,结果发现自己的父亲是 null,没有父亲 / 父亲加载完了,没找着类,才由自己进行加载

此时,BootstrapClassLoader 就会搜索自己负责的标准库目录相关的类,如果找到就加载,如果没找到,就继续由子 类加载器 进行加载,于是 ExtensionClassLoader 真正搜索扩展库相关的墓库,如果找到就加载,如果没找到就还是由子 类加载器 进行加载

ApplicationClassLoader 真正搜索用户项目相关的目录,没找到就由子类加载器进行加载(由于当前没有 子 了,就只能抛出 类找不到 这样的异常)

为什么要有上述这个顺序?

上述这套顺序,其实出自于 jvm 实现代码的逻辑,这段代码大概是类似于 “递归” 的方式写的

这个顺序,最主要的目的就是为了保证 BootstrapClassLoader 能够先加载,ApplicationClassLoader 能够后加载,这就可以避免用户创建了一些奇怪的类,引起不必要的 bug

假设用户在自己的代码中,写了个 java.lang.String 这个类,按照上述加载流程, jvm 加载的还是标准库的类,不会加载到用户自己写的类

这样就能保证,即使出现上述问题,也不会让 jvm 已有代码混乱,最多是用户自己写的类不生效罢了

再另一方面,类加载器其实是可以用户自定义的,上述三个类加载器是 jvm 自带的,用户自定义的类加载器,也可以加入到上述流程,就可以和先有的加载器配合使用

站在 jvm 的角度,类加载的核心应该是:解析 .class 文件,解析每个字节是干什么的

破坏双亲委托模型:

自己写的类加载器,可以去遵守也可以不遵守,是否遵守,主要是看需求

tomcat ,,去加载 webapp 这里就是单独的类加载器,不遵守双亲委派模型


三、JVM 垃圾回收机制 GC

1、了解 GC

什么是垃圾?

垃圾指的是不再使用的内存

垃圾回收就是把不用的内存帮我们自动释放了

C 语言中有 malloc ,C++ 有 new ,这些属于动态申请内存(在堆上申请一块内存空间)

上述内存空间需要手动方式进行释放:free,delete

如果不手动释放,这块内存的空间就会一直存在,一直存在到进程结束(堆上的内存生命周期比较长,不像 栈,栈的空间会随着方法的执行结束,栈帧自动销毁而自动释放,堆则默认不能自动释放

这可能会导致一个很严重的问题:内存泄露

如果内存一直占着,又不释放,就会导致剩余空间越来越少,进一步导致后续的内存申请操作申请失败

因为内存泄露是一个很严重的问题,因此大佬们就想了一些办法来解决这个问题,垃圾回收(GC)是其中最主流的一种方式

GC 的好处:非常省心,让程序员写代码简单点,不容易出错

GC 的坏处:需要消耗额外的系统资源,也有额外的性能开销

另外,GC 这里还有一个比较关键的问题:STW 问题(stop the world)

1、如果有时候,内存中的垃圾已经很多了,此时触发一次 GC 操作,开销可能非常大,大到可能就把系统资源吃了很多

2、另一方面,GC 回收垃圾的时候,可能会涉及一些 锁 操作,导致业务代码无法正常执行

JVM 里面有很多块内存区域,GC 主要是针对其中的 堆 进行释放

GC 是以 “对象” 为基本单位进行回收的!!!(而不是字节)

GC 回收的是整个对象都不在使用的情况,而一部分使用,一部分不使用的对象,暂时先不回收

要回收,就是回收整个对象,而不会回收半个对象,因此我们说 GC 是以对象为基本单位进行回收的

这样设定,目的就是 “简单”


2、GC 实际工作过程:

(1)找到垃圾 / 判定垃圾

判定哪个对象以后一定不用了,哪个对象后面还可能使用

关键思路:抓住这个对象,看看到底有没有 “引用” 指向它

在 java 中,只有一条路:通过引用来使用!!!

如果一个对象,有引用指向它,就可能被使用到,如果一个对象没有引用指向,就一定不会再被使用了

具体如何知道对象是否有引用指向?

两种典型实现:

(1)引用计数 [不是 java 的做法]

给每个对象,都分配了一个计数器(整数)

每次创建引用指向该对象,计数器就 +1,每次该引用被销毁了,计数器就 -1

JVM相关知识点_第7张图片

缺点:

1、内存空间浪费的多(利用率低)

每个对象都得分配一个计数器,如果对象特别多,占用的额外空间就会很多(尤其是对象特别小的情况)

2、循环引用的问题

JVM相关知识点_第8张图片

接下来,如果 a 和 b 引用销毁,此时 1 号对象和 2 号对象引用计数都 -1,但是结果都还是 1,不是 0

虽然不是 0,不能释放内存,但是实际上这两个对象已经没有办法被访问到了


(2)可达性分析 [ java 的做法]

java 中的对象,都是通过引用来指向并访问的

经常,是一个引用指向一个对象,这个对象里的成员,又指向别的对象JVM相关知识点_第9张图片

整个 java 中所有的对象,就通过类似于上述的关系,通过这种 链式 / 树形 结构,整体给串起来

可达性分析,就是把所有这些对象被组织的结构,视为是 树,从树根节点出发,遍历树,所有能被访问到的对象,标记为 “可达”,不能被访问到的,就是不可达

每次你 new 一个对象,jvm 都会记录下来,jvm 会知道一共哪些对象,每个对象的地址...

相当于 jvm 自己捏着一个所有对象的名单,通过上述遍历,把可达的标记出来,剩下的不可达的就可以作为垃圾进行回收了

可达性分析,需要进行类似于 “树遍历” 这个操作,相比于引用计数来说,肯定要更慢一些,但是空间利用率提高了,同时解决了循环引用的问题

但是速度慢没关系,上述可达性分析遍历操作,并不需要一直执行,只需要每隔一段时间,分析一遍就可以了(虽迟,但到)

进行可达性分析遍历的起点,称为:GCroots,主要有以下几种:

1、栈上的局部变量

2、常量池中的对象

3、静态成员变量

一个代码中有很多这样的起点,把每个这样的起点都往下遍历一遍即可,就完成了一次扫描过程


(2)对象的释放

如何清理垃圾?,主要是三种基本方法

1、标记清除

简单粗暴,但是存在内存碎片问题

被释放的内存空间是零散的,不是连续的,而申请空间要求的是连续空间

这就可能会导致总的空闲空间很大,但是每一个具体空间都很小,此时申请大一点的空间就会失败了


2、复制算法

JVM相关知识点_第10张图片

复制算法,就是把不是垃圾的对象,复制到另外一半,然后把整个空间删除掉

每次触发算法,都是向另一侧复制,内存中的数据拷贝过去

缺点:空间利用率低,如果垃圾少,有效对象多,复制成本就比较大了


3、标记整理

JVM相关知识点_第11张图片

类似于顺序表删除中间元素,会有元素搬运的操作,保证了空间利用率,同时也解决了内存碎片的问题

缺点:效率不高,如果要搬运的空间比较大,开销也很大


上述做法,并不完美,基于上述这些基本策略,我们又有了一个复合策略:“分代回收”

把垃圾回收,分成不同的常见,有的常见使用这个算法,有的场景使用那个算法,各展所长

分代是怎么分的? 

基于一个经验规律:如果一个东西存在的时间比较长了,大概率会继续长时间持续存在下去

java 对象,要么就是生命周期特别短,要么就是特别长,我们就根据生命周期的长短,分别使用不同的算法

我们给对象引入一个概念:年龄(熬过 GC 的轮次)

我们可以认为:年龄越大,对象存在的时间就越久

JVM相关知识点_第12张图片

伊甸区:刚 new 出来的,年龄是 0 的对象

熬过一轮 GC,对象就要被放到幸存区了

虽然看起来幸存区很小,伊甸区很大,但是一般够放(根据经验规律,大部分的 java 对象都是 “朝生夕死”,生命周期特别短)

伊甸区 => 幸存区 使用的是 “复制算法”

到了幸存区域之后,也要周期性的接受 GC 的考验,如果变成垃圾,就要被释放,如果不是垃圾,就拷贝到另一个幸存区(这两个幸存区同一时刻只存在一个,两个之间使用复制算法来回拷贝)

由于幸存区体积不大,此处的空间浪费,也能接受

如果这个对象,已经在两个幸存区中,来回拷贝很多次,就要进入老年代

老年代,都是年纪大的对象,生命周期普遍更长

针对老年代,也要周期性GC 扫描,但是频率更低了,如果老年代的对象是垃圾了,使用标记整理的方式进行释放

上述是 GC 中典型的垃圾回收算法

实际上,JVM 在实现的时候,会有一些差异,事实上,JVM 有很多的 “垃圾回收实现”,称为 垃圾回收器

回收期具体的实现做法,会按照上述算法思想展开,但是会有一些 变化 / 改进,不同的垃圾回收器,侧重点不同,这里就不再介绍了

你可能感兴趣的:(Java,EE,jvm,服务器,运维,java,算法,后端)