【JavaEE初阶】JVM内存划分和类加载过程以及垃圾回收

目录

🌲内存划分

🚩堆(线程共享)

🚩栈

🚩元数据区

🍃类加载过程

🚩双亲委派模型

🎄垃圾回收机制(GC)

🚩找到谁是垃圾(不被继续使用的对象)

🚩释放对应的内存

🏀标记-清除

🏀复制算法

🏀标记-整理

🏀分代回收


🌲内存划分

JVM也就是Java进程,这个进程一旦跑起来之后,就会从操作系统这里,申请一大块内存空间,JVM接下来就要进一步的对这个大的空间进行划分,划分成不同区域,每个区域都有不同的作用。

具体如何划分的呢?

JVM运行时数据区域也叫内存布局,但需要注意的是它和Java内存模型((JavaMemoryModel,简 称JMM)完全不同,属于完全不同的两个概念,它由以下5大部分组成:

🚩堆(线程共享)

堆的作用:程序中创建的所有对象都在保存在堆中。(也就是new出来的对象)

成员变量也是在堆中,new出来的对象包含了成员变量,这些东西是一起的

对于里面的新生代来年代后续讲述

🚩栈

分为Java虚拟机栈和本地方法栈。

保存了方法的调用关系,例如写代码时A调用B,B调用C......,这里的调用就是使用栈来维护,只不过虚拟机栈放的Java代码的调用关系,而本地方法栈是针对JVM内部的调用关系,也就是C++代码的调用关系

注意:上述的栈和堆与数据结构的栈和堆没有任何关系,只是名字相同

🚩元数据区

以前叫做方法区,从Java8开始,叫做元数据区。

里面放的是"类对象"。

还放了方法相关信息,类中有一些方法,每个方法都代表了一系列指令集合(JVM字节码指令),还有常量池,编译出来的字节码。

🚩程序计数器(PC)

他是内存区域中最小的区域,只需要保存当前要执行的下一条指令(JVM字节码)的地址。

具体代码实现:

基本原则:

一个对象在哪个区域,取决于对应变量的形态。

  • 1)局部变量 =>栈上
  • 2)成员变量 =>堆上
  • 3)静态成员变量 =>元数据区/方法区

补充:上述四个区域中,堆和元数据区是整个进程只有一份,栈和程序计数器是每个线程都有一份,则堆和元数据区都是多个线程共享同一份数据,每个线程的局部变量,则不是共享的,每个线程都是有自己的一份。

🍃类加载过程

当前写的Java代码,是一个 .java文件,是在硬盘上的,一个Java进程要跑起来,需要先把 .java文件变成 .class文件,还是在硬盘上,在加载到内存中,得到"类对象"。

一个Java进程要跑起来,也就是要执行指令,要执行的cpu指令,都是通过字节码让JVM翻译出来,也就需要让字节码进入到内存中。

接下来我们来看下类加载的执行流程。

对于一个类来说,它的生命周期是这样的:

  • 1)加载

在硬盘上,找到对应的 .class文件,读取文件内容

  • 2)验证

检查 .class文件的内容是否符号要求。

.class文件是由javac编译器生成的,具体生成的 .class文件里面具体是什么样的格式,在Java官方文档中是有明确定义的。

  • 3)准备

给类对象分配内存空间。

  • 4)解析

针对字符串常量进行初始化,把刚才 .class文件中的常量的内容取出来,放到元数据区

  • 5)初始化

针对"类对象"中的各个部分进行初始化(不是针对对象初始化,和构造方法无关),给执行静态成员,执行静态代码块进行初始化等。

面试:记住上述5个步骤,以及各个变量的内存区域

🚩双亲委派模型

双亲委派模型出现在上述"加载"这个环节,根据代码中写的"全限定类名"找到对应的 .class 文件。

全限定类名指 包名 + 类名。例如String => java.long.String ,List => java.util.List。

双亲委派模型描述了JVM加载 .class文件过程中,找文件的过程。这就涉及到"类加载器"

"类加载器"在JVM中包含了一个特定的模块/类,这个类负责完成后续类加载的过程。

JVM中内置了三个类加载器:负责加载不同的类

  • 1)BootstrapClassLoader:负责加载标准库的类
  • 2)ExtentionClassLoader:负责加载JVM扩展库的类(前面学习过程中没有涉及到任何扩展类,历史遗留,本身很少使用)
  • 3)ApplicationClassLoader:负责加载第三方库的类和你自己写的代码的类

他们三个类存在一个父子关系:这个父子关系不是继承表示的,而是通过类加载器中存在一个"parent"这样的字段指向自己的"父亲"。

注意:"双亲委派模型"本身翻译是不标准的,更准确的翻译为"父亲委派模型"。

工作过程:

例如,给定了一个类的"全限定类名",自己写的类 => java111.Test

这就是双亲委派模型,拿到任务,先交给父亲处理,父亲处理不了,再自己处理。

上述过程主要为了应对场景:

