Java 内存管理

弄清JVM(Java Virtual Machine)的内存管理模型对了解java GC工作原理是很有必要的。最近正好看到一篇文档写的不错,介绍了Java内存管理的处理方式,包括JVM内存分配各个区域的含义,以及如何监测协调GC工作。翻译后在此记录。
原文传送门

内存分配区域

图片来源网络

如上图所示,JVM内存被分成了几个区域,粗略的看JVM的堆内存分成了两块区域----新生域,年老域。

新生域内存管理

刚创建的新对象会被分配到新生域。当新生域使用殆尽,GC就开始工作了。这种状况下促发的GC被称作次级GC(Minor GC)。而新生域又被分成三个部分

  • Eden Memory.就像是上帝(JVM)划分出来的伊甸园区域,可以想象刚生成的对象在这里快快乐乐的生活着。
  • 两个Survivor Memory(幸存域),也就是上图中的S0和S1区域。

新生域的主要特性:

  • 大多数新创建的对象都被分配到了Eden Memory
  • 当Eden Memory被新分配的对象填充满时,次级GC开始工作,幸存下来的对象被移到其中一个Survivor Memory(幸存域)。
  • 次级GC会同时检查Survivor Memory(幸存域),将其中一个Survivor Memory(幸存域)中的对象移到另一个Survivor Memory(幸存域),保证一个Survivor Memory(幸存域)为空。
  • 当一个对象经过次级GC多次扫描后,依然幸存,那么它就会被移到年老域(Old generation).JVM会设置一个阀值,当一个对象被次级GC扫描过的次数达到这个阀值,而这个对象依然幸存,则该对象就被移到年老域(Old generation)。

年老域内存管理

年老域的内存存放的是一些长生命周期和被次级GC多次扫描依然幸存下来的对象。GC在年老域内存吃紧的情况下开始工作,这种GC称为主级GC(Major GC),通常主级GC消耗的时间更长。

世界暂停事件(Stop the World Event)

次级GC和主级GC开始工作时,应用线程就会停下来,因此整个Java程序也就会处于停止状态,这种情况就是"Stop the World Event"。

新生域存放的是短生命周期的对象,因而次级GC的这个过程会很快结束,在程序看来几乎不受影响。

主级GC需要扫描所有的幸存对象,因而它花费的时间会较长。一旦主级GC工作,应用程序就会暂停下来,直观感受就是程序不够流畅,无法快速响应业务事件处理,主级GC运行次数过多,甚至会引发程序的超时错误。要想应用程序跑的流畅,就得少去促发主级GC工作,要少促发主级GC工作,就得尽可能的保证年老域的内存空间不被填充满。所以这提醒我们一定要珍惜内存空间,尤其在Android移动设备上。

不同的GC策略会影响GC工作消耗的时长。为了让应用程序运行流畅度最优,就有必要根据应用程序的运行场景运用不同的GC策略。

持久域(Permanent Generation)

JVM通过元数据(metadata)来记录应用程序的类和方法,这些元数据就被放在持久域就,另外Java公共的库文件和方法也被放在这里,持久域不属于堆内存。在内存满时该块内存中的对象也可能被回收。Java8中已经删除该区域。

方法域(Method Area)

方法域属于持久域中的一块,它被用来存放定义方法的代码和类文件结构。

内存池(Memory Pool)

内存池由JVM创建出来存放不可变对象,比如String,它可能在堆内存分配也可能在持久域分配,这取决于JVM的内存管理机制。

运行时常量池( Runtime Constant Pool)

运行时常量池用于存放编译期生成的各种字面量和符号引用以及静态方法,这部分内容将在类加载后存放,它属于方法域的一部分。

栈内存(Stack Memory)

栈内存被执行线程所用。它用来存放方法内的变量,这些变量通常是一些指向堆内存对象的引用,它们生命周期短。

堆内存调节器(Heap Memory Switches)

