【JVM】Java内存区域与内存溢出异常

最近学习《深入理解Java虚拟机》,打算把学习所得和体会整理出来,方便以后反复再看。

本文主要介绍了JVM内存的组成、堆中对象创建和布局、常见的内存溢出异常,这三部分内容。

一 概述

对于Java程序员来说,不需要自己操心垃圾对象的回收和内存的管理,JVM会帮我们完成内存的管理,这样不容易出现内存泄漏和内存溢出问题,也能节省很多开发时间。但是如果想成为一个高级的Java开发人员写出高性能的代码,肯定是需要懂得JVM的工作原理。

JVM由四大部分组成:Class Loader、Runtime Data Area、Execution Engine、Native Interface

这里写图片描述

  • ClassLoader
    类加载器负责加载class文件,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

  • Runtime Data Area
    运行时数据区是JVM的重点,JVM对内存的管理都是在这个区域完成的。Java虚拟机在执行 Java程序的过程中会把他管理的内存划分若干个不同区域,其中主要由5部分组成,其中有2部分是线程共享的;另外3部分是线程独享的,也就是每个线程都有自己独立的这部分区域
    线程共享:Heap + MethodArea
    线程独享:PC Register + Stack + NativeMethodArea

  • Execution Engine
    执行引擎,Class被加载后,会把指令和数据信息放入内存中,Execution Engine负责吧这些指令解释给操作系统

  • Native Interface
    Native Interface负责调用本地接口,他的作用是调用不同语言和操作系统的接口给JAVA用

二 运行时数据区域

2.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行的字节码的信号指示器。计数器内记录的是正在执行的虚拟机字节码指令的地址

2.2 虚拟机栈

虚拟机栈(JVM Stack)平时一般都简称为栈,他描述的是Java方法执行的内存模型:每个方法执行时都会创建一个栈帧(Stack Frame),每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧结构:

  • 局部变量表
    • 基本数据类型变量
    • 对象引用变量
    • returnAddress类型变量
  • 操作数栈
  • 动态链接
  • 方法出口

如下图所示,展示两个栈帧:
这里写图片描述

2.3 本地方法栈

本地方法栈(Native Method Stack)的结构和虚拟机栈相同,两者的不同在于本地方法栈为本地方法服务,虚拟机栈为Java方法服务。甚至有一些虚拟机(比如:Sun HotSpot)直接把本地方法栈和虚拟机栈合二为一

2.4 堆

Java堆(Heap)是Java虚拟机管理的内存中最大的一块,Java堆唯一的目的就是存放对象的实例,几乎所有对象实例都是在这里分配内存。

JVM的垃圾回收主要也是针对这个区域,从内存回收的角度来看,堆可以分成新生代和老年代两部分,下图中的Total Heap Size就是Java堆的全部区域,其中NewSize就是新生代。PermSize对应的方法区中的永久代

这里写图片描述

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样

2.5 方法区

方法区(Method Area)用于储存已经被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码。

在方法区有个重要的区域叫做运行时常量池(Runtime Constant Pool),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,同样在程序运行时产生的常量也会存储在这里

三 堆中对象探秘

在学习Java虚拟机的运行时数据区后,我们大致明白了虚拟机内存的基本情况,下面就重点看一下堆中对象创建、布局和访问的具体过程。这部分我们只讨论普通对象,不包括数组和Class对象

3.1 对象的创建

一般来说对象创建的起点是new指令,下面就仔细研究一下JVM在遇到new指令后的整个运行过程

(1)加载类

首先JVM会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有就执行类加载过程

(2)分配内存

类加载检查通过后,虚拟机将会为新生对象分配内存,对象所需内存大小在类加载的时候就已经完全确定了。

内存分配完之后,虚拟机会将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

(3)设置对象头

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都是存在对象头当中的。对象头具体内容会在3.2中介绍

(4)init方法

上面的工作完成后,从虚拟机的角度来看,一个新的对象已经创建好了,但从Java程序的角度来看,对象创建才刚刚开始,因为方法(构造器方法)还没有执行。相当于new方法已经执行完了,下面该执行构造器方法了

3.2 对象的内存布局

这一节我们主要来看看对象在Java堆中被存成了什么样子

每个对象在Java堆中主要分为3个区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

(1)对象头

对象头:主要分为Mark Word和Class Metadata Address两部分

- Mark Word:用于存储对象自身的运行时数据
    - 哈希码
    - GC分代年龄
    - 锁状态标志
    - 线程持有的锁
    - 偏向线程ID

-Class Metadata Address:类型指针
    虚拟机通过这个指针来确定这个对象是哪个类的实例

