【JavaEE初阶】了解JVM

文章目录

  • 一. JVM内存区域划分
  • 二. JVM类加载机制
    • 2.1 类加载整体流程
    • 2.2 类加载的时机
    • 2.3 双亲委派模型(经典)
  • 三. JVM垃圾回收机制(GC)
    • 3.1 GC实际工作过程
      • 3.1.1 找到垃圾/判定垃圾
        • 1. 引用计数(不是java的做法,Python/PHP)
        • 2. 可达性分析(Java的做法)
      • 3.1.2 清理垃圾
        • 1. 标记清除
        • 2. 复制算法
        • 3. 标记整理
        • 4. 分代回收

一. JVM内存区域划分

JVM启动的时候,会申请到一整个很大的内存区域.JVM是一个应用程序,要从操作系统里申请内存.JVM就根据需要,把空间分为几个部分,每个部分各自有不同的功能.具体划分如下:
在这里插入图片描述

  • Native Method Stacks(本地方法栈):native表示是JVM内部的C++代码.就是给调用native方法(JVM内部的方法)准备的栈空间.

  • Program Counter Register(程序计数器):记录当前线程执行到那个命令.(很小的一块存一个地址)每个线程有一份.

  • JVM Stack(虚拟机栈):虚拟机栈是给Java代码使用的栈. 此栈是JVM中一个特定的空间,对于 JVM虚拟机栈
    ,这里存储的是方法之间的调用关系.整个栈空间内部,可以认为是包含很多个元素(每个元素表示一个方法),把这里的每个元素称为**“栈帧”,这一个栈帧里,会包含这个方法的入口地址,方法的参数,返回地址,局部变量等.
    而对于 本地方法栈 , 存储的是
    native方法之间的调用关系**.
    虚拟机栈,不是只有一个,而是有很多个,每个线程都有一个.
    在这里插入图片描述
    由于函数调用,是有先进后出特点的.此处的栈,也是先进后出的.
    栈空间整体一般都不会很大,但是每个栈帧其实占得空间比较小,一般代码无限递归才会出现栈溢出情况.

  • Heap(堆):整个JVM空间最大的区域.new出来的对象,类的成员变量,都在堆上.**堆是一个进程只有一份,栈是每个线程有一份.**一个进程里有多个线程.所以一个进程有多个栈.每个jvm就是一个java进程.
    在这里插入图片描述

    注意这里的常见说法:栈是线程私有的.(此说法不完全对).私有的意思是我的你用不了.实际上,一个线程栈上的内容,可以被另一个线程使用.比如通过变量捕获,一个线程可以访问到另一个线程的局部变量.

  • Metaspace(元数据区):即方法区.一个进程里只有一块,多个线程共用一块.

  • 在这里插入图片描述
    小结:

  • 局部变量 在 栈

  • 普通成员变量 在 堆

  • 静态成员变量 在 方法区/元数据区

二. JVM类加载机制

2.1 类加载整体流程

类加载,准确的说,类加载就是.class文件,从文件(硬盘)被加载到内存中(元数据区)这样的过程.这个过程是非常复杂的.
.class文件可以有多个类对象
.class文件是编译后的Java源代码,它包含了编译后的字节码指令.
类加载主要分为以下几个过程:
在这里插入图片描述

  • 加载:把.class文件找到(找的过程),打开文件,地文件,把文件内容读到内存中.

  • 验证:检查.class文件格式是否正确. .class文件是一个二进制文件,这里的格式是有严格说明的,官方提供了JVM虚拟机规范.规范文档上描述了.class的格式:在这里插入图片描述
    java代码中写的类的所有信息,都会包含在上述.class文件中.使用二进制的方式重新组织.

  • 准备:给类对象分配内存空间(此时内存初始化为全0)

  • 解析:针对字符串常量进行初始化,把符号引用转为直接引用.
    字符串常量,得有一块内存空间,存这个字符的实际内容,还得有一个引用,用来保存这个内存空间的起始地址.
    在类加载之前,字符串常量,此时是处在.class文件中的,此时这个"引用"记录的并非是字符串常量的真正地址,而是它在文件中的"偏移量".(或者是占位符)(符号引用)
    类加载之后,才真正在把这个字符串常量放到内存中,此时才有"内存地址",这个引用才能被真正赋予成指定内存地址.(直接引用)

    举个🌰: 小学的时候,学校组织以班为单位大家看电影,
    但是到电影院之前我不知道自己的真实地址(真实座位),但是我知道我前面是A,后面是B,此时到了电影院,我们仨也是挨着的.(符号引用)
    此时我只知道自己的相对位置. 到了电影院之后,老师组织同学们坐下.我坐下之后才知道我们的真正位置.(直接引用)

  • 初始化:调用构造方法,进行成员初始化,执行代码块,静态代码块,加载父类.

