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

2024-09-04 00:28

本文主要是介绍【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,依此内推)。

这篇关于【JavaEE初阶】JVM内存划分和类加载过程以及垃圾回收的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1134472

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

MyBatis延迟加载与多级缓存全解析

《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓... 目录MyBATis延迟加载策略一对多示例一对多示例MyBatis框架的缓存一级缓存二级缓存MyBat

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input