Java:JVM

devtools/2024/11/14 23:21:15/

1.JVM内存区域的划分

一个Java写的程序跑起来,就得到了一个Java进程 = JVM + 上面运行的字节码指令;

进程:操作系统资源分配的基本单位;

内存区域的划分:

1.程序计数器

在内存空间里(比较小的空间),保存了下一个要执行的指令的内存地址(元数据区的地址);

这里的"下一条要执行的指令"是Java的字节码(不是CPU的二进制机器语言);

2.堆

JVM上最大的空间, new出来的对象都在堆上;

3.栈

函数中的局部变量,函数的形参,函数之间的调用关系;

分为Java虚拟机栈(JVM之上,运行的Java代码的函数调用关系)本地空间栈(JVM里头C++代码的函数调用关系);

4.元数据区(方法区)

Java程序中的指令.指令都是包含在类的方法中的;

保存了代码中涉及到的类的相关信息; 类的static属性也被包含在其中;

---------------------------------------------------------------------------------------------------------------------------------

在一个Java进程中,元数据区和堆是只有一份的.

程序计数器和栈则可能有多份;

当一个Java进程中有多个线程的时候,每个线程都有自己的程序计数器和栈,线程就代表一个"执行流";

---------------------------------------------------------------------------------------------------------------------------------

代码中的变量都处在上述的哪个区域呢?

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

(1)局部变量 -> 栈;

(2)成员变量 -> 堆;

(3)静态成员变量 -> 元数据区(方法区);

---------------------------------------------------------------------------------------------------------------------------------

2.JVM类加载的过程

Java程序 -> .java文件;

javac编译 -> .class文件;

运行Java进程的时候,JVM就要读取.class里面的内容,并且执行里面的命令;

读取.class内容的过程就是类加载的过程.

把类涉及到的字节码从硬盘读取到内存中(元数据区).

加载一个.class文件,就会用.class里的指令创建一个类对象.

类对象中就包含了.class文件中的各种信息,基于类对象就能创建该类的实例;

比如:类的名字;

类有哪些属性,属性名是什么,每个属性类型是什么:public/private;

类有哪些方法,每个方法名是什么,参数是什么,方法的类型:public/private;

继承的父类有哪些,实现的接口有哪些...

因为有了类对象,才能进行反射,反射的api都是从类对象中获取信息的;

类加载的输入:.class文件(类的全限定名);

类加载得到的结果:内存中对应的类对象;        

---------------------------------------------------------------------------------------------------------------------------------

类加载的具体步骤

1.加载

把.class文件找到; 在代码中先见到类的名字,然后进一步的找到对应的.class文件.

打开并读取文件内容;

---------------------------------------------------------------------------------------------------------------------------------

2.验证

验证读到的.class文件中的数据是否正确,是否合法;

Java标准文档中,明确定义了.class文件的格式是怎么样的;

magic:计算机圈子约定俗成的做法.

二进制文件,会在开头的若干个字节,设置一个固定的常数进去;

通过这个常数,标识当前这个文件是啥样的文件.

minor_version/major_version:需要确保编译时使用的JDK和运行时使用的JDK版本一致;

JDK是可以向前兼容的(使用Java8的JDK编译出的.class文件放到Java17一般是可以运行的)

---------------------------------------------------------------------------------------------------------------------------------

3.准备

分配内存空间;

最终需要得到类对象 -> 需要内存;

根据刚才读取到的内容,确定出类对象所需要的内存空间,申请这样的内存空间,

并且把内存空间中所有的内容,都初始化为0;

(Java创建一个内存空间,都会把这个内存空间全设为0,后续再进一步初始化;

C/C++则不会进行置零操作,此时内存上对应的数据是上次使用残留的)

---------------------------------------------------------------------------------------------------------------------------------

4.解析

主要是针对类中的字符串常量进行处理;

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程.

有很多其他的指令使用"hello"字符串常量,在文件中,这些指令就会使用"偏移量"表示"hello"字符串;

---------------------------------------------------------------------------------------------------------------------------------

5.初始化

针对类对象做最终的初始化操作;

执行静态成员的赋值语句;

执行类中的静态代码块;

针对父类也要进行加载(如果当前加载的类有父类,并且父类还没有被加载,这个环节也会触发对父类的加载);

---------------------------------------------------------------------------------------------------------------------------------

双亲委派模型

更准确应该叫单亲委派模型/父亲委派模型;

类加载五个部分,第一个步骤中里面的一个环节;

给定类的全限定名,找到对应的class文件的位置;

---------------------------------------------------------------------------------------------------------------------------------

类加载器:JVM中的功能模块,JVM中,已经内置了一些类加载器完成上述的"类加载"过程;

