当前位置: 首页 > news >正文

JVM相关

1.JVM内存区域

一个运行起来的java进程就是一个Java虚拟机,就需要从操作系统中申请一大块内存。

内存中会根据作用的不同被划分成不同的区域:

(1)栈:存储的内容是代码在执行过程中,方法之间的调用关系(栈中每一个元素就是一个栈帧,代表一个方法的调用,包含方法的形参,返回值,局部变量)。

(2)堆:这里存储的内容是代码中new的对象

(3)方法区(1.8开始:元数据区):存储类对象(.class文件加载到内存就成为了类对象)

(4)程序计数器:存放每个线程,下一条指令执行的地址。空间较小主要就是用来存放一个“地址”,表示下一条要执行的指令,在内存的哪个地方(在方法区中去找下一条指令,指令是以二进制的形式存储在类对象中)。

程序计数器中的值会随着指令的执行,从而自动进行更新,去指向下一条指令。

如果是顺序执行的代码,那么下一条执行的指令地址就是把指令地址进行递增。

如果是条件循环执行的代码,那么下一条执行的指令地址就可能会跳到比较远的地址。

ps:栈和程序计数器每一个java线程一份,而堆和方法区是每一个java进程一份。(仅限java,放在c++中就不一定是这样了)

2.JVM类加载

(1)基本流程:

java代码经过编译后生成.class文件,java程序要想运行起来就需要将jvm读取这个.class文件,并且把里面的内容构造成类对象,保存到内存的方法区中。

官方文档中把类加载的过程分成了5个步骤:

1)加载:找到.class文件,并且打开文件,读取文件中的内容(通过全限定名称查找)

2)验证:检查当前二进制文件是否符合格式的要求,具体格式

3)准备:给类对象分配内存空间,并且为不被final修饰的static变量赋初始默认值(被final修饰的变量在此时会被赋值成程序员给定的值)

4)解析:针对类对象中字符串常量的处理,进行了一些初始化的操作。(符号引用到直接引用)

字符串常量在经过编译之后,eg:final String s = "hello",s本来是存储内存地址,但在.class文件中会重新针对该字符串进行创建出一个引用,此时这个引用就不是存储内存地址了,而是存储文件偏移量(字符串长度),通过这个变量就可以知道目前字符串处于哪个位置。到解析的阶段(类加载的时候),会重新将这个引用替换成内存地址。

5)初始化:对类对象中的各种属性进行初始化,还需执行静态代码块和加载一下父类(子类中调用构造方法,会先帮助父类进行构造)

(2)双亲委派模型:

是在加载过程中的第一个步骤。

首先需要了解一下类加载器

是JVM中的一个模块,JVM内置了三个类加载器:

1)BootStrap ClassLoader

2)Extension ClassLoader

3)Application ClassLoader

这三个类加载器之间的关系就好似爷父子之间的关系,但不是靠继承构成的,而是由类加载中的一个属性(parent)来指向父类加载器。

加载的具体流程是:

①根据给定的全限定类名,找到对应的.class文件。

②从Application ClassLoader作为入口,开始执行查找的逻辑。

③Application ClassLoader不会立即扫描自己所负责的目录(负责搜索项目当前目录和第三方库的对应目录),而是会交给自己的父类加载器Extension ClassLoader。

④Extension ClassLoader不会立即扫描自己所负责的目录(负责搜索JDK中一些扩展库对应的目录,是JDK标准库之外的一些库,属于对JDK标准库的扩展),而是会交给自己的父类加载器BootStrap ClassLoader。

⑤BootStrap ClassLoader,也不会立即扫描自己所负责的目录(负责搜索JDK标准库对应目录),而是会交给自己的父类加载器,但是却发现没有父类加载器,因此只能区扫描自己负责的目录,一旦在标准库中查找到对应类的.class文件,此时加载的过程就完了,扫描结束过后如果没有扫描到,就会交给它的子类加载器(Extension ClassLoader)扫描。

⑥如果在Extension ClassLoader扫描到了就结束加载过程,没有扫描到就交给它的子类加载器(Application ClassLoader)扫描。

⑦如果在Application ClassLoader扫描到了就结束加载过程,没有扫描到此时应该交给它的子类加载器扫描,但是发现没有了,所以此时会抛出一个异常ClassNotFoundException

