likes
comments
collection
share

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

作者站长头像
站长
· 阅读数 31

内存管理与系统安全性紧密相关。在C/C++中,程序员需要自己管理内存的申请和释放,然而在Java、Go等语言中,则有了较为完善的垃圾回收机制,程序员不需要关心一些底层的细节也能很好地开发应用。然而了解一些底层机制始终是有必要的,它可以提升开发效率,便于在出现bug时纠正。在笔者之前的一篇文章中,【Go学习】高级特性(二)垃圾回收,曾写了Go的垃圾回收机制,这篇文章就来探讨一下Java的。由于基本思想是互通的,本文仅讨论不同的点。

HotSpot采用的算法是分代算法。

分代思想

  • 分为年轻代、老年代、永久代。
  • 年轻代分为:
EdenSurvivorSurvivor
  • Java8中移除了永久代,转而由MetaSpace代替(一块本地化的堆内存区)

为什么要移除永久代?

  • 发现这部分区域的内存经常不够用或发生泄露。
  • 虚拟机之一的JRockit没有永久代。

MetaSpace如何进行内存分配?

基本思想:类与类加载器有相同的生命周期

Java中垃圾回收算法的演进

  • 串行回收

Java1.3.1之前仅支持Serial(串口)

  • 并行回收

利用多核特性

  • 并发标记清理回收(CMS,Mostly Concurrenct Mark and Sweep)

垃圾回收与用户程序同时进行

  • 并发回收(G1) 清理大的堆内存空间时可满足特定的暂停应用的时间

关于CMS?

在年轻代中使用拷贝算法(会暂停用户线程),在老年代中使用CMS(避免将用户线程暂停太长时间)。

如何实现呢?通过一个空闲链表来管理回收的空间。

具体执行步骤分为如下几个阶段:

  • 初始标记:

标记的是老年代中由root直接可达、或被年轻代引用的对象。

  • 并发标记:

遍历老年代,从上一步标记的节点开始,标记所有被引用的对象。

  • 并发预清理

将上一步中发生了引用关系变化的结点标记为dirty,并对其可达节点进行标记,以防遗漏。

  • 并行可被终止预清理

执行部分预清理,减少最终标记阶段导致的暂停时间

  • 重标记

暂停用户线程,最终确定并标记老年代中的存活对象

  • 并行清理

删除不再被使用的对象并回收相应内存空间

  • 并发重置

重置数据结构,为下一次清理做准备

关于G1?

简介

全称为Garbage First,顾名思义,回收的垃圾优先。(由于看不懂网上的文章,笔者去翻了一下官方文档),以下是对关键信息的提炼:

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

G1会尽最大可能地保证能暂停用户线程;几乎不需要配置;能保证在延迟和高吞吐之间找到平衡。

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

它适用的应用和环境有如下特点:

  • 堆大小达到10GB或以上,50%的Java堆都存在活数据(笔者注:服务器级别的)
  • 对象分配和提升的速率可能会随时间发生显著变化。
  • 堆中有大量碎片
  • 可预测的目标暂停时间不超过几百毫秒。

使用方法

截止java9,它仍 默认 是GC模式(后面的我没看了),如果需要显式指定,就在命令行中加入-XX:+UseG1GC参数。

基础概念

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

G1是一个分代的、递增的、并行的、主要是并发的、将所有应用线程暂停下来的、疏散的垃圾回收器。它在每一个暂停阶段都能管理目标暂停时间(也就是预测)。

空间重用集中在年轻代,偶尔会发生在老年代。

【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 上图可理解为对上上图的更详细说明。

【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 上图解释了 疏散 的原理:

在选定的内存区域内找到要收集的活动对象并复制到新的内存区域,并在此过程中压缩它们。疏散完成后,活动对象先前占用的空间将重新用于应用程序分配。

【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 注意它不是一个实时的模型,不能绝对保证就是暂停预测时间,只是能保证在相对长的时间之后尽可能地达到目标。

堆分层模型

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

上图来源于官方文档,显示了G1对于堆元素的划分方式,它们被划分为了一个个大小相同的块。其中灰色的表示空;蓝色的表示老年代;红色的表示年轻代(青春是热情似火的);“S”字符表示“Survivor”,还存活;“H”表示“humongous”,巨大的(也就是连续占了不止一个小块)

垃圾回收循环

【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

上图源于官方文档,显示了G1算法中的几个阶段。

其他关于G1的实现及设置

  • 关于初始堆容量比例的设置

这个数值和老年代的占比息息相关 【Java常见问题】基础知识(七)内存管理(垃圾回收机制)

  • 关于标记的问题 【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 笔者认为这一段可以总结为:由于循环的存在,不用过多担心标记错误的问题,它总会在后来的阶段被正确标记。

  • 堆资源很紧张的情况下会如何做? 【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 上图指出了几大关键点:

  1. 撤离失败发生的背景:应用程序保持活动状态的内存过多,以至于撤离找不到足够的空间进行复制
  2. 表现:G1 尝试通过将已移动的任何对象保留在其新位置来完成当前垃圾回收,而不复制任何尚未移动的对象,仅调整对象之间的引用
  3. 时机:撤离失败被假定发生在垃圾回收接近尾声时。此时大多数对象都已移动,并且有足够的空间继续运行应用程序
  4. 如果还是不行,就会启用full-gc方案(我的理解是全部推倒重来)
  • 关于堆中的大对象 【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 上图指出了如下几点:
  1. 大对象的存放位置及形式:对象的起始位置就是块的起始区域,序列最后一个区域中的任何剩余空间都将丢失以进行分配。
  2. 重回收的时机:在清理暂停期间 标记快结束时、或者Full-gc阶段。
  3. 给大对象分配空间有可能导致垃圾回收过早执行
  4. 大对象的位置不能动

浅谈JVM调优所使用的常见参数

JVM的管理是一个很重要的问题,比如单就针对OutOfMemoryError Exception这一类报错,官方文档就洋洋洒洒地写了一大段文字,列举了多种情况: 【Java常见问题】基础知识(七)内存管理(垃圾回收机制) 感兴趣的uu可以看看原文: Understand the OutOfMemoryError Exception

那么在实际调优的过程中,可以从哪些方面去考虑呢?可以从JVM机制对应的内容去考虑:(具体参数参考官方文档)

  • 堆内存相关
    • 指定最大堆和最小堆的值(建议设为相同的值)
    • 设置MetaSpace的大小
    • 设置年轻代空间的初始大小、最大值、整个年轻代空间的大小(可用于计算老年代空间)
  • 将对内存写入文件,便于诊断问题
  • 指定垃圾回收算法
  • 记录并查看GC日志

常见问题

Java是否存在内存泄露的问题?

内存泄露主要有两种情况:

  • 堆中申请的空间没有被释放。垃圾回收机制可以解决这一问题
  • 对象不再使用,但仍保留在内存中。Java已有的机制无法保证这一种情况不出现

如下场景可能会导致上述第二种情况的发生:

  • 使用静态集合类,比如静态的容器Vector、HashMap等
  • 不及时关闭各种连接,如数据库、网络、I/O等
  • 释放对象的同时没有删除相应监听器
  • 变量定义的作用范围大于其使用范围则有一定风险
  • 使用单例模式
Java中堆和栈的区别?
  • 栈主要存放基本数据类型和引用变量;堆用于存放运行时创建的对象
  • 栈用于执行程序时,变量会通过压栈出栈操作在栈中被回收;堆中的对象由gc自动回收。

参考资料

转载自:https://juejin.cn/post/7218110901551562810
评论
请登录