JVM机制

news2024/12/23 16:56:23

文章目录

  • JVM 简介
  • JVM内存划分
    • 堆(线程共享)
    • Java虚拟机栈(线程私有)
    • 本地方法栈(线程私有)
    • 程序计数器(线程私有)
    • 方法区(线程共享)
  • JVM类加载机制
    • 类加载过程
    • 双亲委派模型
  • JVM垃圾回收机制
    • 找到谁是垃圾
      • 引用计数算法(不是JVM采取的方案,而是 Python/PHP 的方案)
      • 可达性分析算法(JVM采用)
    • 释放对应的内存的算法
      • 标记-清除算法(不实用)
      • 复制算法
      • 标记-整理算法
      • 分代算法

JVM 简介

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键。

虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机:JVM、VMwave、Virtual Box

JVM 和其他两个虚拟机的区别:

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
  2. JVM则是通过软件模拟Java字节码的指令集,JVM中主要保留了PC寄存器,其他的寄存器都进行了裁剪

PS: 本文以下部分,默认都是使用 HotSpot,也就是 Oracle Java 默认的虚拟机为前提来进行介绍的

JVM内存划分

注意它和 Java 内存模型(Java Memory Model,简称 JMM)完全不同,属于完全不同的两个概念,它由以下 5 大部分组成:

堆(线程共享)

堆是整个JVM内存区域中最大的区域, 放的就是程序中创建的所有对象

常见的 JVM 参数设置 -Xms10m 是最小启动内存,-Xmx10m 是最大运行内存 ,这都是针对堆的(ms 是 memory start 简称,mx 是 memory max 的简称)

Java虚拟机栈(线程私有)

Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建⼀个栈帧(Stack Frame), 用于存储局部变量表、操作栈、动态链接、方法出口等信息。

本地方法栈(线程私有)

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM (Java实现的方法)使用的,而本地方法栈是给本地方法(C++实现的方法)使用的。

程序计数器(线程私有)

程序计数器是内存区域中最小的区域,保存当前要执行的下一条指令(JVM字节码,不是cpu指令)的地址

如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是⼀个Native方法,这个计数器值为空。

程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!

方法区(线程共享)

方法区用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

在《Java虚拟机规范中》把此区域称之为“方法区”,而在 HotSpot 虚拟机的实现中,在 JDK 7 时此区域叫做永久代(PermGen),JDK 8 中叫做元空间(Metaspace),也叫元数据区

JVM类加载机制

类加载过程

  1. 加载

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

  1. 验证

这一阶段的目的是确保Class文件中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

  1. 准备

准备阶段是为类中定义的静态变量分配内存(在元数据区中)并设置初始值为0。被final修饰的static字段不会设置,因为final在编译的时候就分配了

  1. 解析

针对字符串常量来初始化, 把.class文件的常量放到"元数据区"

  1. 初始化

针对类对象进行初始化(不是针对对象的初始化,和构造方法无关), 执行静态代码块

执行完这五步后类对象就创建完成了,后续代码就可以使用这个类对象创建实例,或者使用里面的静态成员了

双亲委派模型

描述了JVM加载.class文件过程中,找文件的过程

"类加载器"负责 类加载 工作。自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器

  • Bootstrap ClassLoader(爷爷) : 负责加载 标准库 的类, 标准库是Java官方给出的"规范文档"上面要求提供的类
  • Extension ClassLoader(父亲) : 负责加载 JVM扩展库 的类,各个JVM厂商在实现JVM的时候会根据需要,在标准库的基础上做出一些扩展。扩展库是JVM自带的, 安装了JVM就有的(现在很少使用)
  • Application ClassLoader(儿子) : 负责加载 第三方库 和 自己写的类

此处的"父子关系"不是通过 类 的继承表示(不是 父类 子类), 而是 通过一个"parent" 字段指向自己的"父亲"

什么是双亲委派模型?

