JVM八股文

JVM的内存划分

类的加载机制

垃圾回收机制

JVM的内存划分

为什么要进行内存划分?

因为JVM是仿照真实的机器设置的虚拟机,在真实的操作系统中,对进程的空间是有分区设计的,所以JVM也仿照实现了分区域的设计

示意图

在操作系统中,JVM申请一些内存空间后,会根据自己的需求对这个内存进行分配

举例子

就好比你向别人买了一本书,这本书哪里写什么,写在哪里都是随便你的

JVM具体的是怎么划分的?

示例图

四个核心区域

1.程序计数器

一块很小的区域,用来记录指令执行到哪个地址

2.元数据区

保存当前类加载后的数据,比如类的结构,类的方法和字段的定义,属性,参数之类的

.java通过javac编译成.class字节码文件,JVM的类加载器读取class文件,然后存储到元数据区

3.栈

保存方法的调用关系,每次调用方法,就会进入方法的内部执行,方法执行完毕,就返回调用位置继续执行,大小一般是几MB到几十MB

举例

这个栈也是满足先进后出,此处的栈和数据结构的栈不一样

数据结构栈vsJVM栈

数据结构的栈是一种逻辑结构,规定先进后出,可以用链表,数组实现

JVM栈是JVM中具体的一块区域,他说数据结构栈的实际运用场景

这个是可以理解成当程序启动的时候就会创建main,然后在里面如果调用方法,就会创建一个栈帧,然后执行完弹出,继续执行main,然后如果有新的方法调用就会继续执行上述的过程

JVM这个进程,是由C++代码实现的程序,这个进程本身也是存在一系列的方法调用的

在C++的代码中,又构建处理一个JVM虚拟机程序,通过C++的代码执行class文件,通过执行字节码的时候,又创建出一个新的JAVA栈,C++实现的栈,JAVA是无法干预的

上述过程可以理解为套娃,操作系统里面有C++的栈,C++里面有JAVA的栈

4.堆

堆是JVM中最大的空间区域,用来保存new的对象的

Test t = new Test();

如果t是一个局部变量,t在栈上

如果t是一个成员变量,t作为new的一部分存在堆上

如果t是一个静态成员变量,t在元数据区

因为堆是JVM最大的内存区域,所以如果堆上的对象不使用,就会被GC回收

总结

元数据区和堆,整个JAVA进程用的都是同一份

只有栈和程序计数器一个进程可能有多份,因为有多线程的存在

JVM类的加载过程

  1. 类加载的步骤

  2. 类加载的双亲委派模型

类加载的步骤

分为三个大的阶段,其中第二个阶段分为三个步骤,所以一共是五个步骤

1.加载

找到.class文件

会根据类的全限定名打开文件,读取文件的内容到内存中

全限定名(类似于java.lang.String)

类加载.class-类对象

2.验证

解析class文件的读到的内容,是否合法,并且把内容转化成结构化的数据

Class文件,属于二进制的文件,这个文件的格式是有明确的规定的

JAVA官方规定

开头的magic是魔数,用来区分不同的二进制的文件类型

U4表示四个字节的无符号整数

U2表示两个字节的无符号整数

Cp_info filed-info 等表示的是其他的结构体

3.准备

给类申请内存空间,申请的空间(全0的空间)

4.解析

针对字符串常量,进行初始化

字符串常量,本身就包含在class文件中,需要在class文件中解析出来,然后放到内存空间(元数据区或常量池)

5.初始化

针对类对象的各种属性进行填充,如果这个类还有父类没加载,也会触发父类的类加载

双亲委派模型

Parent,更准确来说,并不是双亲,而是双亲中的一方,更适合叫做父亲委派模型

类加载器:JVM中,有专门的模块,负责类的加载

JVM提供了三种类加载器

BoostrapClassLoader

ExtensionClassLoader

ApplicationClassLoader

这三个类加载器负责查找的范围是不一样的

双亲委派模型过程
说人话

当儿子拿到任务之后,不干,丢给父亲干,父亲不干,丢给爷爷干,爷爷能干就干完,干不了,丢给父亲,父亲能干就干,不能干,丢给儿子,儿子能干就干,干不了,那得了,直接就抛异常

注意,类加载器,程序员也是可以自己定义的,你可以选择把你的类加载器放到双亲委派中去,也可以选择不放

垃圾回收(GC)

JAVA中释放内存的手段,就是通过垃圾回收(GC)

GC

GC,会自动帮我们释放内存,那这么好用C++为什么不引入?

GC虽然会自动释放内存,但是他有一个问题STW(stop the world)

在触发了大规模的GC的情况下,会导致其他的业务代码停下,等GC结束再继续执行,而C++考虑的是极致的性能,所以就不考虑,在jdk17之后基本上GC可以控制在1ms之内了。

GC回收的是哪里?

GC回收的主要是堆上的数据,有很多新对象的产生,同时也会有很多的旧对象消亡

GC的工作流程

  1. 找到垃圾(不使用的对象)

  2. 释放垃圾(释放掉对应的内存)

找到垃圾

引入计数

常见于Python和PHP

