深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制

我的主页:2的n次方_     

在这里插入图片描述

1. JVM 内存区域划分

  1. 程序计数器(空间比较小)。保存了下一条要执行的指令的地址(指向元数据区指令的地址)
  2. 堆。JVM 最大的空间,new 出来的对象都在堆上
  3. 栈。函数中的局部变量,函数的形参,函数之间的调用关系都存放在栈上。栈也可以分为 Java 虚拟机栈和本地方法栈,Java 虚拟机栈主要服务于 Java 方法的执行,而本地方法栈主要服务于本地方法(C/C++实现的方法)
  4. 元数据区(方法区)。存放 Java 程序中的指令(指令都是包含在类的方法中),也就是保存了代码中涉及到类的相关信息,同时也保存了类的 static 属性

在一个 Java 进程中,元数据区和堆是只有一份的,程序计数器和栈可能有多份(当一个 Java 进程中有多个线程的时候,每个线程都有自己的程序计数器和栈,因为每个需要记录自己接下来需要执行什么指令还有其中的函数调用关系),由于堆和元数据区是共用一份的,所以如果是在堆上 new 了一个对象,或是元数据区的一个 static 属性,每一个线程都可以访问。

来看下面创建的变量都处在哪个内存区域:

一个变量处在哪个内存区域和变量是不是“内置类型”无关,而是和变量的形态有关

局部变量:栈 成员变量:堆 静态成员变量:元数据区(方法区)

所以说 a,b在堆区 c,d 在方法区,e,f 在元数据区(方法区)

2. JVM 类加载的过程

我们平时写的 Java 代码是一个 .java 文件,通过 javac 编译成 .class 文件,既然是文件,那么都是存储在硬盘上的,当运行 Java 进程的时候,JVM 就需要读取 .class 中的内容,并且执行里面的指令,这就是类加载的过程,也就是把类涉及到的字节码从硬盘上读取到内存中(元数据区), .class 中的指令转变为类对象,类对象中就包含了 .class 文件的各种信息,比如:类的名字,类中定义的属性,方法,实现的接口和继承关系等。

下面介绍一下类加载的具体过程:

2.1. 加载

  1. 加载。找到对应的 .class 文件,通过类的全限定名打开并读取文件内容,获取定义此类的二进制字节流。

2.2. 验证

  1. 验证。验证是确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求,在 Java 标准文档中,也明确定义了 Class 文件的格式

2.3. 准备

  1. 准备。准备阶段是正式为类中被 static 修饰的变量(类变量)在方法区中分配内存空间的,这些类变量在整个类的生命周期中都是有效的,并且被所有的实例对象所共享,根据上面读取到的内容来确定出需要的内存空间,然后申请内存空间,并把空间内所有的内容都初始化为 0 。

例如,对于 public static int value = 10;,在准备阶段,value 会被初始化为 0,而不是 10。真正的赋值 10 是在初始化阶段进行的。

2.4. 解析

  1. 解析。主要是针对类中的字符串常量进行处理,JVM 将常量池中的符号引用替换为直接引用的过程,也就是初始化常量的过程。

符号引用和内存引用:字符串常量已经在 Class 文件中了,这时是没有“地址”这样的概念的,地址是内存的地址,还没有加载到内存中,这时是用“文件偏移量”来表示的,当加载到内存空间之后就会使用地址来标识,也就是直接引用

2.5. 初始化

  1. 初始化:Java 虚拟机真正开始执⾏类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始
    化阶段就是执⾏类构造器⽅法的过程 。例如创建类的实例,调用类的静态方法,访问类或接口的静态变量(不包括 final 修饰的静态变量,在编译阶段就确定了值),初始化子类时,父类会先被初始化,执行静态代码块。

普通的成员变量(非静态变量)不会在 JVM 类加载过程中的初始化阶段被初始化。类加载过程中的初始化阶段主要是针对类变量(被 static 修饰的变量)以及执行静态代码块。普通成员变量是在对象创建时,即通过构造函数或其他实例初始化方法进行初始化的。当使用 new 关键字创建对象时,会为对象在堆内存中分配空间,并对成员变量进行初始化等操作。

2.6. 双亲委派模型

双亲委派模型是 Java 类加载机制中的一种重要设计模式,也就是上述类加载中的第一步

JVM 默认是有三个类加载器的:

这里的父子关系并不是 Java 中的继承关系,而是类似与指向的一个 parent 引用

双亲委派模型工作原理:

