【Java】JVM内存回收
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?
- 理论上,在解释器的每条字节码的边界都可以放一个 safepoint,不过挂在 safepoint 的调试符号信息要占用内存空间,如果每条机器码后面都加 safepoint 的话,需要保存大量的运行时数据,所以要尽量少放置 safepoint,在 safepoint 会生成 polling 代码询问 VM 是否要“进入 safepoint”,polling 操作也是有开销的。
- 通过 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
总结:
- SafePoint 可以放置在每条字节码的边界,不过会带来较大开销;
- 在 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
总结:
- 开启 SafePoint,修改 polling_page(轮询页)为物理不可读。
- 其他线程进入 SafePoint 会去读取 polling_page;
- 读取时会触发段异常,对应的操作系统信号为 SIGSEGV;
- 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 memoryreturn 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
- 虚拟内存存储程序头,使有限的物理内存能启用更多的应用;
- CPU 只监听虚拟内存表,查看程序头实际存储位置,决定是从外存(磁盘)还是内存(RAM)中读取程序头。
- 如果程序头不在物理内存中,需要由物理内存根据程序头内容,从磁盘中获取并且读入 RAM 中。若此时内存不够,则会淘汰内存中不常运行的程序头。
全文记录于 2022-11-20。
在 11-19 时与 @Slowlysee 探讨(单方面教学) 之后,系统性学习了相关的知识,记录了其中的关键与难点问题。
【Java】JVM内存回收相关推荐
- Java jvm 内存回收机制
原文:Java jvm 内存回收机制 源代码下载地址:http://www.zuidaima.com/share/1782298898271232.htm 在Java中,它的内存管理包括两方面:内存分 ...
- java垃圾回收菜鸟_java程序员不懂JVM内存回收,两年后也是个菜鸟
java程序员不懂JVM内存回收,两年后也是个菜鸟 在学java程序员的时候,如果你还不懂JVM内存回收,那么你就只能是个很一般的程序员菜鸟了,那么什么是JVM内存回收呢?今天我们就来学习,都还不深入 ...
- jvm gc垃圾回收机制和参数说明amp;amp;Java JVM 垃圾回收(GC 在什么时候,对什么东西,做了什么事情)
jvm gc(垃圾回收机制) Java JVM 垃圾回收(GC 在什么时候,对什么东西,做了什么事情) 前言:(先大概了解一下整个过程) 作者:知乎用户 链接:https://www.zhihu.c ...
- java jvm内存模型_Java(JVM)内存模型– Java中的内存管理
java jvm内存模型 Understanding JVM Memory Model, Java Memory Management are very important if you want t ...
- java 手动内存回收_java内存与回收调优
要了解Java垃圾收集机制,先理解JVM内存模式是非常重要的.今天我们将会了解JVM内存的各个部分.如何监控以及垃圾收集调优. Java(JVM)内存模型 正如你从上面的图片看到的,JVM内存被分成多 ...
- java加快内存回收_java内存管理之垃圾回收及JVM调优
GC(garbage Collector 垃圾收集器) 作用:a.内存的动态分配:b.垃圾回收 注:Java所承诺的自动内存管理主要是针对对象内存的回收和对象内存的分配. 一.垃圾标记 程序计数器.J ...
- JVM内存回收算法简述
2019独角兽企业重金招聘Python工程师标准>>> 在第一代面向对象语言C++中,最让人头疼以及影响敏捷开发的无疑是内存的申请与回收 在程序运行时,使用享元设计使用的一些代码复用 ...
- Java的内存回收机制
在Java中,它的内存管理包括两方面:内存分配(创建Java对象的时候)和内存回收,这两方面工作都是由JVM自动完成的,降低了Java程序员的学习难度,避免了像C/C++直接操作内存的危险.但是,也正 ...
- Java JVM内存模型
简述JVM内存模型 线程私有的运行时数据区: 程序计数器.Java 虚拟机栈.本地方法栈. 线程共享的运行时数据区:Java 堆.方法区. 简述程序计数器 程序计数器表示当前线程所执行的字节码的行号指 ...
最新文章
- 2021年大数据基础(五):​​​​​​​​​​​​​​​​​​​​​分布式技术
- Recyclerview 添加一个数组
- 没有“好的”数据,AI就没有未来?听听云测数据怎么说
- C++ const与define
- php+微信开发+解绑,微信开发之解绑设备通知的方法
- Lisp语言: 在Windows下搭建CLisp环境
- 检测线程是否存活代码!
- boost::hana::make_pair用法的测试程序
- 计算机组成西电裘答案,西电计算机组成原理.ppt
- 创建数组表格PHP苹果价格,如何从PHP数组创建HTML表?
- (一)Eureka搭建服务注册中心
- sql语句:CASE WHEN END 的用法
- 【图像隐写】基于matlab GUI DCT数字水印嵌入与提取【含Matlab源码 943期】
- 音频处理之语音加速播放
- centos7 vi保存退出_怎么保存退出vi编辑 vi常用命令大全
- excel怎么录入身份证号码快速方便?
- mysql求和语句大全_经典SQL语句大全(1)
- 新书出版:《数字滤波器的MATLAB与FPGA实现——Altera/Verilog版(第2版)》已开始印刷出版
- Git Gitosis
- RabbitMQ镜像队列与负载均衡