JVM(一):Java内存区域与内存溢出异常

1. JVM运行时数据区域

  • 程序计数器(各线程私有)
    一块较小的内存空间,可看成是当前线程执行字节码的行号指示器,用于记录当前线程字节码执行到哪儿了,这样线程切换的时候才能够恢复到正确的执行位置。

  • Java虚拟机栈(各线程私有)
    其生命周期和线程相同。Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(局部变量表、操作数栈、动态链接、方法出口等信息),一个方法的调用直至完成对应着一个栈帧在虚拟机栈中的入栈到出栈。
    局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(指针/句柄)、returnAddress类型。

  • 本地方法栈(各线程私有)
    功能类似虚拟机栈,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用的Native方法服务。

  • Java堆(各线程共享)
    Java堆在虚拟机启动时创建,是JVM管理的内存中最大的一块。用于存放所有的对象实例和数组。Java堆可以是物理上不连续的内存空间(逻辑上连续即可)。

  • 方法区(各线程共享)
    用于存放已经被虚拟机加载的类信息和运行时常量池(动态常量池)内的数据。
    类信息包含类的版本、字段、方法、接口等描述信息以及Class文件的常量池(静态常量池),Class文件的常量池用于存放编译期生成的各种字面量和符号引用。
    运行时常量池在类加载后会将Class文件常量池中的内容放入运行时常量池中,同时运行期间产生的常量也可以放入到方法区的运行时常量池中(例如String的intern()方法)。

2. 直接内存

直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。NIO中引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后用Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这块内存是直接分配在物理内存中的(Java堆外),不受Java堆大小的限制,但是受物理内存大小限制(物理限制和处理器寻址空间限制)。

3. 对象创建

HotSpot虚拟机中,对象的创建过程大致如下:

  • 虚拟机遇到new指令时,先进行类加载检查
  • 在Java堆中为对象分配内存空间
  • 内存分配完成后,虚拟机将该内存空间都初始化为零值(对象头除外)
  • 设置对象头信息
  • 执行init方法,初始化对象

4. 对象内存布局

HotSpot虚拟机中,对象的内存布局可分为三个区域:对象头、实例数据和对齐填充。

  • 对象头包含:a)用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等);b)类型指针(对象指向它的类元数据的指针),虚拟机通过该指针确定对象是哪个类的实例。
  • 实例数据:对象真正存储的有效信息(代码中定义的各种类型的字段内容),包括父类继承的和子类中定义的。
  • 对齐填充:HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对齐填充用于补全实例数据。

5. 对象访问

Java程序通过上的reference数据来操作上的具体对象。

  • 使用句柄访问


    在Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象的实例地址和类型地址。
    好处:reference中存储的是句柄地址是固定的,即使对象被移动了(例如GC)也不需要修改reference,只需修改句柄中的对象实例地址。

  • 使用直接地址访问


    reference中存储的是Java堆中对象实例地址,对象的布局中需要包含对象类型的相关信息。
    好处:节省了一次指针定位的时间开销,速度更快。

6. OutOfMemoryError异常

  • Java堆溢出
/**
 * JVM启动参数:
 * -verbose:gc 打印GC信息
 * -Xms20m 最小堆内存
 * -Xmx20m 最大堆内存,最大堆内存=最小堆内存时不会自动扩展堆
 * -XX:+HeapDumpOnOutOfMemoryError 当内存溢出时Dump出当前内存中堆的转储快照
 * -XX:HeapDumpPath=D:\dump 堆转储快照保存地址
 */
public class HeapOOM {
    static class OOMObject {

    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}

// 异常信息
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\dump\java_pid105876.hprof ...
Heap dump file created [26566702 bytes in 0.273 secs]

解决方法:

  1. 利用内存映像分析工具(例如Eclipse Memory Analyzer)分析Dump出来的堆转储快照,确认内存中的对象是否是必要的(即确定是内存泄露还是内存溢出)
  2. 如果是内存泄露,进一步通过工具查看泄露对象到GC Roots的引用链,从而定位泄露代码的位置
  3. 如果不存在内存泄露,则检查JVM的堆参数设置,检查代码中是否存在某些对象生命周期过长、持有状态时间过长等情况。
  • 虚拟机栈和本地方法栈溢出
    HotSpot虚拟机中不区分虚拟机栈和本地方法栈。

1)如果某个线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError

/**
 * 虚拟机栈StackOverflow
 * -Xss128k 栈容量大小
 */
public class JVMStackSOF {
    public void stackLeak() {
        stackLeak();
    }

    public static void main(String[] args) {
        JVMStackSOF sof = new JVMStackSOF();

        sof.stackLeak();
    }
}

// 异常信息
Exception in thread "main" java.lang.StackOverflowError
    at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
    at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
    at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)

出现StackOverflowError的原因:每个方法执行时都会往栈中压入一栈帧(各栈帧的大小也不一样),当不停的压入栈帧达到虚拟机允许的最大深度时就会抛出StackOverflowError。大多情况下,栈深达到1000-2000没问题,足够正常的方法调用。

2)如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError:


/**
 * 虚拟机栈OutOfMemoryError
 * -Xss2m 每个线程的栈内存
 */
public class JVMStackOOM {
    private void keepRunning() {
        while (true) {

        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    keepRunning();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JVMStackOOM oom = new JVMStackOOM();
        oom.stackLeakByThread();
    }
}

// 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

产生OutOfMemoryError的原因:操作系统给每个进程分配的内存是有限的(例如32位Windows限制为2G),那么:
所有线程的虚拟机栈+本地方法栈+程序计数器内存(可忽略)=进程内存限制-最大堆内存(Xmx)-最大方法区容量(MaxPermSize)。
当所有线程的虚拟机栈+本地方法栈内存超出限制后就会出现OutOfMemoryError。

  • 方法区溢出
    方法区用于存放类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。如果在运行时产生大量的类填满方法区直至溢出,就会产生OutOfMemoryError。

  • 本机直接内存溢出
    直接内存溢出,一个明显的特征是Heap Dump文件中没有明显的异常。如果直接或间接使用了NIO,而Dump文件很小,可以考虑是直接内存溢出。


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

推荐阅读更多精彩内容