2.2 类加载的时机

一个类,不是java程序已运行,就把所有的类都加载了.而是真正使用的时候才加载(懒汉模式)
在以下场景中会触发类加载:

  1. 构造类的实例
  2. 调用这个类的静态方法/使用静态属性
  3. 加载子类,就会先加载其父类

以上都是用到了再加载,一旦加载过后,后续使用就不用再加载.

2.3 双亲委派模型(经典)

此过程发生在加载阶段.
双亲委派模型:描述的是加载 找.class文件的基本过程.
JVM默认提供了三个类加载器:

  • 启动类加载器(BootStrap ClassLoader):负责加载Java标准库中的类(无论是哪种JVM的实现,都会提供这些一样的类)
  • 扩展类加载器(Extension ClassLoader):负责加载JVM扩展库中的类.(由实现JVM的厂商/组织提供的额外功能)
  • 应用程序类加载器(Application ClassLoader):负责加载项目中自己写的类以及第三方库中的.

在这里插入图片描述
上述三个类,存在"父子关系"(不是父类子类,相当于每个class loader有一个parent属性,指向自己的父 类加载器).
那么上述类加载器如何配合工作呢?
首先加载一个类的时候,实现从ApplicationClassLoader开始.但是ApplicationClassLoader会把家在任务交给父亲,让父亲去进行.于是ExtensionClassLoader要去加载了,但是也不是真的加载,而是再委托给自己的父亲.于是BootstrapClassLoader要去加载了,也是想委托给自己的父亲,结果发现,自己的父亲是null. 没有父亲/父亲加载完了,没找着类,才由自己进行加载,此时BootstrapClassLoader就会搜索自己负责的标准库目录的相关的类,如果找到就加载,如果没找到,就继续由子类加载器进行加载.然后由ExtensionClassLoader真正搜索扩展库相关的目录,如果找到了就加载,如果没找到就由子类加载器进行加载.然后由ApplicationClassLoader真正搜索用户项目相关的目录,如果找到就加载,如果没找到就由子类加载器进行加载(由于当前没有子了,只能抛出 类找不到这样的异常)
在这里插入图片描述
那么为什么要有上述顺序呢?
上述这套顺序其实是出自于JVM实现代码的逻辑,这段代码大概是使用递归的方式写的.但这个顺序,最主要的目的就是为了保证Bootstrap能够先加载,Application能够后加载.这样就可以避免说因为用户创建了一些奇怪的类,引起不必要的bug.
假设用户在自己的代码中写了个java.lang.String按照上述加载流程,此时JVM加载的还是标准库的类,不会加载到用户自己写的这个类. 这样就能保证,即使出现上述问题,也不会让jvm已有代码混乱,最多是用户自己写的类不生效.
另一方面,类加载器,其实是可以用户自定义的.上述三个类加载器是jvm自带的.用户自定义的类加载器,也可以加入到上述流程中,就可以和现有的加载器配合使用,

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

垃圾:指的是不再使用的内存空间.垃圾回收,就是把不用的内存帮我们自动释放.
在执行程序时,要在堆上申请一块内存空间.在C/C++中,上述内存空间需要手动方式进行释放.如果不手动释放的话,这块内存的空间就会持续存在,一直存在到进程结束(堆上的内存生命周期比较长.不像栈,栈的空间会随着方法执行结束,栈帧销毁而自动释放,堆则默认不能自动释放.)那么这就可能会导致一个严重的问题—内存泄漏.如果内存一直占着不用,又不释放,就会导致剩余空间越来越少.进一步导致后续的内存申请操作失败.因此,大佬们想了一些办法,来解决内存泄漏的问题.
GC是其中最为主流的方式.(Java Go Python PHP JS大部分的主流语言都是使用GC解决内存泄漏的问题的)

但GC中有一个比较关键的问题:STW问题(stop the world)
如果有时候,内存中的垃圾已经非常多了,此时触发一次GC操作,开销可能非常大,大到可能把系统资源吃了很多.另一方面GC回收垃圾的时候可能会涉及到一些锁操作.导致业务代码无法正常执行.这样的卡顿,极端情况下,可能是出现即使毫秒甚至上百毫秒.

JVM中有很多内存区域:

  1. 程序计数器
  2. 元数据区

  3. GC主要是针对进行释放的.
    GC是以"对象"为基本单位,进行回收的.而不是字节!
    在这里插入图片描述
    GC回收的是整个对象都不再使用的情况.而一部分使用一部分不使用的对象,暂且不回收.