比如你自己代码中写了一个类,这个类的名字和标准库/扩展库冲突了,JVM就会确保加载的类是标准库中的类(就不加载你自己写的类了)。相当于我自己写了一个java.long.String,那么这套模型就能够确保最终在JVM中加载原有的java.long.String了。

类加载过程中的双亲委派模型也是一个经典面试题。

🎄垃圾回收机制(GC)

垃圾回收机制,是Java提供的对于内存(变量或者对象)自动回收的机制。

GC回收的是"内存",更准确的说是对象,回收的是堆上是内存。

一定是一次回收一个完整的对象,不能回收"一部分对象"。

GC的具体流程,主要有两个步骤:

🚩找到谁是垃圾(不被继续使用的对象)

谁是垃圾这个事情,并不太好找,一个对象什么时候创建这个是明确的,但什么时候不在使用,这个时机往往很模糊。在编程中,一定要确保代码中使用的每个对象,都得是有效的,千万不要出现"体现释放"的情况。

因此判定一个对象是否是垃圾,判定方式就比较保守。比如,如果使用"上次使用时间"的方式来判定垃圾,就是不行的,这就容易错杀。

此处就引入了一个比较保守的做法,判定某个对象,是否存在引用指向它。在代码中都是通过对象的引用来使用的,那么如果没有引用指向这个对象,意味着这个对象注定无法在代码中被使用了。这就可以视为这个对象是垃圾了。

例如:Test t = new Test(); t = null; => 修改t的指向,new Test对象没有引用指向了,就视为垃圾。

具体怎么判定某个对象是否有引用的指向呢?方式有很多,此处介绍两种方式:

  • 1)引用计数(不是JVM采取的方案,而是Python / PHP的方案)

引用计数描述的算法为:

给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。

看似比较好用,但是存在两个缺陷:

a)消耗额外的存储空间

如果你的对象比较大浪费空间还好,如果对象比较小,并且对象数目还多,空间占用多了,空间的浪费的就多了。

b)存在"循环引用"的问题(面试官考引用计数,也就是靠你循环引用问题)

例如:

两行实例代码对应的图示

接下来:a.t = b,进行引用复制,把b里面的地址复制给Test类中的引用类型的成员,也就是把b地址复制给a对象中t成员,此时就有两个引用指向0x200了,那么0x200中的引用计数器就为2。b.t = a也是同理。

然后再执行 a = null; b = null;,此时a中就为null,意味着0x100中的引用计数器就为1,b为null,意味着0x200中的引用计数器就为1,这时候这两个对象相互指向对方,就导致了两个对象的引用计数都为1(不为0,不是垃圾),但是你外部代码也无法访问这两个对象!!!

  • 2)可达性分析(是JVM采取的方案)

这个解决了空间的问题,也解决了循环引用问题,也付出了时间上的代价。

核心思想:"遍历",JVM把对象之间的引用关系理解成了一个"树形结构",JVM就会不停的遍历这样的结构,把所有能遍历访问到的对象标记成"可达",剩下的就是"不可达"。

在这里面,是有很多课这样的树(不一定是二叉树),这些树的根节点如何确定的?(GC roots)

🚩释放对应的内存

🏀标记-清除

直接把标记为垃圾的对象对应的内存,释放掉(简单粗暴)。

"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中

需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收

🏀复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。

此算法实现简单,运行高效。算法的执行流程如下图 :

这里面最大的问题,空间浪费的太多了,另一方面要保留的对象比较多,时间花费也不少。

🏀标记-整理

能解决内存碎片,也能解决

标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动(类似于顺序表删除中间元素),然后直接清理掉端边界以外的内存。流程图如下:空间利用率的问题。

这样的搬运时间开销更大。在JVM中实际的方案,是综合上述的方案,更复杂的策略。

🏀分代回收

也就是分情况讨论,根据不同的场景/特点,选择合适的方案。根据对象的年龄来讨论的(我们说GC有一组线程会进行周期性的扫描,某个对象经历了一轮GC扫描之后,还是存在,没有成为垃圾,那么年龄 +1,依此内推)。

相关推荐
SimonKing几秒前
消息积压、排查困难?Provectus Kafka UI 让你的数据流一目了然
java·后端·程序员
考虑考虑5 分钟前
点阵图更改背景文字
java·后端·java ee
ZHE|张恒13 分钟前
Spring Boot 3 + Flyway 全流程教程
java·spring boot·后端
TDengine (老段)38 分钟前
TDengine 数学函数 CRC32 用户手册
java·大数据·数据库·sql·时序数据库·tdengine·1024程序员节
心随雨下1 小时前
Tomcat日志配置与优化指南
java·服务器·tomcat
Kapaseker1 小时前
Java 25 中值得关注的新特性
java
wljt1 小时前
Linux 常用命令速查手册(Java开发版)
java·linux·python
撩得Android一次心动1 小时前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
canonical_entropy1 小时前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
chilavert3181 小时前
技术演进中的开发沉思-174 java-EJB:分布式通信
java·分布式