当一个应用程序需要加载一个类时,应用程序类加载器首先会将加载请求委派给它的父类加载器扩展类加载器,扩展类加载器再委派给启动类加载器。如果启动类加载器无法加载该类,扩展类加载器会尝试加载。如果扩展类加载器也无法加载,最后应用程序类加载器才会尝试自己去加载。

那为什么不一开始就给启动类加载器来执行而是又绕了一圈?

  1. 避免类的重复加载:由于父类加载器优先加载,相同的类只会被加载一次,避免了类的重复加载和内存浪费。
  2. 保证核心类库的安全性:防止核心类库被恶意篡改(程序员自己写的类和标准库中的类名字一样,直接从启动类加载器加载会覆盖掉标准库的类),确保了 Java 程序的稳定性和安全性。

就类似与公司中的员工,领导和老板,当员工发现问题是需要上报给领导,领导再上报给老板,如果说这个问题比较重要,老板就亲自解决,如果不是那么重要,但也有一定分量,就交给领导解决,如果只是一些小事,就让员工自己解决

其实类加载器可以程序员自定义,自定义的类加载器可以遵守双亲委派模型也可以不遵守。

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

JVM 的垃圾回收主要是在堆内存进行的,这里存储的都是 new 出来的对象,当这些对象没有引用指向的时候,就成为了垃圾,如果放任不管,就会一直占用着内存,最终造成内存泄露

进行垃圾回收可能会触发 “Stop The World”(STW)问题:在执行垃圾回收的某个阶段,为了保证垃圾回收的准确性和一致性,需要暂停所有应用程序线程,使得整个应用程序出现短暂的停顿。

所以说 C++ 这种追求高性能的语言并没有使用垃圾回收机制

接下来看 JVM 中是如何进行垃圾回收的

首先,进行垃圾回收肯定需要先标记出哪些对象是垃圾

那么怎么去找哪个对象是垃圾呢?

3.1. 识别垃圾

方案一:引用计数。

给每一个对象分配一个计数器,来统计有多少引用指向,每增加一个引用,计数器 + 1,每减少一个引用,计数器 - 1,当计数器减为 0,就变成垃圾了

这个方法看似可行,python 等一些语言采用的就是这个,但是还存在一个问题:

无法解决循环引用的问题。例如,对象 A 和对象 B 相互引用,除此之外没有其他地方引用它们,此时它们的引用计数器都不为 0,但实际上它们已经不再被程序使用,应该被回收。

同时,引入计数器,也会消耗掉额外的空间

方案二:可达性分析

以一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,是垃圾对象,也就是在类似图的结构中从多个根节点遍历一遍,如果发现有的对象没有任何路径可以到达,也就是没有引用指向了,那么就是垃圾

可达性分析有效的解决了循环引用的问题,可以作为根节点的可以是方法区中类静态属性引用的对象(static 修饰的变量引用的对象),常量池中引用的对象,方法中的局部变量引用的对象,本地方法栈引用的对象。

可达性分析由于需要每次都遍历一遍对象,所以是比较消耗时间的

3.2. 垃圾回收的方法

  1. 标记 - 清除法。

在上面遍历的时候标记出哪些是垃圾,然后回收所有被标记的对象所占用的空间,这种方法有很大的缺点:由于每次申请的内存都是一块连续的区域,所以会产生内存碎片,回收后的内存不完整,接下来再申请内存的话可能会申请失败

  1. 复制算法。

将堆空间分为两个大小相等的区域,一般称为 From 区和 To 区。当进行垃圾回收时,将 From 区中仍然被引用的对象(也就是需要继续使用的对象)复制到 To 区,然后清空 From 区。下次垃圾回收时,交换 From 区和 To 区的角色。

这种算法缺点也很明显,就是空间利用率低,一大块区域只有一半能用,同时如果存活下来的对象有很多,复制的成本也比较大

  1. 标记 - 整理算法。

标记阶段就是把哪些对象是垃圾标记出来,接下来整理就是把存活的对象移动到另一端,然后回收边界以外的空间

这种方法的缺点就是移动对象的时候比较消耗时间,特别是对象很多的情况下

3.3. 分代收集算法

根据对象的生命周期将堆空间分为新生代和老年代,也就是在可达性分析的过程中,每经过一轮扫描,对象仍然存活(不是垃圾),年龄就 + 1,新生代中的对象生命周期较短,很多对象短时间内就变成垃圾了,老年代中的对象生命周期较长。