3.1 GC实际工作过程

  1. 找到垃圾/判定垃圾
  2. 再进行对象的释放

3.1.1 找到垃圾/判定垃圾

找到垃圾/判定垃圾的关键就是看到底有没有"引用"指向它.如果一个对象,有引用指向它,就可能被使用到;如果一个对象,没有引用指向了,就不会再被使用了.
具体如何知道对象是否有引用指向呢?

1. 引用计数(不是java的做法,Python/PHP)

给每个对象分配了一个计数器(整数).每次创建一个引用指向该对象,计数器就+1,每次该引用被销毁,计数器就-1.

{Test t = new Test();//Test对象的引用计数1Test t2 = t;//t2也指向了t,引用计数2Test t3 = t;//引用计数就是3
}
//大括号结束,上述三个引用超出作用域失效.此时引用计数就是0了,此时new Test()对象就是垃圾了.

这个办法简单有效,但是java没有采用.但是这个办法也有一定的缺点:
1. 内存空间浪费的多(利用率低)
每个对象都要分配一个计数器,如果按四个字节计算.代码中的对象非常少就无所谓.但是如果对象特别多,占用的额外空间就会很多,尤其是每个对象都比较小的情 况.例如:一个对象体积1k,此时多4个字节,无所谓.一个对象体积事4字节,此时多4个字节,相当于体积扩大一倍.
2. 存在循环引用的问题

class Test{Test t = null;
}
Test a = new Test();//1号对象,引用计数是1
Test b = new Test();//2号对象,引用计数是1
a.t = b;//a.t也指向2号对象了,2号对象的引用是2
b.t = a;//b.t也指向1号对象了,1号对象的引用是2

接下来,a和b引用销毁,此时a和b计数-1,但引用计数结果还都是1,不能释放资源,但实际这两个对象已经无法访问了。python/php使用引用计数,需要搭配其他机制来避免循环引用.
举个🌰:
在这里插入图片描述

2. 可达性分析(Java的做法)

把对象之间的引用关系理解成了一个树形结构,从一些特殊的起点出发,进行遍历,只要能遍历访问到的对象,就是"可达的",再把"不可达的"当作垃圾即可.
在这里插入图片描述

就像上述的二叉树,root指向根节点a.
如果root.right.right = null ,此时就表示f不可达
如果root.right = null此时就表示c不可达,f也不可达了。
可达性分析的关键要点,进行上述遍历,需要有"起点"被称为gcroots.以下常做为根起点:

  1. 栈上的局部变量(每个栈的每个局部变量,都是起点)
  2. 常量池中引用的对象
  3. 方法区中,静态成员引用的对象

可达性分析,总的来所,就是从所有的gcroots的起点出发,看看该对象里又通过引用能访问那些对象,依次遍历,把所有可以访问的对象都给遍历一遍(遍历的同时把对象标记成"可达"),剩下的遍历不到的对象就是"不可达".

可达性分析的特点:可达性分析克服了引用计数的两个缺点,但是也有自己的缺点:

  1. 消耗的时间更多,因此某个对象成了垃圾,也不一定能第一时间发现,因为扫描的过程,需要消耗时间
  2. 在进行可达性分析的时候,依次遍历,一旦这个过程中,当前代码中的对象引用关系发生了变化,这就会使情况变得更加复杂。比如,当一个对象指向下一个对象,刚遍历完这个对象,这个对象的引用变了。因此,我们为了更准确的遍历,需要让其他的业务线程暂停工作(STW问题)。

3.1.2 清理垃圾

主要有三种基本做法:

1. 标记清除

这种策略,就是直接把垃圾对象的内存释放,但是这个方式的缺点就是会产生内存碎片.
在这里插入图片描述
我们从内存中申请空间的时候,都是整块的连续的空间,现在这里空闲的空间是离散的,独立的空间,总的空间可能很大.假如总的空闲的空间可能超过了1G,但是你想申请500MB可能都不一定申请到。

2. 复制算法

为了解决内存碎片的问题,又引入了复制算法.复制算法,是把整个内存空间,分成两半,一次只用一半.
在这里插入图片描述
现在将2和4标记为垃圾,要释放垃圾,复制算法会将左边不需要释放内存的空间复制到右边的空间中,然后整体释放左边空间的内存.

复制算法,就是把"不是垃圾"的对象复制到另外一半,然后把整个空间删除掉.
每次触发复制算法,都是向另外─侧进行复制,内存中的数据拷贝过去.