总结:所谓的双亲委派模型其实就是一个按照优先级查找.class文件的一个过程,之所以有这么一套流程,是为了确保JDK标准库扫描的优先级最高,其次是扩展库,最后才是项目当前目录和引入的第三方库对应目录。就好比:你在自己的代码中使用String(导入的包是java.lang.String),在类加载的时候加载的JDK标准库中的类,而不是自己写的这个类。

 3.JVM垃圾回收机制(GC垃圾回收)

在java中new对象,是动态申请内存(运行时分配),如果一个资源申请了内存空间,长时间不使用但是不释放,就可能会造成内存泄漏。

在java中给出了一个方案,也就是垃圾回收机制,让JVM,自动判定某个内存是否不再使用了,

如果后面这个内存确实不用了,JVM就会自动回收把这个内存给回收掉了,此时就不需要手动回收了。

GC是垃圾回收,GC回收的目标是内存中的对象。对于java来说就是释放堆上的new出的对象,栈上的对象是随着栈帧的生命周期(方法执行结束,栈帧自然销毁,内存自然释放),静态变量,生命周期是整个程序,这个始终存在就意味着静态变量无需释放的,真正释放的就是堆上的对象。

GC回收垃圾的过程主要有两个步骤:

(1)找到垃圾

有两种主流的方案:

①引用计数

new出来的对象,会单独划分空间,来保存有一个计数器,计的是当前有多少引用指向该对象。

如果一个对象没有引用指向了(即引用计数为0),就可以将该对象视为垃圾了。

但是使用该种方式可能会存在两种问题:

·比较浪费内存:

计数器怎么说也得有两个字节,如果对象本身比较小,那么此时这个计数器所占空间比例就比较大了。

·存在循环引用问题:

②可达性分析:

有一组线程,周期性扫描代码中的所有对象,把所有可以访问到的对象,都给标记成“可达”,反之,如果经过扫描后,不可达的对象,就成垃圾了,需要被回收。

eg:

当然这里的遍历不一定是二叉搜索树,这里可达的实现大概率是靠N叉搜索树实现,这一步就是针对当前对象,看对象中有多少不同引用类型的成员,然后再对每一个类型的成员进行进一步的遍历。

通过上述过程,不难看出可达性分析是比较耗费资源的(开销较大)。 

(2)回收垃圾

有三种基本的回收思路:

①标记删除:

扫描到一个不可达的对象,就直接进行释放,这个方案非常不好,那就是会产生很多的内存碎片。释放代码,是为了让其他代码能够申请一块连续的内存空间,随着时间的推移,内存碎片的情况就会愈演愈烈,就会导致后续申请连续内存空间变得困难。

②复制算法:

通过复制的方式,把有效的对象,归类到一起,在同一释放剩下的对象。

也就是把内存一分为二,一次只用其中一半。但这种方式有两个明显的问题:

a.内存利用率不高

b.如果有效的对象很多,那么复制的开销将会很大

③标记整理:

既能解决内存碎片的问题,又能够解决上述内存利用率不高的问题。

eg:

类似于顺序表删除元素的搬运操作,使用该种方式搬运开销仍然很大。

而JVM中GC垃圾回收主要思想是分代回收(具体实现可能有一些调整和优化),是对上述思路的集合,让不同的方案,扬长避短。

分代回收有一个很重要机制就是:对象能活过的GC扫描轮次越多,就是越老(代表当前对象是暂时不会被释放的)

eg:下图是整个分代回收的全部过程:


http://www.mrgr.cn/news/29667.html

相关文章:

  • 【Git】远程仓库
  • C++中为什么构造函数和析构函数不允许调用虚函数?
  • 浅谈Spring Cloud:Nacos的配置
  • do { ... } while (0) 的意义
  • etsts
  • 英飞凌—TC377芯片详解(2)
  • 「全球大模型竞技场」更新:DeepSeek-V2.5全面领跑国内模型
  • Uinty Collider 有几种?
  • nonlocal本质讲解(前篇)——从滤波到Nonlocal均值滤波
  • B端:分享一波简洁、高颜值的pad端管理界面。
  • WSL中使用AMBER GPU串行版
  • AI修手有救了?在comfyui中使用Flux模型实现局部重绘案例
  • 【MQTT协议使用总结】基于-FreeRTOS平台-移植MQTT协议栈
  • C++编程:多线程环境下std::vector内存越界导致的coredump问题分析
  • [Golang] Context
  • 双指针算法
  • 基于虚拟阻抗的逆变器下垂控制环流抑制策略MATLAB仿真
  • FreeRTOS学习——接口宏portmacro.h
  • 完结马哥教育SRE课程--服务篇
  • GAMES101(2~3作业)