新创建的对象首先在伊甸区分配内存,当伊甸区满时,触发一次新生代垃圾回收,将伊甸区和幸存区 1中仍然存活的对象复制到幸存区 2,然后清空伊甸区和幸存区 1。下次垃圾回收时,交换幸存区 1 和幸存区 2 的角色,继续重复上述操作,幸存区 1 到 幸存区 2 二者的转化会反复进行多次

由于新生代对象大多存活时间少,要复制的也比较少,效率并不会有多大影响,同时空间也没有太大浪费,两个幸存区的空间比较小,也不会有太大空间的浪费。

当一个对象在幸存区反复拷打,年龄不断增加,就进入了老年代,老年代的对象生命周期长,如果还采用复制算法的话,要复制的对象就很多,影响效率,所以就可以采用标记 - 整理算法,需要整理的次数也不多。

上述过程就很好的利用了各个算法的优势来结合,通过分代收集算法,提高了垃圾回收的效率

3.4. 垃圾回收器

JVM 中还提供了多种“垃圾回收器”,对上面介绍的分代回收算法进行了进一步的扩充和具体实现

CMS 垃圾回收器

先大概的进行一个初始标记,然后再通过多线程的方式并发的进行重新标记,标记之后再进行并发的清理,通过这样的方法,能和业务线程并发执行,使 STW 的时间尽可能的缩短

G1 垃圾回收器

G1 是把整个堆内存分层很多小块,进行垃圾回收时,不要求一个周期就把所有的垃圾全都回收,而是一轮周期回收一部分,通过限制每一轮 GC 花的时间在可控范围之内,同时划分为多个区域也是为了更好的使用多线程

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/885559.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器

SpringMVC九大内置组件之HandlerMapping处理器映射器-AbstractHandlerMethodMapping类以及子类RequestMappingHandlerMapping如何将Controller修饰的注解类以及类下被注解RequestMapping修饰的方法存储到处理器映射器中。 从RequestMappingHandlerMapping寻找: AbstractHandle…

unity一键注释日志和反注释日志

开发背景:游戏中日志也是很大的开销,虽然有些日志不打印但是毕竟有字符串的开销,甚至有字符串拼接的开销,有些还有装箱和拆箱的开销,比如Debug.Log(1) 这种 因此需要注释掉,当然还需要提供反注释的功能&am…

Spring1

1.Spring系统架构图 (1)核心层 Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块 (2)AOP层 AOP:面向切面编程,它依赖核心层容器,目的是==在不改变原有代码的前提下对其进行功能增强== Aspects:AOP是思想,Aspects是对AOP思想的具体实现 (3)数据…

C语言进阶版第14课—内存函数

文章目录 1. memcpy函数的使用和模拟实现1.1 memcpy函数的使用1.2 模拟实现memcpy函数 2. memmove函数的使用和模拟实现2.1 memmove函数的使用2.2 memmove函数的模拟实现 3. memset函数4. memcmp函数 1. memcpy函数的使用和模拟实现 1.1 memcpy函数的使用 memcpy函数的原形voi…

虚商目前有哪些业务痛点?

虚拟运营商当前面临的业务痛点主要集中在市场竞争、运营成本、技术依赖、用户体验及政策监管等方面。 一、市场竞争激烈 1、竞争者数量增加: 随着市场准入门槛的降低,越来越多的企业进入虚拟运营商市场导致市场竟争日益激烈。为了争夺市场份额,企业不得不…

SRC漏洞挖掘 | 针对Spring-Boot 框架漏洞的初探

💗想加内部圈子,请联系我! 💗文章交流,请联系我!🍬 博主介绍 👨‍🎓 博主介绍:大家好,我是 一个想当文人的黑客 ,很高兴认识大家~ ✨主…

kafka分区和副本的关系?

概念来一波 比如一个topic的消息存放在两个分区中,分区1和分区2.每个分区都有自己的一个副本。即比如分区1有副本1/副本2/副本3,分区2也有分区2的副本1/副本2/副本3。一个节点上的一个topic的可以由多个分区存放,但是每个分区的leader副本会尽…

Spring Boot技术在足球青训管理中的创新应用

3 系统分析 3.1 可行性分析 可行性分析是该平台系统进行投入开发的基础第一步,必须对其进行可行性分析才能够降低不必要的需要从而使资源合理利用,更具有性价比和降低成本,同时也是系统平台的成功的未雨绸缪的一步。 3.1.1 技术可行性 技术可…