缺点:

  1. 空间利用率低
  2. 如果要是垃圾少,有效对象多,复制成本就比较大了~~

3. 标记整理

在这里插入图片描述
这种方法,保证了空间利用率,同时也解决了内存碎片问题
但是这种做法的缺点:

  1. 效率不高如果要搬运的空间比较大,此时开销也很大

4. 分代回收

基于上述这些基本策略,搞了一个复合策略"分代回收"
把垃圾回收,分成不同的场景,有的场景有这个算法,有的场景有那个,各展所长.

分代是怎么分的?
基于一个经验规律:如果一个东西,存在的时间比较长了,那么大概率还会继续的长时间持续存在下去.(要没早就没了,既然存在,肯定有点用)
规律不等于"定律",允许例外,针对大部分情况有效的.

上述规律,对于Java的对象也是有效的.(是有一系列的实验和论证过程)
java的对象要么就是生命周期特别短,要么就是特别长.根据生命周期的长短,分别使用不同的算法.
给对象引入一个概念,年龄.(单位不是年,而是熬过GC的轮次)(经过了这一轮可达性分析的遍历,发现这个对象还不是垃圾.这就是"熬过一轮GC") 年龄越大,这个对象存在的时间就越久.
在这里插入图片描述
刚new 出来的,年龄是0的对象,放到伊甸区.(出自圣经,上帝在伊甸园造小人)
熬过一轮GC,对象就要被放到幸存区了.虽然看起来幸存区很小,伊甸区很大,一般够放.
伊甸区到幸存区,采用的是复制算法.

幸存区之后,也要周期性的接受GC的考验.
如果变成垃圾,就要被释放.如果不是垃圾,拷贝到另外一个幸存区(这俩幸存区同一时刻只用一个),在两者之间来回拷贝(复制算法),由于幸存区体积不大, 此处的空间浪费也能接受.如果这个对象已经再两个幸存区中来回拷贝很多次了这个时候就要进入老年代了·

老年代都是年纪大的对象.生命周期普遍更长.针对老年代,也要周期性GC扫描,但是频率更低了
如果老年代的对象是垃圾了,使用标记整理的方式进行释放.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——单链表OJ题

单链表OJ题 前言一、删除链表中等于给定值 val 的所有节点二、反转一个单链表三、返回链表的中间结点四、输出该链表中倒数第k个结点五、将两个有序链表合并六、链表的回文结构七、将链表分割成两部分八、找出第一个公共结点九、判断链表中是否有环总结 前言 在前面的博客中我…

jenkins的cicd操作

cicd概念 持续集成( Continuous Integration) 持续频繁的(每天多次)将本地代码“集成”到主干分支,并保证主干分支可用 持续交付(Continuous Delivery) 是持续集成的下一步,持续…

智能优化算法——哈里鹰算法(Matlab实现)

目录 1 算法简介 2 算法数学模型 2.1.全局探索阶段 2.2 过渡阶段 2.3.局部开采阶段 3 求解步骤与程序框图 3.1 步骤 3.2 程序框图 4 matlab代码及结果 4.1 代码 4.2 结果 1 算法简介 哈里斯鹰算法(Harris Hawks Optimization,HHO),是由Ali As…

高并发与性能优化的神奇之旅

作为公司的架构师或者程序员,你是否曾经为公司的系统在面对高并发和性能瓶颈时感到手足无措或者焦头烂额呢?笔者在出道那会为此是吃尽了苦头的,不过也得感谢这段苦,让笔者从头到尾去探索,找寻解决之法。 目录 第一站…

android AIDL 学习使用

