【Java】JVM内存回收

2023-11-15

SafePoint检查

Safepoint 可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停。在 SafePoint 保存了其他位置没有的一些当前线程的运行信息,供其他线程读取。这些信息主要为线程上下文的任何信息,例如对象或者非对象的内部指针等等。一般这么理解 SafePoint,就是线程只有运行到了 SafePoint 的位置,他的一切状态信息,才是确定的,也只有这个时候,才知道这个线程用了哪些内存,没有用哪些内存;并且,只有线程处于 SafePoint 位置,这时候对 JVM 的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,之后继续运行,每个线程都有一份自己的内存使用快照,这时候其他线程对于内存使用的修改,线程就不知道了,只有再进行到 SafePoint 的时候,才会感知。

安全点检查,确认当前线程的运行信息。

GC 一定需要所有线程同时进入 SafePoint,并停留在那里,等待 GC 处理完内存,再让所有线程继续执。像这种所有线程进入 SafePoint 等待的情况,就是 Stop The World。

在 SafePoint 位置保存了线程上下文中的任何东西,包括对象,指向对象或非对象的内部指针,在线程处于 SafePoint 的时候,对这些信息进行修改,线程才能感知到。所以,只有线程处于 SafePoint 的时候,才能针对线程使用的内存进行 GC,以及改变正在执行的代码。例如 OSR (On Stack Replacement,栈上替换现有代码为 JIT 优化过的代码)或者 Bailout(栈上替换 JIT 过优化代码为去优化的代码)。

参考资料:
https://zhuanlan.zhihu.com/p/161710652

SafePoint的放置

openjdk 中,安全点的实现位于 openjdk/hotspot/src/share/vm/runtime/safepoint.cpp 中。

HotSpot 为例,什么地方可以放置 SafePoint 或者什么地方能放置 SafePoint?

  1. 理论上,在解释器的每条字节码的边界都可以放一个 safepoint,不过挂在 safepoint 的调试符号信息要占用内存空间,如果每条机器码后面都加 safepoint 的话,需要保存大量的运行时数据,所以要尽量少放置 safepoint,在 safepoint 会生成 polling 代码询问 VM 是否要“进入 safepoint”,polling 操作也是有开销的。
  2. 通过 JIT 编译的代码里,会在所有方法的返回之前,以及所有非 counted loop 的循环(无界循环)回跳之前放置一个 safepoint,为了防止发生 GC 需要 STW 时,该线程一直不能暂停。另外,JIT 编译器在生成机器码的同时会为每个 safepoint 生成一些“调试符号信息”,为 GC 生成的符号信息是 OopMap,指出栈上和寄存器里哪里有 GC 管理的指针。

参考文档:https://blog.csdn.net/Candyz7/article/details/127526703
https://blog.csdn.net/WZH577/article/details/109782827

总结:

  1. SafePoint 可以放置在每条字节码的边界,不过会带来较大开销;
  2. 在 JIT 编译的代码中,在所有方法返回之前以及无界循环回跳之前放置 SafePoint。

无界循环,即不知道什么时候会跳出的循环。常见的有 while(true) 、无跳出明确跳出条件的 for 以及使用 long 来表示循环次数等。比如:

for (long i = 1; i <= 1000000000; i++) {
    boolean b = 1.0 / i == 0;
}

在 java1.8.131 或者以上的版本, 在 JVM 运行参数中加上
-XX:+UseCountedLoopSafepoints 参数,可以强制在可数循环中创建安全点。这样的操作可以让所有线程提前进入安全点,触发碎片化的 GC 而不是累积变成 full GC ,这样也是优化的手段。

STW的机制

线程在阻塞之前需要生成 OopMap(Ordinary object pointer Map,普通对象指针 Map)。没有 OopMap ,就需要扫描整个运行栈,查找根对象。OopMap 更像是一种空间换时间的策略,牺牲小部分的空间用来存储对象指针,避免了遍历扫描栈所带来的时间消耗。因为相比内存的价格,降低 GC 延时明显更重要。

