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垃圾回收的基本单位是对象

举例

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

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

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

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

相关推荐
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于SpringBoot的企业销售合同管理设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
jiayong232 小时前
Spring AI Alibaba 深度解析(一):框架概述与核心功能
java·人工智能·spring
AAA简单玩转程序设计2 小时前
Java 异常处理:3 个 “避坑神操作”,告别崩溃式报错
java
徐老总2 小时前
圆形(Circle)和矩形(Rectangle)两个样例类
java
今晚打老虎2 小时前
c++之基础A(二维数组)第四课
开发语言·c++
一只努力的微服务2 小时前
【Calcite 系列】将 INTERSECT 转换为 EXISTS
java·calcite
向往着的青绿色2 小时前
编程式事务,更加精细化的控制
java·开发语言·数据库·spring·性能优化·个人开发·设计规范
ホロHoro2 小时前
数据结构非线性部分(1)
java·数据结构·算法
Rinai_R2 小时前
Go 的调度模型
开发语言·后端·golang