JVM默认有三个类加载器:

BootstrapClassLoader;(爷爷)

负责加载标准库的类;(标准库类,有一个专门的存放位置)

ExtensionClassLoader;(爸爸)

负责加载一些扩展类;(JVM厂商,希望对Java的功能做出一些扩展)

ApplicationClassLoader;(儿子)

负责加载第三方库中的类/自己写的代码的类; 

注意:

这里的父子关系不是Java中父类子类这样的继承关系;

每个类加载器有个parent这样的引用指向父亲;

---------------------------------------------------------------------------------------------------------------------------------

双亲委派模型的工作流程:

输入:类的全限定名(字符串),类似于java.lang.String

得到:找到对应的.class文件.

请求先会一直向上传递,若父亲没有找到,请求才会交给儿子;

若在某一加载器找到了,请求就结束了,就会执行类加载2345步骤;

如果都没有找到,就会抛出异常ClassNotFoundException;

这样模型的目的就是:防止用户自己写的类,把标准库的类覆盖掉.

保证标准库的类,被加载的优先级是最高的,标准库其次,第三方库优先级最低;

---------------------------------------------------------------------------------------------------------------------------------

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

C/C++中,malloc/new一个对象,都需要手动释放内存free/delete,如果不释放就会造成内存泄露;

垃圾回收机制就是为了让程序员可以放心大胆的new对象,防止内存泄漏问题;

GC也是有代价的,需要消耗一定的性能,进行GC的时候可能会触发STW问题(Stop The World)导致程序卡顿,故C/C++没有引入GC;

GC需要负责回收的区域有哪些:

1.程序计数器/栈:

都是跟随线程的,不需要GC.

2.堆:

GC回收的主战场,

3.元数据区:

类对象->类加载,一个程序中要加载的类都是有上线的.不会出现无限增长,内存泄漏的情况;

---------------------------------------------------------------------------------------------------------------------------------

GC是如何进行回收的:

以对象为维度进行回收的.

---------------------------------------------------------------------------------------------------------------------------------

1.先找出谁是垃圾.

需要针对每个对象分别判定.

方案一:引用计数(Java没有采纳)

在Java中使用对象一般都是通过"引用"来实现的;

如果一个对象如果没有引用指向了,就可以认为这个对象是垃圾了;

给每个对象分配一个计数器,衡量有多少个引用指向,

每次增加一个引用,计数器+1; 每次减少一个引用,计数器-1;

当计数器减为0,此时这个对象就是垃圾了;

 在JVM中并没有采纳,Python/PHP使用这种方案;

存在以下两个问题:

(1)消耗额外的空间;

(2)引用计数可能会导致"循环引用"使得上述的判定出错;(需要引入"环路检测"机制来解决,代价更大了)

---------------------------------------------------------------------------------------------------------------------------------

方案二:可达性分析(Java采用的方案)

用时间换空间;

在JVM中,专门搞了一波线程,周期性的扫描代码中的所有对象,判断某个对象是否"可达"(可以被访问到)

对应的,"不可达"的对象就是垃圾了;

可达性分析的起点叫做GC root;

一个程序中不是只有一个GC root, 而是有很多个;

哪些变量可以作为GC root 呢:

(1)栈上的局部变量(引用类型);

(2)方法区中,静态的成员(引用类型);

(3)常量池中引用指向的对象;

把所有的GC root都遍历一遍 ,针对每一个尽量往下延伸;

---------------------------------------------------------------------------------------------------------------------------------

2.释放垃圾的内存空间.

方案一:标记-清除方法

针对内存中的对应对象进行释放:

这样的做法,会引入"内存碎片问题":

很可能要释放的多个内存,不是连续的,

虽然把上述内存释放掉,但是整体上这些释放掉的空间并没有连在一起;

后续申请内存空间的时候就可能申请不了,因为申请的内存是连续的;

---------------------------------------------------------------------------------------------------------------------------------

方案二:复制算法

同一时刻只使用内存的其中一半:

把不是垃圾的对象都拷贝到内存的另一侧,然后把存放垃圾的这一半内存全部释放掉;

缺点:

1.内存空间利用率低;

2.如果存活下来的对象比较多,复制成本也比较大;

方案三:标记-整理方法

---------------------------------------------------------------------------------------------------------------------------------

非常类似于顺序表删除中间元素的过程;

缺点:搬运的开销也比较大;

---------------------------------------------------------------------------------------------------------------------------------

方案四:分代算法(JVM采取的方法)

把上述几个方案综合一下,取长补短;

JVM根据对象的年龄(根据周期性的可达性分析来计算年龄),把对象进行区分:

