【Java常见问题】基础知识(七)内存管理(垃圾回收机制)
内存管理与系统安全性紧密相关。在C/C++中,程序员需要自己管理内存的申请和释放,然而在Java、Go等语言中,则有了较为完善的垃圾回收机制,程序员不需要关心一些底层的细节也能很好地开发应用。然而了解一些底层机制始终是有必要的,它可以提升开发效率,便于在出现bug时纠正。在笔者之前的一篇文章中,【Go学习】高级特性(二)垃圾回收,曾写了Go的垃圾回收机制,这篇文章就来探讨一下Java的。由于基本思想是互通的,本文仅讨论不同的点。
HotSpot采用的算法是分代算法。
分代思想
- 分为年轻代、老年代、永久代。
- 年轻代分为:
Eden | Survivor | Survivor |
---|
- Java8中移除了永久代,转而由MetaSpace代替(一块本地化的堆内存区)
为什么要移除永久代?
- 发现这部分区域的内存经常不够用或发生泄露。
- 虚拟机之一的JRockit没有永久代。
MetaSpace如何进行内存分配?
基本思想:类与类加载器有相同的生命周期
Java中垃圾回收算法的演进
- 串行回收
Java1.3.1之前仅支持Serial(串口)
- 并行回收
利用多核特性
- 并发标记清理回收(CMS,Mostly Concurrenct Mark and Sweep)
垃圾回收与用户程序同时进行
- 并发回收(G1) 清理大的堆内存空间时可满足特定的暂停应用的时间
关于CMS?
在年轻代中使用拷贝算法(会暂停用户线程),在老年代中使用CMS(避免将用户线程暂停太长时间)。
如何实现呢?通过一个空闲链表来管理回收的空间。
具体执行步骤分为如下几个阶段:
- 初始标记:
标记的是老年代中由root直接可达、或被年轻代引用的对象。
- 并发标记:
遍历老年代,从上一步标记的节点开始,标记所有被引用的对象。
- 并发预清理
将上一步中发生了引用关系变化的结点标记为dirty,并对其可达节点进行标记,以防遗漏。
- 并行可被终止预清理
执行部分预清理,减少最终标记阶段导致的暂停时间
- 重标记
暂停用户线程,最终确定并标记老年代中的存活对象
- 并行清理
删除不再被使用的对象并回收相应内存空间
- 并发重置
重置数据结构,为下一次清理做准备
关于G1?
简介
全称为Garbage First,顾名思义,回收的垃圾优先。(由于看不懂网上的文章,笔者去翻了一下官方文档),以下是对关键信息的提炼:
G1会尽最大可能地保证能暂停用户线程;几乎不需要配置;能保证在延迟和高吞吐之间找到平衡。
它适用的应用和环境有如下特点:
- 堆大小达到10GB或以上,50%的Java堆都存在活数据(笔者注:服务器级别的)
- 对象分配和提升的速率可能会随时间发生显著变化。
- 堆中有大量碎片
- 可预测的目标暂停时间不超过几百毫秒。
使用方法
截止java9,它仍 默认 是GC模式(后面的我没看了),如果需要显式指定,就在命令行中加入-XX:+UseG1GC
参数。
基础概念
G1是一个分代的、递增的、并行的、主要是并发的、将所有应用线程暂停下来的、疏散的垃圾回收器。它在每一个暂停阶段都能管理目标暂停时间(也就是预测)。
空间重用集中在年轻代,偶尔会发生在老年代。
上图可理解为对上上图的更详细说明。
上图解释了 疏散 的原理:
在选定的内存区域内找到要收集的活动对象并复制到新的内存区域,并在此过程中压缩它们。疏散完成后,活动对象先前占用的空间将重新用于应用程序分配。
注意它不是一个实时的模型,不能绝对保证就是暂停预测时间,只是能保证在相对长的时间之后尽可能地达到目标。
堆分层模型
上图来源于官方文档,显示了G1对于堆元素的划分方式,它们被划分为了一个个大小相同的块。其中灰色的表示空;蓝色的表示老年代;红色的表示年轻代(青春是热情似火的);“S”字符表示“Survivor”,还存活;“H”表示“humongous”,巨大的(也就是连续占了不止一个小块)
垃圾回收循环
上图源于官方文档,显示了G1算法中的几个阶段。
其他关于G1的实现及设置
- 关于初始堆容量比例的设置
这个数值和老年代的占比息息相关
-
关于标记的问题 笔者认为这一段可以总结为:由于循环的存在,不用过多担心标记错误的问题,它总会在后来的阶段被正确标记。
-
堆资源很紧张的情况下会如何做? 上图指出了几大关键点:
- 撤离失败发生的背景:应用程序保持活动状态的内存过多,以至于撤离找不到足够的空间进行复制
- 表现:G1 尝试通过将已移动的任何对象保留在其新位置来完成当前垃圾回收,而不复制任何尚未移动的对象,仅调整对象之间的引用
- 时机:撤离失败被假定发生在垃圾回收接近尾声时。此时大多数对象都已移动,并且有足够的空间继续运行应用程序
- 如果还是不行,就会启用full-gc方案(我的理解是全部推倒重来)
- 关于堆中的大对象 上图指出了如下几点:
- 大对象的存放位置及形式:对象的起始位置就是块的起始区域,序列最后一个区域中的任何剩余空间都将丢失以进行分配。
- 重回收的时机:在清理暂停期间 标记快结束时、或者Full-gc阶段。
- 给大对象分配空间有可能导致垃圾回收过早执行
- 大对象的位置不能动
浅谈JVM调优所使用的常见参数
JVM的管理是一个很重要的问题,比如单就针对OutOfMemoryError Exception
这一类报错,官方文档就洋洋洒洒地写了一大段文字,列举了多种情况:
感兴趣的uu可以看看原文:
Understand the OutOfMemoryError Exception
那么在实际调优的过程中,可以从哪些方面去考虑呢?可以从JVM机制对应的内容去考虑:(具体参数参考官方文档)
- 堆内存相关
- 指定最大堆和最小堆的值(建议设为相同的值)
- 设置MetaSpace的大小
- 设置年轻代空间的初始大小、最大值、整个年轻代空间的大小(可用于计算老年代空间)
- 将对内存写入文件,便于诊断问题
- 指定垃圾回收算法
- 记录并查看GC日志
常见问题
Java是否存在内存泄露的问题?
内存泄露主要有两种情况:
- 堆中申请的空间没有被释放。垃圾回收机制可以解决这一问题
- 对象不再使用,但仍保留在内存中。Java已有的机制无法保证这一种情况不出现
如下场景可能会导致上述第二种情况的发生:
- 使用静态集合类,比如静态的容器Vector、HashMap等
- 不及时关闭各种连接,如数据库、网络、I/O等
- 释放对象的同时没有删除相应监听器
- 变量定义的作用范围大于其使用范围则有一定风险
- 使用单例模式
Java中堆和栈的区别?
- 栈主要存放基本数据类型和引用变量;堆用于存放运行时创建的对象
- 栈用于执行程序时,变量会通过压栈出栈操作在栈中被回收;堆中的对象由gc自动回收。
参考资料
转载自:https://juejin.cn/post/7218110901551562810