原理:每个对象在new的时候,会搭配一个小的内存空间,保存一个整数,这个整数就表示当前有多少个引用指向他,每次进行引用赋值的时候,就会自动触发计数的修改,通过引用计数就知道有多少个引用,当引用为0的时候,就认为这个对象是垃圾

弊端:
内存消耗更多

当对象本身就很小的情况下,引入计数的消耗空间的比例就很大

假设引用计数是四个字节,对象本身是八个字节

那么引用计数就提高了50%的空间占用率,这个可是非常恐怖的情况

可能出现循环引用的情况

彼此之间相互引用,但是确保标签ab都撕了

构成了类似于死锁的情况,导致这两个的引用都不为0,这两个对象就都无法使用

可达性分析

JAVA中采用的就是可达性的分析

引入计数,空间花销变大

可达性分析,使用时间换空间

以一些特定的对象作为遍历的起点
  1. 栈上的局部变量(引用类型)
  2. 常量池引用指向的对象
  3. 静态成员(引用类型)
  1. 尽可能的进行遍历

判断某个对象是否能够访问到

每次访问到一个对象就会把这个对象标记成可达

当所有的对象都被遍历完之后,未被标记成可达的对象,就是不可达,那么就会被标记成垃圾,接下来就要等待GC的回收

举例

会一步一步,一个个都去遍历,找到就标记可达,找不到就标记不可达

在这段代码中,模拟了可达性分析的过程,JVM会一步步往后走,能到达就标记可以,不可以就标记成垃圾,这样就可以确保找到垃圾

如果把root.right.right标记成null,那么在下一轮的GC扫描中,会发现这个对象不可达,就会标记成垃圾,就会被清除

GC的扫描是周期性的,隔一段时间就会触发,这个也是C++为什么不引入GC的原因,影响效率。

如何进行垃圾回收?

标记-清除

把垃圾对象的内存做个标记,然后直接清除,这样做会产生内存碎片问题

清楚之后的空间并不是连续的,这个就会导致本来是4GB的内存,需要浪费1GB进行存储这些碎片,而且由于申请内存空间的时候,必须是连续的,不能是这种断开的,这就会导内存没放东西,但是就是被卡着,不给你用

复制算法

会申请两倍的空间,一半用来存储,一半用来复制,每次使用就只使用其中的一部分

把不是垃圾的对象拷贝到一侧,是垃圾的就直接清楚,然后把一侧彻底释放,这样就解决了前面的内存碎片问题

弊端

  1. 空间利用率低
  2. 一旦不是垃圾的对象很多,或者对象很大,那么复制的成本就会很高
标记-整理

在优化了内存碎片的同时保证内存的利用率

使用一个数组,每当产生了垃圾的时候,就使用后一个覆盖前一个,这样就解决了空间的问题,类似于顺序表的搬运

弊端

依旧还是复制,如果很大的情况下,依旧是需要消耗大部分的资源

分代回收(JAVA的答案)

代,表示的是年龄,也就是被GC扫描的次数

某个对象被GC的可达性分析一轮,如果不是垃圾,那么该对象的年龄就+1

策略

针对不同的年龄对象,采用不同的策略

  1. 如果是年龄大的对象,那么大概率就会存在很久,扫描的次数就会降低
  2. 如果是年龄小的,大概率会很快就夭折,所以扫描的次数就会很高

(能活下来的就是老油条,肯定有能活着的本事,小年轻就不好说了,容易夭折)

过程

创建一个对象,这个对象首先会被放在伊甸园,然后经过GC的扫描,这个时候大部分的对象都会被消灭,剩下的会通过复制算法,复制到幸存者区,在幸存者区中,已经剩下很少的对象了,这个时候,也是要被GC扫描的,也会被淘汰掉一部分,然后剩下的会被通过复制算法,再被复制到新的幸存者区,来回反复,如果经历了多次的GC扫描都没问题的情况下,那么就会被复制到老年代,这里也会接受GC的扫描,不过次数会减少

因为新生代的对象,大部分都会很快就被GC干掉,所以这个时候的复制算法的开销就是可控的,老年代的生命周期都是很长的,所以开销也是可控的

注意:当某个对象足够大的时候,就会直接进入老年代,不需要进行淘汰

GC垃圾回收的基本单位是对象

举例

伊甸园:公司收到大量简历,筛选掉大部分

幸存区:经过笔试和面试剩下的人

老年代:进入公司之后虽然成为了正式员工,但是还是有绩效考核

注意:背景够用可以直接不用面试直接入职(老板儿子)

相关推荐
system_error_x13 分钟前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
酷炫码神26 分钟前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
驭渊的小故事44 分钟前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客1 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1881 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su2 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
传奇开心果编程3 小时前
【Rust入门练中学】第5课:引用与借用
开发语言·rust·引用·编程语言理论·通用编程语言·借用
qetfw3 小时前
Bash 变量:export、位置参数与默认值展开验证
linux·开发语言·chrome·bash
电子云与长程纠缠3 小时前
UE5魂系游戏开发教程05
开发语言·学习·游戏·ui·ue5