新生代:一般创建的对象都会进入新生代;

老年代:大对象和经历了 N 次(⼀般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代

移动到老年代.

刚创建的对象就处在伊甸区(比较大);

根据经验规律,绝大部分对象是活不过第一轮GC的,

留存下来的对象,就会被拷贝到幸存区(比较小,内存浪费少);

幸存区是两个相等的空间,按照复制算法进行处理;

反复进行多次...

当对象年龄不断增长就会被放到老年代的区域;

根据经验规律,老年代的对象,生命周期比较长;

对于老年代,进行GC的频率就会降低,另外老年代是通过标记整理方法来回收(次数比较少,时间开销浪费也少);

---------------------------------------------------------------------------------------------------------------------------------

垃圾回收器

"分代回收"是JVM的GC中的基本思想方法.

具体落实到JVM实现层面上,JVM还提供了多种的"垃圾回收器";

对上述的分代回收做进一步的拓展和实现;

关注CMS/G1即可

CMS

把整个GC过程拆成多个阶段,能和业务线程并发执行的就尽量并发,

尽可能减少STW的时间;

G1

把整个内存分成很多块,不同的颜色/字母表示这是新生代(伊甸区/幸存区)/老年代;

进行GC的时候,不要求一个GC就把所有的内存都回收一边,而是一轮GC只回收其中的一部分;

限制一轮GC花的时间/工作量,使STW的时间在可控范围内;


http://www.ppmy.cn/devtools/134018.html

相关文章

Spring Boot MySQL 分库分表

1.首先需要在 pom.xml 中配置相关依赖 <properties><java.version>17</java.version><shardingsphere.version>5.0.0</shardingsphere.version><sharding-jdbc-spring-boot-starter.version>4.1.1</sharding-jdbc-spring-boot-starter…

C++ 中的异常处理机制是怎样的?什么情况下应该使用异常处理?异常处理的优缺点是什么?

1) C 中的异常处理机制是怎样的&#xff1f; 异常是一种处理错误的方式&#xff0c;当一个函数发现自己无法处理的错误时就可以抛出异常&#xff0c;让函数的直接或间接的调用者处理这个错误 throw: 当问题出现时&#xff0c;程序会抛出一个异常。这是通过使用 throw 关键字来…

微服务架构面试内容整理-消息驱动-RocketMQ

RocketMQ 是一个开源的分布式消息中间件,由阿里巴巴开发,具有高可靠性、高性能和可伸缩性的特点。它适用于各种场景的消息传递,包括异步处理、事件驱动架构和微服务之间的通信。以下是 RocketMQ 的主要特点、工作原理和使用场景: 主要特点 1. 高吞吐量和低延迟: RocketMQ…

深入探索 React Hooks:原理、用法与性能优化全解

一、引言 在现代 React 开发领域,Hooks 已成为不可或缺的一部分,赋予函数组件强大功能,使其能胜任复杂任务。本文将全面剖析 React Hooks,助您深入理解并熟练运用。 二、React Hooks 是什么 (一)Hooks 出现的背景 早期 React 主要依赖类组件,其通过this.state管理状…

vue2和vue3的区别详解

vue2 VS vue3 对比vue2vue3配置脚手架cmd命令行可视化方式创建脚⼿架组件通信props、$emit、provide、$arrts、EventBus等props、$emit、provide、inject、arrts等数据监听watch,computedwatch,watchEffect,computed双向绑定Object.definePropertyProxyAPI⽣命周期四个阶段befo…

学习笔记——PLCT汪辰:开发RISC-V上的操作系统(持续更新)

目录 第0章 下载源码 运行环境 构建和使用说明 第1章 记录一个本人没听说过的架构 第2章~第4章 第0章 下载源码 git clone https://gitee.com/unicornx/riscv-operating-system-mooc.git 运行环境 推荐使用 Ubuntu 20.04&#xff0c;Ubuntu 20.04 是目前最新的 Ubun…

element-ui】使用el_upload上传文件无法动态修改action

问题&#xff1a;最近在使用el_upload上传文件时&#xff0c;发现无法动态修改action的值&#xff0c;进行提交时&#xff0c;caseId2还是默认值null 原因&#xff1a;el-upload的先执行上传&#xff0c;后执行action里的响应&#xff0c;也就是赋值等操作。 解决方法&#x…

LeetCode【0059】螺旋矩阵II

本文目录 1 中文题目2 求解方法&#xff1a;数理逻辑2.1 方法思路2.2 Python代码2.3 复杂度分析 3 题目总结 1 中文题目 给定一个正整数 n n n &#xff0c;生成一个包含 1 1 1 到 n 2 n^2 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n n x n nxn 正方形矩…