在 STW 之前,要开启 SafePoint;若开启 SafePoint,则要将 polling_page 物理页属性变为不可读。在 Hotspot 中,有 SafepointSynchronize::begin 函数,其中有一行代码 os::_polling_page

如果 os::_polling_page 对应的物理页属性是可读的,这段代码并没什么特殊意义。但是如果是不可读的,读的时候就会触发段异常,对应的操作系统信号:SIGSEGV 。

JVM 捕获了这个 OS 异常,并进行了处理。所有的线程都是在这个地方 STW 的。

参考文档:https://www.jb51.net/article/235673.htm

总结:

  1. 开启 SafePoint,修改 polling_page(轮询页)为物理不可读。
  2. 其他线程进入 SafePoint 会去读取 polling_page;
  3. 读取时会触发段异常,对应的操作系统信号为 SIGSEGV;
  4. JVM 捕获异常并进行处理,使线程阻塞在当前位置。

并发清除阶段

在 JDK 1.5 中,出现了** CMS (Concurrent 一Mark 一 Sweep)收集器**,这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。CMS 收集器采用的是并发回收(非独占式)。

并发清除( Concurrent一Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

CMS 采用的是 Mark Sweep 方式清除,会造成内存碎片,那么为什么不把算法换成 Mark Compact 呢?

因为当并发清除的时候,用 Compact 整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact 更适合“Stop the World”这种场景下使用。而 CMS 为了实现低延时,就会尽量避免 STW ,故采用 Mark Sweep 的方式。

垃圾回收器的选择:

  • 如果你想要最小化地使用内存和并行开销,请选Serial GC;
  • 如果你想要最大化应用程序的吞吐量,请选Parallel GC;
  • 如果你想要最小化GC的中断或停顿时间,请选CMS GC。

参考文档:
https://blog.csdn.net/qq_51409098/article/details/126739012

G1回收器

因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region) (物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等。

G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First) 。

GC如何释放物理内存

GC 要想释放内存,必定需要通过 JVM 将 Java 应用的内存占用归还给 OS ,这样才能降低物理内存占用。

GC 执行标记完毕,下次分配内存的时候,就能够分配到被标记的地址。
这属于非常惰性的操作,实际内存占用上并没有达到降低内存的效果。如果后续分配的内存比较少,那么内存将会迟迟得不到释放,影响性能。

对于内存实际占用上,依赖于 JVM 的底层调用。

对象占用内存的清除,在应用和操作系统之间,还有个“内存二道贩子”,叫malloc。在应用释放内存之后(即 JVM 执行 GC 之后),就会触发 free 操作,但是 free 操作之后,内存也不一定就真的还给操作系统了,可能是还给内存二道贩子了。这样造成的后果就是实际占用的物理内存并没有降低。

这些内存二道贩子的实现,常见的有 arena、glibc、ptmalloc、ptmalloc2、jemalloc 等。

glibc:高地址的内存没有被回收掉,低地址的内存不允许被回收。
jemalloc:是一个能够快速分配/回收内存,减少内存碎片,对多核友好,具有可伸缩性的内存分配器。

JVM 在启动时保留内存并向操作系统请求额外的内存,直到达到配置的任何限制。它以块增量的方式执行此操作,一次请求 MB 或更多内存,因为从 OS 逐字节请求内存效率非常低。

批量操作,思想类似于令牌桶。一次性请求批量,避免频繁请求触发系统调用。

在 Java 中,如果需要手动操作,使用堆外内存,主要通过 Unsafe 类来实现。可以通过反射调用获取到 Unsafe 类并且调用 unsafe.allocateMemory()unsafe.freeMemory() 方法。

void* os::malloc(size\_t size, MEMFLAGS memflags, const NativeCallStack& stack) {
    ...
    u_char* ptr;
    ptr = (u_char*)::malloc(alloc_size);   //调用C++标准库函数 malloc(size)
    ....
    // we do not track guard memory
    return MemTracker::record\_malloc((address)ptr, size, memflags, stack, level);
}