VM SWITCH VM SWITCH DESCRIPTION
-Xms 初始堆内存大小
-Xmx 最大堆内存
-Xmn 新生域大小,余下空间就是年老域
-XX:PermGen 设置持久域(Permanent Generation)大小
-XX:MaxPermGen 最大持久域
-XX:SurvivorRatio 伊甸园(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被两个Survivor spaces均分。默认值为8。
-XX:NewRatio 年老域和新生域比值,默认为2,也就是说年老域大小是新生域大小的2倍

Java提供了许多设置内存大小以及各个内存域占比大小的调节器。常用的调节器如下:

VM SWITCH VM SWITCH DESCRIPTION
-Xms 初始堆内存大小
-Xmx 最大堆内存
-Xmn 新生域大小,余下空间就是年老域
-XX:PermGen 设置持久域(Permanent Generation)大小
-XX:MaxPermGen 最大持久域
-XX:SurvivorRatio 伊甸园(Eden space)和幸存者(Survivor Space)比值, 比如新生域共分配了10M,-XX:SurvivorRatio=2,那么Eden Space就占5M,余下的5M被两个Survivor spaces均分。默认值为8。
-XX:NewRatio 年老域和新生域比值,默认为2,也就是说年老域大小是新生域大小的2倍

更详细的调节器配置信息请查看JVM Options Official Page

垃圾回收器

GC(Garbage Collection)是一个进程,它专注于标示和清理没有引用的对象,释放内存空间给新分配的对象腾地方住。其他某些语言这个过程都是程序猿自己实现的,而Java自动完成了这个过程。

GC作为一个后台进程,一直默默监察着应用程序的运行过程,寻找--->标记-->释放那些没有引用到的对象,为新对象腾空间。
典型的GC处理涉及到的过程如下:

  1. 标记:GC标记哪些对象在使用,哪些对象没有地方使用
  2. 正常删除:GC删除无用的对象所占有的空间,这些空间可以被其他存活的对象所使用
  3. 删除后汇集:为了提升性能,删除无用对象后,所有幸存对象被汇集在一起,如此新对象分配内存时效率会更高。

上述过程可能存在如下问题:

  • 大多数新创建的对象都是很快就变为无用的,而GC的运行频率又不宜过高,针对这种caseGC显得不够高效。
  • 长生命周期的对象也会被GC多次扫描标记。

正是为了规避上述问题,Java将堆内存划分成了不同的区域,也就是之前提到的新生域和年老域。

GC类型

垃圾回收策略共有5种类型,根据应用程序业务场景的不同,可以设置差异化的内存调节器。

  • Serial GC (-XX:+UseSerialGC): Serial GC 采用简单的标记-清楚-整理策略。比如副线GC和主线GC的模式,Serial GC 针对小型CPU上运行的简单应用很实用,特别是那些低内存设备上运行的小型应用。
  • Parallel GC (-XX:+UseParallelGC): Parallel GC 和Serial GC 很类似,但它可以有N个线程去处理新生域上的GC回收工作,这里的N值对应CPU的核数。可以用
    -XX:ParallelGCThreads=n 选项去设定。
    Parallel GC 也是采用单线程模式在年老域上进行GC工作的。
  • Parallel Old GC (-XX:+UseParallelOldGC): 和Parallel GC类似,但该种配置可以采用多线程GC来处理年老域上的GC工作。
  • Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): 因为CMS不会整理、压缩堆空间,带来的好处就是GC工作时暂停的时间很短暂,.它作用在年老域上,和工作线程并发执行。针对新生域上的内存处理,它采用的是Parallel GC的处理方式。该GC策略适用需要实时快速响应的应用程序上。可以通过
    -XX:ParallelCMSThreads=n
    来设置CMS的线程数。
  • G1 Garbage Collector (-XX:+UseG1GC): G1 Garbage Collector是Java 7新加入的,其目的是用了代替CMS回收策略. 它同样是并发执行,但会逐步压缩堆空间。
    Garbage First Collector不用于其他其中GC策略,它没有新生域年老域的概念。对它而言,堆内存会被分成多个相同大小的区域,GC运行,首先回收无用对象最多的小区域。Garbage-First Collector Oracle Documentation有更详细的说明。

GC监测

