资讯专栏INFORMATION COLUMN

Java 内存结构备忘录

wow_worktile / 3415人阅读

摘要:本文详细描述了堆内存模型,垃圾回收算法以及处理内存泄露的最佳方案,并辅之以图表,希望能对理解内存结构有所帮助。该区域也称为内存模型的本地区。在中,内存泄露是指对象已不再使用,但垃圾回收未能将他们视做不使用对象予以回收。

本文详细描述了 Java 堆内存模型,垃圾回收算法以及处理内存泄露的最佳方案,并辅之以图表,希望能对理解 Java 内存结构有所帮助。原文作者 Sumith Puri,本文系 OneAPM 工程师编译整理。

下图展示了 Java 堆内存模型,以及运行在 Java 虚拟机中任意 Java 应用的 PermGen (内存永久保存区域),下面的比率展示了 JVM 各代类型允许的内存大小分配情况,所有的数据均适用于 Java 1.7 及以下版本。该图也被称为 Java 内存模型的“管理区(Managed Area)”。

Java 内存结构(Java 内存模型)

除此之外,还有一块堆栈区(Stack Area),可通过 -Xss 选项进行配置。该区域存储了所有线程的堆引用、本地引用、程序计数器寄存器、代码缓存以及本地变量。该区域也称为内存模型的本地区(Native Area)。

Java 内存模型(结构)的管理区 [Young Generation/Nursery] 伊甸园区(Eden Space)

所有新对象都首先在 Eden Space 创建。一旦该区达到由 JVM 设定的任意阈值,新生代垃圾回收机制(Minor GC)就会启动。它会首先清除所有的非引用对象,并将引用对象从 "eden" 与 "from" 区移至 "to" 幸存者区。垃圾回收一结束,"from" 与 "to" 的角色(名字)就会对换。

[Young Generation/Nursery] 幸存者 1 区 (From)

这是幸存者区的一部分。或者视为幸存者区中的一个角色。这儿就是之前垃圾回收中的 "to" 角色。

[Young Generation/Nursery] 幸存者 2 区 (To)

这也是幸存者区的一部分。也可以视为幸存者区中的一个角色。垃圾回收过程中的所有引用对象都会从 "from" 与 "eden" 区移至此处。

[Old Generation] 年老代区(Tenured)

根据阈值限定的不同,对象们会从 "to" 幸存者区移至年老代区。你可以使用 -XX:+PrintTenuringDistribution 检查阈值,该指令会按照年龄显示对象(占用的字节空间)。年龄是指对象在幸存者区内移动的次数。

其他重要的标记还有 -XX:InitialTenuringThreshold-XX:MaxTenuringThreshold-XX:TargetSurvivorRatio ,这些标记能帮你实现最佳的年老代区与幸存者区使用方案。

通过设置 -XX:InitialTenuringThreshold-XX:MaxTenuringThreshold,可以指定年龄的最初值与最大值,而幸存者区 (To) 的使用率则由 -XX:+NeverTenure-XX:+AlwaysTenure 决定。前者是指永远不将对象存储到年老代区,而后者恰恰相反,总是将对象存储到年老代区。

此处进行的垃圾回收是年老代垃圾回收(Major GC)。当堆空间已满或者年老代区占满时,就会触发 Major GC。此时,通常会由一个“停止一切(Stop-the-World)”事件或线程执行垃圾回收。此外,还有另一种称为全垃圾回收(Full GC)的垃圾回收机制,会涉及诸如永久内存区域。

与整体堆内存相关的另两个重要且有趣的标记是 -XX:SurvivorRatio-XX:NewRatio,前者指定伊甸园区相对幸存者区的比率,后者指定年老代区相对新生代区的比率。

[Permanent Generation] 永久代区(Permgen space)

永久代区(Permgen)用于存储以下信息:常量池 (内存池),字段与方法数据及代码。

垃圾回收算法 串行 GC(Serial GC) (-XX:UseSerialGC): 针对年轻代与年老代的垃圾回收

该算法使用简单的“标记-清扫-压缩(mark-sweep-compact)”循环清理年轻代与年老代,适合内存占用较低、CPU 使用量较少的客户端系统。

并行 GC(Parallel GC) (-XX:UseParallelGC): 针对年轻代与年老代的垃圾回收