主要底层就是 C++ 的标准库函数 malloc 函数。

如果在使用 gdp dump 出内存信息之后,发现使用到了本地内存,而且不是用 unsafe 分配的本地内存,那么就可以判断是自行调用了 C 库来分配内存。

参考文档:
https://blog.csdn.net/u012804784/article/details/123124325
https://blog.csdn.net/weixin_70730532/article/details/124734986
https://www.codenong.com/54267714
https://blog.csdn.net/xmtblog/article/details/118004663
http://t.csdn.cn/SD5BW

Java加载so解决方案

SO 文件就是动态链接库,都是 C/C++ 编译出来的。与 Java 比较它通常是用的 Class 文件(字节码)。

通过 Java 函数 System.load 进行全局静态的 so 加载/卸载。业务场景有对 so 实现动态加载/替换的需求,但 Java 并没有直接动态加载 so 的机制。

在 System.load 以及 ClassLoader.java 中:

public NativeLibrary(Class<?> fromClass, String name, boolean isBuiltin) {
    this.name = name;
    this.fromClass = fromClass;
    this.isBuiltin = isBuiltin;
}

没法通过 System.load() 重复加载同名 so 或者直接动态替换 so,也没法在 Java 层拿到 dlopen 返回的句柄,所以我们没法在 Java 代码层实现 so 的动态加载。还有一种做法是先卸载 (System.unload) ,再加载 (System.load) ,但这个过程不是无损的。

实现动态加载,可以参考:https://cloud.tencent.com/developer/article/1005860?from=15425

JVM 支持启动的时候用环境变量来指定内存分配的 so 文件。为了实现修改 Java 使用的内存管理库函数,我们可以拿到指定需要的库函数,打包成 so 文件,最后在 SystemPath 中链接上,修改启动参数即可。

<dependency>
  <groupId>xxx</groupId>
  <artifactId>engine</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>
    ${pom.basedir}/lib/xxx.jar
  </systemPath>
</dependency>

启动参数,我们需要指定为 java -Djava.ext.dirs=./lib -jar target/xxx.jar 。这样就完成了 so 文件引入了本地 jar 包,使用指定库函数完成了默认函数的替换。

参考资料:
https://www.likecs.com/show-204352481.html

OS最大支持的内存

最大支持内存和操作系统有直接关系,即使是 64 位处理器,使用 32 位操作系统支持的内存也最多为 2 的 32 次方,就是 4G。在 Windows32 位操作系统中最大只识别 3、25 到 3、 75 之间,根据 Windows 版本不同而不同,而 64 位操作系统的寻址能力就是 2 的 64 次方。也就是 17179869184G。只是理论值,实际中不会用到这么大的内存。

这指的是 OS 理论上支持的内存值,包括 Virtual RAM 。

Java中虚拟内存和实际内存

使用 Top 命令,可以查看 Java 应用内存占用,有 VIRT 和 RES 两项。

  • VIRT 是虚拟内存空间:虚拟内存映射中所有内容的总和。它在很大程度上是没有意义的。
  • RES 是驻留集大小:当前驻留在 RAM 中的页数。在几乎所有情况下,这是 在说“太大”时应该使用的唯一数字。但这仍然不是一个很好的数字,尤其是在谈到 Java 时。

参考文档:http://events.jianshu.io/p/169f84d933a7

虚拟内存

进程消耗的虚拟内存是进程内存映射中所有内容的总和。这包括数据(例如,Java 堆),还包括程序使用的所有共享库和内存映射文件。在 Linux 上,您可以使用 pmap 命令查看映射到进程空间的所有内容。

虚拟内存是一种在不扩大实际内存容量的情况下,让内存看上去能放下更多程序的方法。这是怎么做到的?