如果一个类加载器收到了类加载的请求,它不会自己先去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该委托到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才自己尝试去完成加载

在这里插入图片描述

工作过程:
例如,给定一个自己写的 类 。全限定类名(包名+类名): java111.Test

此时加载过程如下:

  1. 从Application ClassLoader 开始。Application ClassLoader 并不会立即搜索第三方库的相关目录而是把任务交给自己的父亲(Extension ClassLoader)来处理
  2. 工作就到了 Extension ClassLoader。Extension ClassLoader 也不会立即搜索扩展库的目录,也是把任务交给自己的父亲(Bootstrap ClassLoader)来处理
  3. 工作就到了 Bootstrap ClassLoader。Bootstrap ClassLoader 也想交给自己的父亲来处理,但是它的parent指向null, 只能自己处理。Bootstrap ClassLoader 就在标准库的目录中搜索 java111.Test
  4. 如果这个类在标准库找到了,找文件的过程就结束了;如果没找到,任务还是继续交给儿子来处理
  5. 工作回到了Extension ClassLoader。此时就搜索扩展库对应的目录。如果找到就结束,没找到就还给儿子处理
  6. 工作回到了Application ClassLoader。此时就搜索第三方库/用户自己写的目录了,找到了结束,没找到,任务还是继续交给儿子来处理, 此时没有儿子了,就会抛出ClassNotFoundException异常

双亲委派模型主要是为了应对这个场景:比如自己代码里写的全限定类名和标准库冲突了,JVM会确保加载的类是标准库的(不加载自己写的类了),如果标准库缺失,整个Java进程没法正常工作了

类名可以重复,全限定类名不能重复

双亲委派模型的优点

  1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了
  2. 安全性:使用双亲委派模型也可以保证 Java 的核心 API 不被篡改
  3. 确保自己添加的类加载器都能被执行到

类加载器并非是固定就只有3个,还可以手动添加更多的类加载器到中间

JVM垃圾回收机制

  • 程序计数器、虚拟机栈、本地方法栈:不需要额外回收。这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然跟着线程回收了
  • 元数据区: 一般也不需要,都是加载类,很少"卸载类"
  • 堆: GC的主力部分

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。

堆在内存中, GC回收的是"内存" , 而且一定是回收完整的对象,而不是回收半个对象

GC主要是两个步骤:

  1. 找到谁是垃圾(不用的对象)
  2. 释放对应的内存

找到谁是垃圾

如果某个对象没有引用指向它,就认为是不再使用了。介绍两种方式判定某个对象是否有引用指向:

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

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

引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法,但两个缺陷:

  1. 消耗额外的存储空间。对象比较小,引用计数空间占比就大了,并且对象越多,空间浪费就越多
  2. 在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。

循环引用示例:

class Test {
    Test t;
}


public class Main {
    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.t = b;
        b.t = a;
        a = null;
        b = null;
    }
}

a实例里面有b的实例,b实例里面有a的实例,就算双方置为null, 双方的引用计数还是1

可达性分析算法(JVM采用)

虽然解决了空间和循环引用问题,但是花了更多的时间

把对象之间的引用关系,用"树形结构"管理起来。会周期性不停遍历这样的结构,把能够遍历到的对象标记为"可达",剩下的就是"不可达"

此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的

在Java中,可作为GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  2. 方法区中类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中 JNI(Native方法)引用的对象。

由于可达性分析需要消耗一定的时间,因此Java的垃圾回收,没法做到"实时性"。周期性进行扫描(JVM提供了一组专门负责GC的线程,不停的进行"扫描"工作)

除了最早我们使用"引用"来查找对象,现在我们还可以使用“引用”来判断死亡对象了。

释放对应的内存的算法

通过上面的算法我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,我们先看下垃圾回收机器使用的几种算法:

标记-清除算法(不实用)

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。

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

  • 效率问题 : 标记和清除这两个过程的效率都不高
  • 空间问题 : 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够大的连续内存而不得不提前触发另一次垃圾收集(申请的内存都是"连续的")