如果是数据对象,那对象头中还需要一块记录数据长度的空间

(2)实例数据

示例数据部分就是对象真正存储的有效信息,也是程序代码中定义的各种类型的字段内容。无论是从父类继承下来的,还是子类中定义的,都需要在这一步空间中存下来

(3)对齐填充

对齐填充很容易理解,就是用来起到对齐作用的占位符。应为对象的起始地址必须是8字节的倍数,所以需要在示例数据后面填充一些占位符,保证下一个对象实例的起始地址是8字节倍数

3.3 对象的访问定位

对象已经在Java堆中创建好了,那怎么访问到对象呢,就是通过JVM Stack中的reference数据(对象引用)来访问

现在主流的访问方式有两种:句柄 和 直接指针

(1)句柄

在Java堆中会分配一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而且句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图

这里写图片描述

(2)直接指针

直接指针访问方式,reference中储存的直接就是对象地址

这里写图片描述

这两种方式各有优势,句柄:reference中储存的是稳定的句柄地址,对象被移动(GC时对象经常会被移动)时只改变局病种的实例数据指针,而reference本身不需要修改;直接指针:速度快

四 内存溢出异常

在写Java程序时,经常会遇到OutOfMemoryError 和 StackOverFlowError两种异常,分别代表着内存溢出异常和栈溢出异常

在学习JVM内存后,我们写一些dome来测试下什么时候会产生这两种异常。

为了达到效果需要对Java虚拟机的参数进行设置,改变Java栈和堆的大小,进而方便溢出异常的发生,我用的是IntelliJ IDEA,通过下面截图中的VM options进行设置
这里写图片描述

4.1 Java堆溢出

将VM options设置为-Xms20m -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

import java.util.ArrayList;
import java.util.List;

/**
 * -Xms20m -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * Created by Zcl on 2017/5/10.
 */
public class HeapOOM {
    static class OOMObject{

    }

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

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

这个Demo中会不停的创建OOMObject对象,然后加入到list中,也就是不停在Java堆中申请空间存放OOMObject对象,直到堆中空间被消耗殆尽,报出异常:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7432.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
4.2 虚拟机栈溢出

由于在HotSpot虚拟机中不区分虚拟机栈和本地方法栈,所以我们一起测试这部分的溢出。JVM Stack即可以报出StackOverFlowError,也可以报出OutOfMemoryError

(1)StackOverFlowError

将VM options设置为-Xss128k

/**
 * -Xss128k
 * Created by Zcl on 2017/5/10.
 */
public class JVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

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

这个Demo中新建了一个sof对象,然后调用stackLeak( )方法,这个方法会不停的再调用自己。stackLeak( )方法每被调用一次就会在Java栈中生成一个新的栈帧,直到Java栈空间被消耗完,报出异常:

Exception in thread "main" java.lang.StackOverflowError
    at JVMStackSOF.stackLeak(JVMStackSOF.java:12)
    at JVMStackSOF.stackLeak(JVMStackSOF.java:13)
    at JVMStackSOF.stackLeak(JVMStackSOF.java:13)
    at JVMStackSOF.stackLeak(JVMStackSOF.java:13)
    at JVMStackSOF.stackLeak(JVMStackSOF.java:13)
    ...后面还有很多就省略了,这里展示了递归调用stackLeak产生的溢出

(2)OutOfMemoryError

将VM options设置为-Xss2M

/**
 * -Xss2M
 * Created by Zcl on 2017/5/10.
 */
public class JVMStackOOM {
    private void dontStop() {
        while (true) {
        }
    }

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

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

在运行这个Demo之前,记得要先保存当前的工作,由于Windows平台虚拟机中,Java的线程是映射到操作系统的内核线程上的,因此运行时可能会导致操作系统死机,我就死了一次 = =。

这个Demo中创建了oom对象,在stackLeakByThread( )方法中不停的创建新的线程,每个线程都执行dontstop( )方法。由于每个线程都有自己的JVM Stack,每个stack都需要占用内存,直到将内存空间消耗殆尽,报出异常:

Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread
4.3 方法区溢出

将VM options设置为-XX:PermSize=10M -XX:MaxPermSize=10M

/**
 * -XX:PermSize=10M -XX:MaxPermSize=10M
 * Created by Zcl on 2017/5/10.
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用list是为了保持常量的引用,防止FULL GC回收常量
        List<String> list = new ArrayList<String>();
        int i=0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

由于常量池是方法区的一部分,我们就通过不停的创建常量来测试。常量池空间消耗完时,报出异常:

Exception in thread "main" java.lang.OutOfMemoryError:PermGen space
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值