JVM内存区域与OOM

说明:本篇博客属于读书笔记,大量参考《深入理解Java虚拟机》这本书

JVM的内存

程序计数器

  • 程序计数器是线程私有的,每一个线程都有自己的一个程序计数器,并且互不干扰,程序计数器相当于当前代码所执行指令的指针,控制了当前线程的执行流程,当Java程序在执行Java方法的时候,程序计数器记录的是当前执行代码的指令地址,当Java程序正在执行Native方法,程序计数器则为空(Undefined),程序计数器是不会抛出OOM异常的

Java虚拟机栈

  • Java虚拟机栈也是线程私有的,它的生命周期与线程的生命周期相同,Java程序在执行一个方法的时候都会创建一个栈帧(Stack Frame)用于存储局部变量,方法出口等信息,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中的入栈到出栈的过程,虚拟机栈中存储这Java的基本数据类型以及对象的引用,在Java虚拟机栈中会抛出StackOverflowError异常和OOM异常,下面来看一个demo:
public class DemoMain {

    public static void main(String[] args) {
        System.out.println("test");
        DemoMain.testMethod();
        System.out.println("end");
    }

    public static void testMethod() {
        testMethod();
    }
}

如上的应用程序运行之后,在我的机器上会抛出StackOverflowError:

test
Exception in thread "main" java.lang.StackOverflowError
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)
    at com.lhd.jvmdemo1.DemoMain.testMethod(DemoMain.java:12)

当虚拟机在执行方法testMethod的时候,这时候就会在Java虚拟机栈上创建一个栈帧,然后入栈,然而在testMethod方法内又不断的递归调用testMethod方法,导致Java虚拟机栈不断的嵌套执行testMethod方法,不断的创建testMethod的栈帧,然后入栈,而testMethod并没有执行完成,所以testMethod对应的栈帧不会出栈,当Java虚拟机栈中的栈深度超过了虚拟机允许的深度,这时候就抛出了StackOverflowError异常了,如果虚拟机可以动态拓展,在新的栈帧入栈的时候再去申请内存,要是申请不到足够的内存,此时就会抛出OOM异常了

本地方法栈

  • 本地方法栈是线程私有的,存储Native方法的信息,这个内存区域也会抛出StackOverflowError和OOM异常

Java堆

  • Java堆是线程共享的,这是虚拟机中内存最大的一块,它唯一目的就是用来存放对象实例的,就是:
Object obj = new Object();

obj是对象的引用,存储在Java虚拟机栈中,而new出来的Object对象实例就存储在Java堆中,obj引用指向Java堆中实例的地址,Java堆是垃圾回收管理的主要区域,Java堆的内存空间不需要物理上的连续,只需要逻辑上的连续即可,Java堆也会抛出StackOverflowError 和OOM异常

方法区

  • 方法区是线程共享的内存区域,它用来存储已经被虚拟机加载的类信息(类名,类字段,方法名等),常量(final修饰),静态变量(static修饰)等,此区域也会抛出OOM异常

运行时常量池

  • 运行时常量池是方法区的一部分,常量池用于存放编译期生成的各种字面量(文本字符串、声明为final的常量值等)和符号引用(类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符),运行时常量池具有动态性,也就是说不一定是预置到class中的常量才能进入运行时常量池,在运行期间也可能将新的常量放入池中,例如String类的intern(),该内存区域也会抛出OOM异常

字符串常量池

  • 字符串常量池也是方法区的一部分,用来存放字符串常量,举个例子:
public class DemoMain {

    public static void main(String[] args) {
        System.out.println("test");
        String s1 = "s1";
        String s2 = "s1";
        String s3 = new String("s1");
        System.out.println(s1 == s2);
        System.out.println("end");
    }
}

以上运行的结果是:

test
true
end

也就是说是s1指向的地址和s2指向的地址是一样的,为什么?因为“s1”这个字符串常量被存储到了字符串常量池中了,虚拟机发现了s1对象指向“s1”,s2对象也指向“s1”,因此不会再次创建一个“s1”,而是将s1和s2对象都指向存储于常量址的“s1”,这里就做到了常量池的对象共享,节省内存