可以用命令行或者可视化工具监测应用程序背后的GC运行情况。这里我用自己写的一段简单测试代码,用命令行来进行GC监控.
先看测试代码,在一个10次的循环结构中,每次去申请一个10M大小的空间,这里忽略掉string申请的空间。

package com.azhengye.test;

import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            String string = "i="+i;
            byte[] bt = new byte[1024*1024*10];
            System.out.print(string);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们在命令行里编译运行.


这里写图片描述

可以看到程序运行正常。
接下来参考之前介绍的内存调节器,我们在运行时加上一些参数。


这里写图片描述

问题出来了。
前两次我们配置Xms为2m,运行时爆出错误提示了初始堆内存过小。在最后一次将Xms设置为10m该错误消失,但却爆出了我们常见的OOM问题。

这个简单的例子就说明Java提供的这种内存调节器作用,在某些应用程序运行时,我们要根据不同的场景,配置不同的运行参数。

jstat命令行监测内存

JDK提供了jstat命令用了监测JVM内存使用情况以及GC运行状态。
其使用规则如下:

jstat -gc <processid> <time>

进程id可以通过ps命令查看到

192:~/Documents/eclipse_workspace/DemoTestJava $ ps -eaf | grep java
501 2538 1956 0 11:41下午 ttys000 0:02.50 /usr/bin/java -Xmx20m -Xms10m -Xmn3m -XX:PermSize=2m -XX:MaxPermSize=4m -XX:+UseSerialGC -cp bin/ com.azhengye.test.Test
501 2540 1613 0 11:41下午 ttys001 0:00.00 grep java

有了进程号,在用jstat命令查看详细的内存使用情况,每隔1s打印一次内存情况。

jstat -gc 2538 1s

这里写图片描述

对照上图介绍下每一栏的含义。

  • S0C and S1C: 两个幸存域的大小,之前介绍过它们总是相等的。这里也可以印证。它们的大小均为256KB.
  • S0U and S1U: 两个幸存域已经使用的大小。
  • EC and EU: 伊甸园区域的内存大小和已经占用的内存大小,副线GC运行后EU大小就会减少。
  • OC and OU: 年老域内存大小和已经被使用的内存大小。
  • PC and PU: Perm Gen内存大小和已经被使用的内存大小。
  • YGC and YGCT: YGC 表示副线GC运行的次数,YGCT表示副线GC所消耗的时间。
  • FGC and FGCT: FGC 表示主线GC运行次数. 相应的FGCT打印的是主线GC消耗的时间.
  • GCT: 副线GC和主线GC消耗时间总和。

jvisualvm可视化监测

JDK同样提供了jvisualvm可视化的监测工具,首次打开需要先安装Visual GC 插件。下图是我截取的界面,功能比较多,这里不做过多介绍了。


这里写图片描述

内存和GC调整

这一步轻易不要走,程序运行的优化更多的注意了应该放在软件实现上。除非很明显的定位到程序运行受到了GC的拖累,或者确实需要调整内存的分配情况才能让程序运行起来最佳。
下面是几点调整建议:

  • 主线GC运行太过频繁,可以尝试增大年老域内存。
  • OOM问题,可以适当增加堆内存大小。
  • 采用不同的GC策略,监测程序运行情况,选取最合适的。

参考链接

了解CMS(Concurrent Mark-Sweep)垃圾回收器
Java 8新特性探究(9):跟OOM:Permgen说再见吧
Java8 Demos and Samples

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • JAVA内存管理 JVM结构 Class Loader类加载器的作用是根据给定的全限定名类名(如java.lang...
    听歌的老头阅读 347评论 0 1
  • 本文引用自:深入理解Java虚拟机的第2章内容 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙...
    溜溜小毛驴儿阅读 770评论 0 34
  • 2.9 JVM内存管理 2.9.1 运行时数据区域 JVM所管理的内存可以分为一下几个运行时数据区域: 其中方法区...
    jianhuih阅读 367评论 0 0
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供...
    简欲明心阅读 89,354评论 17 311
  • 小颜你丫 仰望星空,那一角落是最美。 仰望星空,那一角落是最亮。 仰望星空,那一角落是真挚。 仰望星空,哪一角落是...
    小颜儿你丫阅读 242评论 0 0