复制算法

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。最大的问题是空间浪费太多了,如果要保留的空间比较大,回收的空间比较少,复制的开销也不小

标记-整理算法

标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存

能解决内存碎片和空间利用率问题,但是时间开销更大

分代算法

当前JVM实际的方案是综合上述方案的"分代回收"。分代算法是根据对象存活周期将堆划分为几块。通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。

某个对象,经历了一轮GC之后,还不是垃圾,"年龄"就会+1

在这里插入图片描述

一般是把Java堆分为新生代和老年代。新创建的对象都会进入新生代,在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法(存活的对象较少,复制开销也很低,生存区(S0/S1)空间也不必很大)

HotSpot实现的复制算法流程如下:

  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

复制算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不使用复制算法。老年代中对象存活率高(GC扫描频率低)、没有额外空间对它进行分配担保,就采用"标记-清理"或者"标记-整理"算法。

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

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

相关文章

校园表白墙源码修复版

此校园表白墙源码基于thinkphp,因为时代久远有不少bug,经本人修复已去除大部分bug,添加了美化元素。 https://pan.quark.cn/s/1f9b3564c84b https://pan.baidu.com/s/1bb9vu9VV2jJoo9-GF6W3xw?pwd7293 https://caiyun.139.com/m/i?2hoTc…

群控系统服务端开发模式-应用开发-业务架构逻辑开发准备工作

安装与仓库已经调整完毕,现在开发业务架构逻辑,其次再开发功能逻辑。业务架构逻辑开发与功能逻辑开发不是一回事,一定要明白。业务架构指的是做某一件事或是某一种类型的事的逻辑,在互联网web应用中通常指一套系统的外在逻辑&…

js 的宏任务和微任务

宏任务 (macro-task) 与微任务 (micro-task) 在 JavaScript 中,宏任务(macro-task)和微任务(micro-task)是任务队列(task queue)中两个不同的任务类型,它们是 JavaScript 异步编程机…

DBeaver查看已保存连接的密码

打开Dbeaver窗口菜单-首选项-工作空间,找到工作空间路径 在文件管理器中打开工作空间路径\General.dbeaver,找到credentials-config.json。 在Linux下,使用如下命令对credentials-config.json文件进行解密 openssl aes-128-cbc -d -K babb4…

13 实战:使用Python和Pygame实现视频运动估计播放器

首先看运行效果: 在多媒体处理领域,视频的运动估计是一个重要的课题。在本文中,我们将详细介绍如何使用Python结合Pygame、OpenCV等库,实现一个支持运动估计的视频播放器。本项目旨在展示如何在Python中处理视频帧,实现块匹配算法进行运动估计,并将结果以可视化的方式呈现…

kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?

大家好,我是锋哥。今天分享关于【kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?】面试题?希望对大家有帮助; kafka 分布式(不是单机)的情况下,如何保证消息的…

TypeScript基础简介

TypeScript是Javascript的一个超集。 TypeScript在原有的基础之上又添加了编译器类型检查的功能,意味着如果使用ts进行开发,会对变量的类型进行较为严格的验证,防止程序员写出可能出错的代码,规范变成习惯,适合大项目开…

2024年MathorCup妈杯大数据竞赛选题人数发布

经过24个小时,各个平台的相关选题投票、相关文章阅读量等各项数据进行统计,利用之前的评估办法(详见注释)。在开赛后24小时,我们基本确定各个赛题选题人数,以帮助大家更好地分析赛题局势。 题目人数A46B72 …

【Vulnhub靶场】DC-4

DC-4靶场下载地址https://www.five86.com/downloads/DC-4.zip 本机IP:192.168.118.128 靶机IP:192.168.118.0/24 信息收集 扫描主机存活,扫描端口,扫描服务 第一步扫描出主机ip为192.168.118.141 nmap -sP 192.168.118.0/24 nm…

Java后端面试题:Java基础篇