对象创建的过程

  • 在JVM中当收到一个new指令的时候首先会去常量池中检查是否存在这个类的符号引用,并检查这个类是否已经被加载,解析和初始化过,如果没有,那就先执行类加载过程
  • 类加载检查过后,接下来JVM就会为新生的对象分配内存,对象所需要的内存空间大小在类加载的时候就能够确定,内存分配其实就是在Java堆中开辟一块确定大小的内存出来,Java堆的内存分配有两种,第一种是“指针碰撞”,当Java堆中的内存是规整的,即用过的内存都在一边,空闲的内存在另一边,那么此时的内存分配就是把指针指向空闲内存空间挪动一段于对象大小相同的距离;第二种是“空闲列表”,当Java堆中使用内存和空闲内存相互交错的时候,此时JVM必须维护一个列表,记录哪些内存是可用的,在分配内存的时候从列表中寻找一块足够大的空间划分给对象,并更新列表上的记录,具体选择哪一种内存分配的方式取决于Java堆内存是否规整,而Java堆内存是否规整取决于GC回收器是否又压缩整理功能
  • 对象内存的分配过程还要注意多线程问题,假如在给一个对象分配内存的时候,指针还没来得及修改,此时又要操作指针给另一个对象也分配内存,解决这个问题又两种方案,一种是堆内存分配动作进行同步操作,另一种是预先给每一个线程在Java堆中分配一块小内存,成为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),那么只有在分配TLAB的时候才需要同步处理,对象内存分配完了之后就要对对象中的值进行初始化为零值,最后再执行<init>方法,也就是构造方法,来初始化对象中的值,这样一个对象才算完全创建成功
  • 所以总结下对象创建的过程大致分为以下几个阶段


    image.png

对象的访问定位

  • Java虚拟机栈中存储的是对象的引用,Java堆中存储的才是对象的实际数据,对象的访问定位通常有两种,一种是句柄访问,一种是指针访问,句柄访问就是在Java堆中有一个句柄池,句柄池中才是存储了对象地址,而JVM栈中的对象引用存的是对象的句柄地址,也就是说reference指向句柄,句柄指向对象,这么做的好处就是对象要是移动,JVM栈中的reference不用做修改,只要修改句柄就行了;指针访问就是JVM栈中的reference直接存的就是对象地址,reference直接指向JVM堆中的对象,这么做的好处就是访问对象速度快,要是对象被频繁的访问,那指针访问的方式将有明显的效率提升

内存溢出

Java堆内存溢出

public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            list.add(new Object());
        }
    }

不断的分配对象,并添加到list当中,这样对象就不会被回收,程序跑一会儿就报Java堆的OOM异常:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

什么情况可能会导致Java堆的内存泄漏?很明显,内存泄漏,一些对象创建后,一直被持有导致GCRoot一直存在,所以不会被回收

虚拟机栈和本地方法栈溢出

  • 虚拟机栈和本地方法栈都会抛出StackOverflowError和OutOfMemoryError,对于StackOverflowError异常会有两种情况,一种是虚拟机栈的深度大于虚拟机规定的最大深度,另一种是在申请栈帧内存的时候没有足够的内存,这时候也会抛出这个异常
public class DemoMain {

    int i = 0;

    public static void main(String[] args) {
        DemoMain demoMain = new DemoMain();
        try {
            demoMain.test();
        } catch (Throwable e) {
            System.err.println("stack:" + demoMain.i);
            e.printStackTrace();
        }
    }

    public void test() {
        i++;
        test();
    }

}

运行结果:

stack:34879
java.lang.StackOverflowError
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
    at com.lhd.jvmdemo1.DemoMain.test(DemoMain.java:22)
  • 线程创建造成的内存溢出
public class DemoMain {

    public static void main(String[] args) {
        DemoMain demoMain = new DemoMain();
        demoMain.createThread();
    }

    public void createThread() {
        while (true) {
            new Thread(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }

}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:714)
    at com.lhd.jvmdemo1.DemoMain.createThread(DemoMain.java:26)
    at com.lhd.jvmdemo1.DemoMain.main(DemoMain.java:12)

这个是本地方法区抛出的OOM异常

方法区和常量池溢出

  • 方法区抛出的OOM本机没有模拟出来,不过方法区的OOM异常是:
java.lang.OutOfMemoryError:PermGen space

在android开发中,如果一个apk的类非常多,安装这个apk的时候就可能出现方法区的内存不够用导致方法区内存溢出

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

推荐阅读更多精彩内容