该算法使用 N 个线程(N 的值可以通过 -XX:ParallelGCThreads=N 设定,N 同时代表垃圾回收占用的 CPU 内核数)。其中,年轻代垃圾回收会使用 N 个线程,而年老代只用一个线程。

并行 Old GC (-XX:UseParallelOldGC): 针对年轻代与年老代的垃圾回收

该算法对年轻代与年老代均使用 N 个线程,其他方面与并行 GC 完全一致。

并发 Mark and Sweep GC (-XX:ConcMarkSweepGC): 针对年老代的垃圾回收

顾名思义,CMS GC 会最小化垃圾回收所需的停顿时间。该算法最适于创建高响应度的应用,且只作用于年老代。它会创建多条垃圾回收的线程,与应用线程同时工作。垃圾回收的线程数量可以使用 -XX:ParallelCMSThreads=n 标记指定。

G1 GC (-XX:UseG1GC): 针对年轻代与年老代的垃圾回收 (将堆内存等分为大小相同的区块)

这是一种并行、并发、不断压缩的低停顿垃圾回收器。G1 是在 Java 7 中引入以取代 CMS GC 的,它会先将堆内存分为多个大小相等的区块,继而执行垃圾回收。通常,从活动数据最少的区块开始,因此以垃圾为先。

最常见的内存溢出问题

所有 Java 程序员都应该知道的最常见的内存溢出问题:

Exception in thread "main": java.lang.OutOfMemoryError: Java heap space( Java 堆内存)。这并不一定意味着内存泄露,也可能是分配的堆内存空间太小。此外,在运行时间较长的应用中,也可能是因为一个无意识的引用被指向堆对象(内存泄露)。即便是应用本身调用的 APIs,也可能保存着指向无依据对象的引用。而且,在大量使用终结器的应用中,对象们有时可能正排在终结队列中。当这样的应用创建高优先级的线程时,会导致越来越多的对象排在终结队列中,最终导致内存溢出。

Exception in thread "main": java.lang.OutOfMemoryError: PermGen space(永久存储空间)。如果加载了很多类与方法,或者创建了很多字符串常量,特别是使用 intern() 方法进行创建(从 JDK 7 开始,interned 字符串就不再存储在 PermGen 中),这类错误就会出现。当出现这类错误时,打印的堆栈跟踪附近可能会出现如下文本:ClassLoader.defineClass。

Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit (请求的数组大小超出 VM 限制)。当请求的数组大小超过可用的堆空间时,这类报错就会出现。这类错误通常归咎于编程错误,在运行时请求了极大的数组大小。

Exception in thread "main": java.lang.OutOfMemoryError:
request bytes for ,交换空间溢出?这是内存泄露最常见的根源。通常,当操作系统没有足够的交换空间,或另一个进程占用了系统中的所有可用内存,就会导致内存泄露。简而言之,由于空间用尽,堆内存无法提供所请求的空间大小。该信息中的 "s" 代表失败的请求所需的内存大小(以字节为单位),而 "r" 代表内存请求的原因。在大多数情况下,此处的 "r" 是报告分配失败的源模块,有时也会是具体的原因。

Exception in thread "main": java.lang.OutOfMemoryError: (Native method)(<原因><堆栈跟踪><本地方法>)。该报错意味着一个本地方法遇到内存分配失败。问题的根源在于 Java Native Interface(Java 本地接口) 中存在的错误,而非 JVM 中运行的代码错误。若是本地代码不检查内存分配错误,应由会直接崩溃,而不会出现内存溢出。

内存泄露的定义

你可以将内存泄露看做一种疾病,而内存溢出错误为一种征兆。但不是所有的内存溢出错误都意味着内存泄露,不是所有的内存泄露都以内存溢出为征兆。

维基百科的定义:在计算机科学中,内存泄露是一种以如下方式发生的资源泄露——计算机程序错误地分配内存,导致不再需要的内存得不到释放。在面向对象的编程语言中,一个对象若存储在内存中,却无法由运行的代码获取,即为内存泄露。

Java 中常用的内存泄露定义

当不再需要的对象引用仍旧多余地予以保存,即为内存泄露。

在 Java 中,内存泄露是指对象已不再使用,但垃圾回收未能将他们视做不使用对象予以回收。