在android studio 2023.2中使用 1、在buidl.gradle增加以下配置,然后同步。不增加这些配置,创建aidl时显示为灰色,不能创建 buildFeatures {compose true// Disable unused AGP featuresbuildConfig falseaidl truerenderScript falseresVal…

Flink学习教程

最近因为用到了Flink,所以博主开了《Flink教程》专栏来记录Flink的学习笔记。 【Apache Flink v1.16 中文文档】 【官网 - Apache Flink v1.3 中文文档】 一、基础 参考链接如下: Flink教程(01)- Flink知识图谱Flink教程&…

使用即时消息缩短 B2B 销售周期

在B2B销售中,时间就是一切。所以企业需要做的就是尽可能快地回复客户的咨询,否则会造成严重的客户流失。什么是 B2B 销售?它就是企业对企业销售,企业主要或仅向其他企业销售其产品或服务,这些产品或服务可以是从软件到…

安卓:BottomNavigationBar——底部导航栏控件

目录 一、BottomNavigationBar介绍 二、BottomNavigationBar的常用方法及其常用类 (一)、常用方法 1. 添加菜单项 2. 移除菜单项 3. 设置选中监听器 4. 设置当前选中项 5. 设置徽章 6. 样式和颜色定制 7. 动画效果 8. 隐藏底部导航栏。 9、设…

C# Blazor 学习笔记(10):依赖注入

文章目录 前言Blazor 依赖注入依赖注入用于解决什么问题?依赖注入的生命周期。测试代码 总结补充日期2023年8月4日 前言 Blazor 具有前后端不分离模式,但是如何直接调用需要一定的设置 Blazor 依赖注入 依赖注入在spring里面很常见,毕竟.N…

Maven里面没有plugins dependence问题解决

说明:今天在做Nacos、Dubbo整合的时候,在父模块中做了版本限制,出错后就又把版本控制什么都删掉,回退到最开始的状态,此时父模块下面的服务右侧的 maven里面没有plugins dependence ,然后项目全都报错。 问…

react18 之 06 之 useReducer 结合 useContext使用 (实现全局状态管理、复杂组件交互逻辑、多个组件需要共享和更新某个状态)

目录 react18 之 06 之 useReducer 结合 useContext使用 (实现全局状态管理、复杂组件交互逻辑、多个组件需要共享和更新某个状态)useReducer 结合 useContext使用 实现全局共享RootCom.jsxA.jsxASon.jsxB.jsxC.jsx执行效果 react18 之 06 之 useReducer 结合 useContext使用 (…

JavaWeb三大组件 —— Servlet

目录 servlet 注册servlet 父pom pom文件 1、通过注解注册 2、使用ServletRegistrationBean注册 API三生三世 第一生Servlet 第二生SpringMVC 今生SpringBoot servlet Servlet的作用: 接受请求参数、处理请求,响应结果,(就…

小白解密ChatGPT大模型训练;Meta开源生成式AI工具AudioCraft

🦉 AI新闻 🚀 Meta开源生成式AI工具AudioCraft,帮助用户创作音乐和音频 摘要:美国公司Meta开源了一款名为AudioCraft的生成式AI工具,可以通过文本提示生成音乐和音频。该工具包含三个核心组件:MusicGen用…

Docker 容器转为镜像

# 容器转成镜像并指定镜像名称与版本号 # commit 时原有容器挂载的目录是不会被写入到新的镜像中去的,数据卷相关的都不会生效 # 但是 root 目录下新建的内容会写入到新的镜像中去 $ docker commit 容器ID 新镜像名称:版本号 $ docker commit -m"描述信息"…

瑞芯微RK3568开发板保姆级护航入门学习嵌入式

资料优势 专为3568编写|迅为原创|拒绝网络拼凑 20个手册2800页手册进行结构分层适用于学习与开发 为了方便大家清晰快速的学习,迅为iTOP-3568开发板手册资料全面升级,对手册内容进行了结构分层,共计20个文档,超2800页的资料专为…

AI写作宝有哪些,分享两种AI写作工具

AI写作宝是一种基于人工智能技术的写作辅助工具。它可以根据用户输入的关键词和主题快速生成文章。AI写作宝可以为用户节省大量的时间和精力,帮助用户快速生成高质量的文章。今天就为大家推荐两款AI写作宝: 一、AI创作家 AI创作家是一款基于人工智能技…

企业如何搭建矩阵内容,才能真正实现目的?

当下,新媒体矩阵营销已成为众多企业的营销选择之一,各企业可以通过新媒体矩阵实现扩大品牌声量、维持用户关系、提高销售业绩等不同的目的。 而不同目的的矩阵,它的内容运营模式会稍有差别,评价体系也会大不相同。 企业在运营某类…

Jenkins工具系列 —— 插件 实现用户权限分配与管理

文章目录 安装插件 Role-based Authorization Strategy添加用户注册配置权限查看当前使用者,获取user id配置管理员权限配置普通用户权限(非管理员权限) 小知识 安装插件 Role-based Authorization Strategy 点击 左侧的 Manage Jenkins —&…

git 公钥密钥 生成与查看

1.什么是公钥 很多服务器都是需要认证的,ssh认证是其中的一种。在客户端生成公钥,把生成的公钥添加到服务器,你以后连接服务器就不用每次都输入用户名和密码了。 很多git服务器都是用ssh认证方式,你需要把你生成的公钥发送给代码仓…

Golang之路---03 面向对象——接口与多态

接口与多态 何为接口 在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。   在 Go 语言中,…