JVM运行时内存区域

1 篇文章 0 订阅
1 篇文章 0 订阅
下面讲解的内存区域划分,以及创建对象实例、对象内存布局、对象的访问,仅仅针对于HotSpot JVM来说。


第一部分,我们来讲解下JVM运行时内存区域是如何划分的,对象实例又是如何创建的,以及对象又是如何访问的?

一、运行时内存区域:

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域:

程序计数器:    
    是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行一个Native方法,这个计数器值则为空。此内存区域是唯一一个在虚拟机规范没有规定任何OutOfMemoryError情况的区域。

    Java虚拟机栈:
    每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。这块区域由于是线程私有的,随着每个线程创建时分配,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常,如果分配的线程数过多,可能导致内存分配不够的问题,此时会抛出OutOfMemoryError异常。

    本地方法栈:
    与虚拟机栈的作用类似,只不过是为线程执行native方法所用,在企业开发中,一般都是使用HotSpot JVM,它是将虚拟机栈和本地方法栈进行统一管理的。因此它也会抛出OutOfMemoryError异常或StackOverflowError异常。

    Java堆:
    Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
    Java堆是GC回收的主要区域,从内存回收的角度来看,由于现在收集器都采用分代收集算法,所以Java堆中还可以细分为新生代和老年代,具体来说就是Eden空间、From Survivor空间和To Survivor空间。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB),主要是为了更快地在创建对象实例时分配内存。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快的分配内存。当堆上没有内存可供分配实例时,将会抛出OutOfMemoryError异常。

    方法区:它主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。该区域的内存回收目标主要是针对常量池的回收和类型的卸载,该区域在没有可用内存分配时,也会抛出OutOfMemoryError异常。

二、JVM如何创建一个Java对象:
    (1)类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。如果有,那么接下来,虚拟机将为新生对象分配内存;
    (2)对象分配内存:对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    (3)内存空间初始化:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值时就直接使用,程序能访问到这些字段的数据类型所对应的零值;
    (4)对象设置:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中,根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。在上面的操作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的角度来看,对象创建才刚刚开始—<init>方法还没有执行,所有的实例字段尚为零值;
    (5)执行init()方法:执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完整的产生出来。

HotSpot JVM中对象的内存布局:
    对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
    第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据被称为“Mark Word”;
    第二部分是实例数据部分,该区域是对象用来真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容;
    第三部分是对象填充部分,由于HotSpot JVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。
    如下图所示:

    

三、对象的访问定位:
    创建对象的目的为了使用对象,我们的Java程序需要通过栈上的reference引用来操作堆上的具体对象,在HotSpot JVM中,它是“直接指针”的方式来访问堆上创建的实例。

第二部分,我们通过JVM运行时抛出的OutOfMemoryError异常信息来证明JVM运行时内存区域是如此划分的:
    注意:在堆内存发生溢出时,我们可以使用MAT 全称 Eclipse Memory Analysis Tools 是一个分析 Java堆数据的专业工具 来分析定位是什么原因造成的内存溢出。
一、Java堆内存溢出
    Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径就可以避免GC现线程清除这些对象(Java虚拟机采用了可达性分析算法来定位无用对象,并没有采用常见的引用计数法,因为引用计数法天生的循环引用问题没有办法得到解决),那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。
    堆内存设置的参数为:-Xmx(最大堆内存容量)和-Xms(最小堆内存容量)

说明:查看异常栈,可以很快得出堆内存发生了内存溢出,并可以定位到哪行代码,
说明:下面用MAT中的Leak Suspects操作来分析这段代码执行过程中的堆转储文件,很容易得出是哪个类的哪个方法的哪行代码操作导致了堆内存溢出:



二、虚拟机栈/本地方法栈溢出:
    对于HotSpot JVM来说,虚拟机栈和本地方法栈是采用同一种分配算法来分配内存的,回收也是。
    关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常;
    如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

说明:查看异常栈,很容易得出是stackLeak()方法的递归操作导致的,

三、方法区溢出
    方法区主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,下面我们用CGLIB动态代理来不断创建新的类型数据来撑爆方法区,

说明:main()方法中抛出了内存溢出异常,是由于类型数据不断创建,而从未卸载过,导致方法区没有新的内存可供分配,

第三部分,讲解一下我们遇到的问题:
    对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,JVM是如何解决的?
    方案一:对分配内存空间的动作进行同步处理,虚拟机采用CAS指令来保证原子操作;
    方案二:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

补充:MAT的操作说明:
    我们用MAT打开hprof文件,打开后的首页,里面是一些堆的基本概要信息,比如空间大小、类的数量、对象实例数量、类加载器等。
Action里面提供了多种分析维度:
(1)Histogram可以列出内存中的对象,对象的个数以及大小;
(2)Dominator Tree可以列出那个线程,以及线程下面的那些对象占用的空间;
(3)Top consumers通过图形列出最大的object;
(4)Leak Suspects通过MA自动分析泄漏的原因,一般使用这个操作就可以定位到原因了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值