当程序不再使用某个对象,但在一些无法触及的位置该对象仍旧被引用,即为内存泄露。也因此,垃圾回收器无法删除它。该对象占用的内存空间无法释放,程序所需的总内存就会增加。久而久之,应用的性能就会下降,JVM 可能会耗尽所有内存。

在某种程度上,当无法再给年老区分配内存时,内存泄露就会发生。

内存泄露最常见的一些情况:

ThreadLocal 变量

循环与复杂的双向引用

JNI 内存泄露

可变的静态域(最为常见)

我建议结合使用 Visual VM 与 JDK,对内存泄露问题进行调试。

常见的内存泄露调试方法

NetBeans 分析器

使用 jhat Utility

创建 Heap Dump

获取当下运行进程的堆内存柱状图

获取内存溢出错误的堆内存柱状图

监控在等待终结的对象数量

第三方内存调试器

调试内存泄露问题的常用策略或步骤:

确认征兆

启用详细的垃圾回收机制(verbose GC)

启用性能分析

分析堆栈跟踪

原文地址:https://dzone.com/articles/java-memory-architecture-model-garbage-collection

OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问 OneAPM 官方博客。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64728.html

相关文章

  • 第6章:可维护性软件构建方法 6.3可维护性构建技术

    摘要:遵循特定规则,利用操作符,终止节点和其他非终止节点,构造新的字符串非终结符是表示字符串的树的内部节点。语法中的生产具有这种形式非终结符终结,非终结符和运算符的表达式语法的非终结点之一被指定为根。 大纲 基于状态的构建 基于自动机的编程 设计模式:Memento提供了将对象恢复到之前状态的功能(撤消)。 设计模式:状态允许对象在其内部状态改变时改变其行为。 表驱动结构* 基于语法的构...

    young.li 评论0 收藏0
  • Javag工程师成神之路(2019正式版)

    摘要:结构型模式适配器模式桥接模式装饰模式组合模式外观模式享元模式代理模式。行为型模式模版方法模式命令模式迭代器模式观察者模式中介者模式备忘录模式解释器模式模式状态模式策略模式职责链模式责任链模式访问者模式。 主要版本 更新时间 备注 v1.0 2015-08-01 首次发布 v1.1 2018-03-12 增加新技术知识、完善知识体系 v2.0 2019-02-19 结构...

    Olivia 评论0 收藏0
  • Java学习路线总结,搬砖工逆袭Java架构师(全网最强)

    摘要:哪吒社区技能树打卡打卡贴函数式接口简介领域优质创作者哪吒公众号作者架构师奋斗者扫描主页左侧二维码,加入群聊,一起学习一起进步欢迎点赞收藏留言前情提要无意间听到领导们的谈话,现在公司的现状是码农太多,但能独立带队的人太少,简而言之,不缺干 ? 哪吒社区Java技能树打卡 【打卡贴 day2...

    Scorpion 评论0 收藏0
  • (CZ深入浅出Java基础)设计模式笔记

    摘要:在设计模式中,所有的设计模式都遵循这一原则。其实就是说在应用程序中,所有的类如果使用或依赖于其他的类,则应该依赖这些其他类的抽象类,而不是这些其他类的具体类。使用设计模式是为了可重用代码让代码更容易被他人理解保证代码可靠性。 这是刘意老师的JAVA基础教程的笔记讲的贼好,附上传送门 传智风清扬-超全面的Java基础 一、面向对象思想设计原则 1.单一职责原则 其实就是开发人员经常说的高...

    李昌杰 评论0 收藏0
  • 一文理清21种设计模式:用实例分析和对比

    摘要:设计模式无论是对于最底层的的编码实现还是较高层的架构设计都有着重要的指导作用。所谓光说不练假把式,今天我就把项目中常见的应用场景涉及到的主要设计模式及其相关设计模式总结一下,用实例分析和对比的方式在一片文章中就把最常见的种设计模式梳理清楚。 设计模式无论是对于最底层的的编码实现还是较高层的架构设计都有着重要的指导作用。所谓光说不练假把式,今天我就把项目中常见的应用场景涉及到的主要设计模...

    PrototypeZ 评论0 收藏0

发表评论

0条评论

wow_worktile

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<