在虚拟内存技术出现之前是将完整的程序从外存(如磁盘)读入内存中,但是现在虚拟内存不这么做,虚拟内存技术将一个完整的程序切割成多份,当 CPU 要执行这个程序时,内存先把该程序的第一份送入 CPU,然后马上又问磁盘拿同一个程序的第二份内容,然后再送入 CPU。这样做就使得内存中可以出现更多的程序头(程序的第一份),而不是一个完整的程序占满整个内存。

说到这里其实还没讲到虚拟内存最精髓的地方,“虚”到底虚在哪?虚拟内存和实际内存都存储着多个程序头(被切割出来的第一份),但是虚拟内存胆子很大,他敢记录实际物理内存中没有记录的程序头。所以在容量上看,虚拟内存比实际物理内存要大很多,“虚”就是“比实际更多”的意思。你可能觉得很奇怪,虚拟内存表里记录了在实际物理内存不存在的程序头,那 CPU 是怎么从实际物理内存中读到不存在的程序头的?这个简单,CPU 只会盯着虚拟内存表看,不会再管实际物理内存里有什么,当 CPU 在虚拟内存表里调用了一个在实际物理内存中不存在的程序头时,物理内存马上去外存(磁盘)里找这个程序头,然后把物理内存中不常运行的程序头踢出去(后台应用被 kill),将 CPU 需要的程序头放到这个空的位置上,供 CPU 使用。另一种情况是如果 CPU 要使用的程序头刚好实际物理内存里有,那就直接用。

参考文档:https://blog.csdn.net/weixin_42243865/article/details/122493634

  1. 虚拟内存存储程序头,使有限的物理内存能启用更多的应用;
  2. CPU 只监听虚拟内存表,查看程序头实际存储位置,决定是从外存(磁盘)还是内存(RAM)中读取程序头。
  3. 如果程序头不在物理内存中,需要由物理内存根据程序头内容,从磁盘中获取并且读入 RAM 中。若此时内存不够,则会淘汰内存中不常运行的程序头。

全文记录于 2022-11-20。

在 11-19 时与 @Slowlysee 探讨(单方面教学) 之后,系统性学习了相关的知识,记录了其中的关键与难点问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Java】JVM内存回收 的相关文章