目录 Java基础 1.请你说说Java中基本数据类型的bit长度? 2.switch支持哪些数据类型?支持long么? 3.讲一下常见编码方式? 4.char能不能存储中文? 5.为什么数组索引从0开始呢?假如从1开始不行吗&#xf…

Java篇图书管理系统

目录 前言 一. 图书管理系统的核心 二. 图书管理系统基本框架 2.1 book包 2.1.1 Book(书籍类) 2.1.2 Booklist (书架类) 2.2 user包 2.2.1 User类 2.2.2 Administrator(管理员类) 2.2.3 Visitor(用户类) 2.…

数据结构 - 散列表,初探

今天我们继续学习新的数据结构-散列表。 01定义 我们先来了解一些常见概念名词解释。 散列:散列表的实现叫做散列,是一种实现以常数级时间复杂度执行查找、插入和删除的技术; 散列值:通过散列函数对输入值(key&…

【c++篇】:从基础到实践--c++内存管理技巧与模版编程基础

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:c篇–CSDN博客 文章目录 前言一.c/c内存分布二.c/c的动态内存管理方式2.1.c语言的动态内存管…

JavaEE初阶---多线程(五)---定时器/线程池介绍

文章目录 1.定时器的介绍2.线程池2.1为什么需要使用线程池2.2如何进行线程池的创建2.3普通的构造方法的局限性2.4该种对象创建的方法的特点2.5线程池的模拟实现的逻辑 3.ThreadPoolExecutor类的介绍3.1构造方法3.2四种拒绝的策略 1.定时器的介绍 下面的这个就是我们的这个定时…

基于JSP的高校食堂食材选购管理系统【附源码】

基于JSP的高校食堂食材选购管理系统 效果如下: 系统首页界面 用户登录页面 食材信息页面 论坛交流界面 管理员登录界面 管理员功能主界面 食材信息管理界面 订单配送管理界面 用户功能主界面 商家功能主界面 司机功能主界面 研究背景 近年来互联网技术的发展使得…

【C++篇】手撕string类:从初级到高级入门

1.为什么手撕string类 在面试或者一些学习场景中,手撕 string 类不仅仅是对字符串操作的考察,更多的是考察程序员对 C 内存管理的理解。例如,深拷贝与浅拷贝的实现,如何正确重载赋值运算符,如何避免内存泄漏&#xff…

线上环境的 JAVA 程序占用太多 CPU 资源,定位原因

线上环境的 JAVA 程序占用太多 CPU 资源,定位原因 top 命令执行显示一下结果 我们可以看到有一个 PID 是 4054 的应用程占用了超过一半的 CPU 资源,这是十分糟糕的事情,这个时候我们首先定位一下他是哪个线程在这里搞事情,这个时…

【JavaSE】认识String类,了解,进阶到熟练掌握

#1024程序员节 | 征文# 下面就让博主带领大家一起解决心中关于String类的疑问吧~~~ 1.字符串构造: 第一种和第二种(有一定的区别,在常量池上) public static void main(String[] args) { // 使用常量串构造 String s1 "h…

【机器学习】——numpy教程

文章目录 1.numpy简介2.初始化numpy3.ndarry的使用3.1numpy的属性3.2numpy的形状3.3ndarray的类型 4numpy生成数组的方法4.1生成0和1数组4.2从现有的数组生成4.3生成固定范围的数组4.4生成随机数组 5.数组的索引、切片6.数组的形状修改7.数组的类型修改8.数组的去重9.ndarray的…

【Visual Studio】下载安装 Visual Studio Community 并配置 C++ 桌面开发环境的图文教程

引言 Visual Studio 是一个面向 .NET 和 C 开发人员的综合性 Windows 版 IDE,可用于构建 Web、云、桌面、移动应用、服务和游戏。 安装步骤 访问 Visual Studio 的官方下载页面: https://visualstudio.microsoft.com/zh-hans/downloads/运行已下载的 V…