C#测试调用Ghostscript.NET浏览PDF文件

Ghostscript.NET是针对Ghostscript的C#封装库,支持解析PostScript语言、操作PDF文件等。使用Ghostscript.NET的GhostscriptViewer 模块可以以图片形式查看PDF文档。本文学习并测试调用Ghostscript.NET模块打开及浏览PDF文件的基本用法。   Ghostscript.NET目前主要…

Mac优化清理工具CleanMyMac X 4.15.6 for mac中文版

CleanMyMac X 4.15.6 for mac中文版下载是一款功能更加强大的系统优化清理工具,软件只需两个简单步骤就可以把系统里那些乱七八糟的无用文件统统清理掉,节省宝贵的磁盘空间。CleanMyMac X 4.15.6 for mac 软件与最新macOS系统更加兼容,流畅地…

24年九月份生活随笔

九月份最后一天,烈士纪念日。 上午看了一会儿直播,庄重的仪式,铭记先辈为新中国抛头颅洒热血,当今盛世,如您所愿。 郑州马拉松官方通告,今天十点公布直通,中签,候补结果。 看完直…

①无需编程 独立通道 Modbus主站EtherNet/IP转ModbusRTU/ASCII工业EIP网关串口服务器

Modbus主站EtherNet/IP转ModbusRTU/ASCII工业EIP网关串口服务器https://item.taobao.com/item.htm?ftt&id743840591638 EtherNet/IP 串口网关 EtherNet/IP 转 RS485 型号 2路总线EIP网关 MS-A1-2021 4路总线EIP网关 MS-A1-2041 4路总线EIP网关(双网口&am…

Ubuntu 系统崩了,如何把数据拷下来

问题描述: Linux系统中安装输入法后,重启后,导致系统无法进入,进入 recovery mode下的resume 也启动不了,所以决定将需要的东西复制到U盘 解决方案: 1.重启ubuntu,随即点按Esc进入grub菜单&am…

编程魔法:基于LLM的AI function开发,如何实现高效数据生成?

基于大语言模型(LLM)的AI function开发,简直就是现代编程界的“魔法棒”! 你好,我是三桥君 最近三桥君有个任务,需要造一些测试数据,比如姓名、手机号、银行卡号、邮箱啥的,用来做测…

OCR识别系统 YOLOv8 +Paddle 方案落地

YOLOv8 PaddleOCR 技术方案落地 Yolov8相关文档Step 1 证件模型的训练Step 2 Yolov8进行图片推理Step 3 PaddleOCR进行识别Step 4 整合Yolov8 PaddleOCR 进行OCR Yolov8相关文档 《yolov8 官方网站》 《Yolov8 保姆级别安装》 Ultralytics YOLOv8 是一款尖端的、最先进的 (S…

js将对象的键和值分别归纳进对象,并将多层对象转化成数据的方法

前言: 后端传给我一个没有处理过的json串,但是我要传入el-tree做渲染,此篇来记录一下整个数据处理过程以及el-tree的使用 需求描述: 一、树结构可以展开可以收缩,默认全部展开 二、有一些关键词需要高亮展示红色 …

SW - 装配图旋转到一个想要的正视图

文章目录 SW - 装配图旋转到一个想要的正视图概述笔记将装配图旋转到自己想要的视图的方法保存当前视图选择自己保存的视图END SW - 装配图旋转到一个想要的正视图 概述 在弄装配图。 如果按照SW默认的视图,Y方向是反的。 原因在于我画零件图时,方向就…

从源码中学习动态代理模式

动态代理模式 动态代理是 Java 反射(Reflection)API 提供的一种强大机制,它允许在运行时创建对象的代理实例,而不需要在编译时静态地创建。 Java 提供了两种主要的方式来实现动态代理: 基于接口的动态代理&#xff1a…

GIT安装及集成到IDEA中操作步骤

最近深感GIT使用技能太差,我只会些皮毛,还是得看官网,总结一下常用的操作方法吧。 GIT环境配置到IDEA中安装 一、GIt的基本的安装 这个不在这里赘述了,自己装一个git吧 二、给IDEA指定本地GIT的安装路径 1、下图这个是我本地的…

postgresql僵尸进程的处理思路

简介 僵尸进程(zombie process)是指一个已经终止但仍然在进程表中保留条目的进程。正常情况下,当一个进程完成执行并退出时,操作系统会通过父进程调用的wait()或waitpid()系统调用来收集该子进程的退出状态。如果父进程未及时调用…