随机推荐

  • 21.openssl编程——EVP

    21 1 EVP 简介 Openssl EVP high level cryptographic functions 提供丰富的密码学中的各种函数 EVP主要封装如下功能函数 a 实现了base64编解码BIO b 实现了加解密BIO c
  • Android Studio 界面显示不全问题

    最近想试试google的android studio开发工具 安装完成后 按照向导新建一个工程 发现向导界面太大 整个屏幕显示不过来 调整界面大小也无法搞定 我用的是ubuntu系统 笔记本分辨率为1366 768 上网百度了好多 发现不少
  • redis集群设置密码

    注意事项 1 如果是使用redis trib rb工具构建集群 集群构建完成前不要配置密码 集群构建完毕再通过config set config rewrite命令逐个机器设置密码 2 如果对集群设置密码 那么requirepass和mas
  • Prometheus 安装及应用(内容较全面)

    prometheus 安装及应用 prometheus 官网 grafana dashboards 模板 prometheus 参考示例 Prometheus普罗米修斯 希腊神话人物 是一个开源的系统监控和告警系统 在kubernetes容
  • 较完整的koa2项目结构实例、脚手架-mysql,gulp

    koa2 starter A koa2 starter by using mysql gulp includes async await pm2 express style middlewares Based on 17koa koa2 d
  • 什么是LDO的线性调整率和负载调整率?

    原文来自公众号 工程师看海 后台回复 LDO仿真文件 LDO是常见的电源架构 线性调整率和负载调整率是两个重要的参数 线性调整率 line regulation 指的是 在特定负载电流条件下 当出入电压变化时 引起的对应输出电压的变化量 从
  • 华为OD题目: 简单的解压缩算法

    华为OD题目 简单的解压缩算法 知识点栈 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 现需要实现一种算法 能将一组压缩字符串还原成原始字符串 还原规则如下 1 字符后面加数字N 表示重复字符N次 例如 压缩内容为A3 表
  • 以太坊如何通过构建智能合约来销售商品?

    如何通过构建以太坊智能合约来销售商品 这是个问题 毫无疑问 比特币已经改变了我们看待和理解什么是金钱 价值以及最近由智能合约产生的所有权的方式 这很有趣 因为几乎每个人都听说过它或加密货币 来自许多业务领域的人 不仅仅是我们 IT工作者 在
  • %f %.2f %lf %.2lf的含义

    f表示单精度浮点型 float 默认保留 lf表示双精度浮点型 double 默认保留6位 2表示只保留小数点后两位数 下面举例 include
  • Attention机制的解释

    The Illustrated Attention Attention Seq2Seq 是一种由Encoder和Decoder组成的结构 可以将输入转化成一种中间形态并在Decoder输出 而Attention则是解决方案 经典的是Bahd
  • 设计模式:简单工厂、工厂方法、抽象工厂

    参考 Java设计模式之创建型 工厂模式详解 简单工厂 工厂方法 抽象工厂 知乎 工厂方法 以生产手机为例 具体的UML图如下 这种方法的优点是对于用户来说 不再需要面对具体的生产逻辑 只需要将生产的安排工作和细节都交给工厂类去做 缺点是在
  • MATLAB算法实战应用案例精讲-【深度学习】ECANet注意力机制(附Python实现代码)

    目录 前言 算法原理 算法思想 局部跨通道交互 Local Cross Channel Interaction
  • python批量复制文件问题,Python—批量复制文件

    批量复制文件 并修改后缀名 import os shutil yml文件根路径 ymlroot E GTEA gaze 原始图片根路径 srcroot E GTEA Gaze Dataset png 目标图片根路径 dstroot E Ga
  • Hadoop命令大全

    Hadoop命令大全 1 列出所有Hadoop Shell支持的命令 bin hadoop fs help 2 显示关于某个命令的详细信息 bin hadoop fs help command name 3 用户可使用以下命令在指定路径下查
  • C++入门编程之成语接龙

    成语接龙问题 问题描述 题目描述 成语接龙是中华民族传统的文字游戏 它有着悠久的历史 也有广泛的社会基础 是老少皆宜的民间文化娱乐活动 请设计四字成语接龙游戏 功能需求 1 计一个四字成语类Idiom 包含成语内容 首字母的拼音 末尾字母
  • validation校验包常用校验注解@NotEmpty,@NotBlank,@NotNull等注解

    一 常用注解 NotEmpty NotBlank NotNull介绍 包位置 javax validation constraints xxx NotEmpty 用在集合上面 一般用来校验List类型 不能注释枚举 而且长度必须大于0 No
  • 用socket实现简单的http服务器功能

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 用最基本的socket实现http服务器的功能 上代码 import java io import java util import java net public cla
  • thymeleaf中常用的金额、数字、百分比、手机号校验及对应java中的格式化

    thy中常用的金额 数字 百分比 金额格式化 格式化为1000 000 000 23元的样式 field amount title 金额 元 align right formatter function value row index re
  • 'react-scripts' 不是内部或外部命令,也不是可运行的程序 或批处理文件--解决方案

    create react app创建的React项目时 有时在安装完其他组件后 再次运行 npm start 命令时会报以上错误 让我很郁闷 不过在上网搜了这个错后原来 是create react app有丢包的缺陷 手动安装包后 需要重新
  • 【Java】JVM内存回收

    SafePoint检查 Safepoint 可以理解成是在代码执行过程中的一些特殊位置 当线程执行到这些位置的时候 线程可以暂停 在 SafePoint 保存了其他位置没有的一些当前线程的运行信息 供其他线程读取 这些信息主要为线程上下文的