这几天我再次阅读了《深入理解Java虚拟机》之第二章“Java内存区域与内存溢出异常”,同时也参考了一些网上的资料,现在把自己的一些认识和体会记录一下。

 (本文为博主原创文章,转载请注明出处)

一、概述

在网上看到很多的各种文章来写Java内存布局/Java内存模型(JMM)/Java内存分配和回收等。初学者,往往容易被搞混淆,这些东西到底都是些啥?讲的是不是同一个东西?如果不是同一个东西,那它们之间又有什么区别和联系?说句实话,笔者在看到这些文章和概念时,一样是有这些疑问的。下面我来说说我的理解,如果有人觉得有异议或者不对的地方,欢迎讨论和指出,谢谢!

1.1 Java内存布局

我认为这是一个静态的概念, 即JVM在概念和作用上对其管理的内存进行划分。如整体上来看,JVM的内存分为堆区和非堆区,而非堆区又包括了方法区、JVM栈、本地方法栈、程序计数器等。直接上图:

图1.1

1.2 Java内存模型(JMM)

这个讲的是Java多线程的情况下,多线程之间的内存通信模型。即如果多个线程存在共享同一块内存的情况下,JVM通过一个什么样的内存模型来解决多线程对共享内存的读取和写入数据的问题。借用网上的一张图:

图1.2

从上图中我们可以看到,JMM的核心思想是把堆中的内存分成了两部分:主内存和本地内存,主内存为所有线程共享,而本地内存则由线程独享,同时,对于共享变量,在线程的本地内存中都保存了一个副本共当前线程使用。其他更具体的细节,笔者会在后续的博客中继续跟进和分析。当然,读者也可以自行在网上搜,会有很多的文章讲JMM的。

参考:http://www.infoq.com/cn/articles/java-memory-model-1

1.3 Java内存分配和回收

这是一个侧重JVM对内存的使用和回收方法的概念。即JVM是如何分配内存,如何回收内存?通常Hotspot的JVM的分配和回收算法(分代垃圾回收)中,又体现出了如下的内存布局图,同样先借用网上的一张图:

图1.3

在JVM的内存空间中把堆空间分为年老代和年轻代。将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。年轻代中又被分为Eden区(圣经中的伊甸园)、和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到年老区。
参考:http://www.cnblogs.com/douba/p/a-simple-example-demo-jvm-allocation-and-gc.html

讲完了这三个概念的比较,接下来进入正题“JVM内存布局”

二、Java虚拟机运行时数据区划分

运行时的数据区域划分,可以参考图1.1,现在就几个数据区域的功能和作用做详细的介绍。

2.1 JVM堆(heap)

其主要作用是用于为几乎所有的对象实例和数组实例的实例化提供内存空间。说通俗点,所有采用new关键字产生的对象的空间都在此分配。如:Map<String,String> map = new HashMap<>(); 这个map所引用的对象即位于JVM的堆中。

JVM heap的特点是:1)内存不一定连续分配,只要逻辑上是连续的即可;

           2)内存的大小可以通过JVM的参数来控制:如 -Xms = 1024M -Xmx = 2048M; 表示JVM Heap的初始大小为1GB,最大可自动伸缩到2GB;

           3)平时所说的Java内存管理即指的是内存管理器对这部分内存的管理(创建/回收);

           4)所有线程共享 

而JVM Heap的内存在采用分代垃圾回收算法的Hotspot JVM中,这片区域会被分割成新生代/老年代/永久代,而新生代中又分成了两部分Eden区和Survivor区(分为from space和to space,各自占Survivor区的50%的内存大小)。此部分的详细信息,可以参考图1.3 。然而,在内存分配的时候,JVM通常会从Heap中为每个线程分配一个线程缓冲区(Thread Local Allocation Buffer, TLAB),TLAB为线程私有,因此在为对象分配内存时,首先会先从TLAB中分配内存,这样做可以保证在分配内存的时候不用锁住整个堆区,减少锁开销。

2.2 方法区

主要用于存储已经由JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Hotspot JVM中,设计者使用“永久代”来实现方法区,这样带来的好处是,JVM可以不用再特别的写代码来管理方法区的内存,而可以像管理JVM 堆一样来管理。带来的弊端在于,这很容易造成内存溢出的问题。我们知道,JVM的永久代使用 --XX:MAXPermSize来设定永久代的内存上限。

值得一提的是,运行时常量池也属于方法区的一部分,所以,它的大小是受到方法区的限制的。运行期间也可以将新的常量放入池中,运用的比较多的就是String的intern()方法。

2.3 JVM栈(Java虚拟机栈)

JVM栈是伴随着线程的产生而产生的,属于线程私有区域,生命周期和线程保持一致,所以,JVM的内存管理器对这部分内存无需管理;

从图1.1中可以看出,栈中有包含了多个栈帧(frame),frame伴随的方法的调用而产生,方法调用完毕,frame也就出栈,从JVM栈中被移除。frame主要保存方法的局部变量、操作数栈、动态链接、方法出口(reternAddress或者抛Exception)。

虚拟机规范中,对此内存区域规定了两种异常情况:

  1)当线程请求的战的深度大于JVM所允许的最大深度,则抛出StackOverflowError;

  2)当栈的内存是可动态扩展的时候,如果扩展时发现堆内存不足时,会抛出OutofMemoryError;

2.4 本地方法栈

与JVM栈非常类似,只不过,本地方法栈是为Java的Native method方法调用的时候服务。JVM规范并未定义如何实现该区域,但是,在Hotspot JVM中,本地方法栈和JVM栈合二为一,所以,本地方法栈同样会抛出StackOverflowError和OutofMemoryError。

2.5 程序计数器

简称PC(Program Counter Register),为线程私有,如果执行的是一个Java方法,那么此时PC里保存的是当前Java字节码的指令地址;如果说执行的是Native方法,那么PC的内容则为空。

2.6 直接内存

这部分内存不属于JVM的内存区域,在这里提出来也是为了在心里对这个有一定的了解。在JDK1.4中提出来的NIO中,就存在这直接访问机器内存的情况,从而避免了机器内存到JVM内存之间数据的来回拷贝情况,能够提高性能。在虚拟机内存调优时,可能常常容易被遗忘的部分就是这部分内存了。这也是有时候,Java程序实际占用的内存大于-Xmx所限制的内存大小的原因之一。

三、Hotspot虚拟机对象分析

3.1 对象的布局

Hotspot JVM中,对象在内存中的布局包括3块区域:对象头(header),对象实例数据(Instance Data)和对齐填充(Padding)。

3.1.1 对象头(Header)

对象头又包含两部分内容:Markword和reference(类类型指针)

3.1.1.1 Markword

用于存储运行时自身的数据,包括哈希码、GC分代年龄、偏向锁标记、线程持有的锁、偏向线程ID、偏向时间戳等。

从markOop.hpp中的注释中,我们可以详细的了解到Markword各个bit位所代表的内容和含义

// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

以32位的系统为例,能总结出如下表:

图3.1

3.1.2 reference(类类型指针)

用于指向对象的类的元数据的指针,通过这个指针,JVM可以知道,这个对象是属于哪个类的实例对象。当然,并不是所有的JVM的实现都会把对象的类类型指针保留在对象上的(这里是由Java对象的内存访问定位实现来决定的)。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

当Java对象的内存访问定位是直接指针访问时(大部分JVM这样实现),才会在对象的头部保存类类型指针,如图

图3.2 直接指针访问对象

当Java对象的内存访问定位采用的是句柄访问时,则不会在对象的头部保存类类型指针,如图

图3.3 句柄访问对象

3.2 JVM对象内存分配过程

通过源码(bytecodeInterpreter.cpp)中的来分析,如下:

// 如果是new操作的时候
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+);
ConstantPool* constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
      // 1. 确保类已经初始化,及类已经加载/验证/准备/解析/初始化完成了。
Klass* entry = constants->slot_at(index).get_klass();
      // 确保类已经被解析
assert(entry->is_klass(), "Should be resolved klass");
Klass* k_entry = (Klass*) entry;
      // 确保类已经被初始化
assert(k_entry->oop_is_instance(), "Should be InstanceKlass");
InstanceKlass* ik = (InstanceKlass*) k_entry;
      // 2. 如果类已经初始化并且能够进行快速内存分配
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
        // 获取对象实例的大小
size_t obj_size = ik->size_helper();
oop result = NULL;
// If the TLAB isn't pre-zeroed then we'll have to do it
       // 如果在TLAB上分配内存,如果TLAB上事先未将内存进行置零处理,则后续在内存分配完毕之后,需要将对象所在的内存进行置零操作
bool need_zero = !ZeroTLAB;
if (UseTLAB) {
         // 如果开启了线程的TLAB,则直接在线程的TLAB上给对象分配内存,如果分配成功,则返回对象的地址,否则返回null,是否开启TLAB是通过虚拟机参数:-XX:+/-UseTLAB参数来设定,默认是开启的。
result = (oop) THREAD->tlab().allocate(obj_size);
}
// Disable non-TLAB-based fast-path, because profiling requires that all
// allocations go through InterpreterRuntime::_new() if THREAD->tlab().allocate
// returns NULL.
#ifndef CC_INTERP_PROFILE
if (result == NULL) {
        // 如果在TLAB上未成功分配到内存给对象,则从堆上为对象分配内存
need_zero = true;
// Try allocate in shared eden
retry:
        // 获取堆顶的地址
HeapWord* compare_to = *Universe::heap()->top_addr();
        // 获取分配obj_size字节的内存后,堆顶的位置
HeapWord* new_top = compare_to + obj_size;
        // 如果堆顶地址小于或等于堆的最大内存地址,则采用原子操作CAS,将堆顶的地址指向new_top,从而完成内存的分配
if (new_top <= *Universe::heap()->end_addr()) {
if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
          // 如果CAS操作成功,则表示内存分配成功,然后把原来堆顶的内存地址赋值给指向对象的引用,即对象的内存地址
result = (oop) compare_to;
}
}
#endif
if (result != NULL) {
// Initialize object (if nonzero size and need) and then the header
         // 如果对象不为空,且需要置零,则置零处理
if (need_zero ) {
          // 我们知道,对象的头是不能置零处理的,只有对象的body部分能置零处理,所以跳过对象的头部内存sizeof(oopDesc)/oopSize部分
HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
obj_size -= sizeof(oopDesc) / oopSize;
if (obj_size > ) {
           // 置零处理
memset(to_zero, , obj_size * HeapWordSize);
}
}
         // 如果对象使用偏向锁
if (UseBiasedLocking) {
result->set_mark(ik->prototype_header());
} else {
result->set_mark(markOopDesc::prototype());
}
        // 内存对齐
result->set_klass_gap();
        // 设置对象的类元数据信息
result->set_klass(k_entry);
// Must prevent reordering of stores for object initialization
// with stores that publish the new object.
OrderAccess::storestore();
SET_STACK_OBJECT(result, );
        // 更新PC的指令地址,执行吓一条指令
UPDATE_PC_AND_TOS_AND_CONTINUE(, );
}
}
}
// Slow case allocation
// 如果未执行快速内存划分,则执行慢速内存划分,可能会采用锁的机制来完成,效率会比较低
CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
handle_exception);
// Must prevent reordering of stores for object initialization
// with stores that publish the new object.
OrderAccess::storestore();
SET_STACK_OBJECT(THREAD->vm_result(), );
THREAD->set_vm_result(NULL);
UPDATE_PC_AND_TOS_AND_CONTINUE(, );
}

从以上源码,简单的总结下对象内存分配的过程:

  1). 确保类已经初始化,及类已经加载/验证/准备/解析/初始化完成了;

  2). 判断该类的实例对象是否能够进行快速内存分配,如果是,则继续,否则跳到10;

  3). 判断JVM是否开启了TLAB选项,如果是则继续,否则跳到7;

  4). 从线程的TLAB进行内存分配;

  5). 判断TLAB内存分配是否成功,如果失败,则继续,否则跳到7;

  6). 如果TLAB内存分配失败,且未定义CC_INTERP_PROFILE,则通过CAS继续进行快速内存分配;

  7). CAS内存分配成功(如果失败,则会抛出OutOfMemoryError的异常,而导致程序终止),则根据实际情况看是否要对对象的内存进行初始化操作(置零);

  8). 设置对象的头的MarkWord和对象所属类的元数据信息;

  9). 设置PC的地址为下一条字节码的地址,并继续执行下一条字节码,本次内存分配结束;

  10). 进入JVM的慢速内存分配通道,进行内存分配。

四、总结

本文首先阐述了一下让笔者在学习之初比较困惑的3个问题,然后结合有关参考资料和Hotspot的源码分析对JVM的内存布局以及对象内存的分配过程做了比较细致的学习和分析,同时对对象在JVM中的表示进行分析。基本上弄清楚了JVM中的内存概念布局。后续还需要在JMM和垃圾回收的算法等方面加强对JVM的内存模型理解。

参考资料:

2. http://blog.csdn.net/shiyong1949/article/details/52585256
内存区域介绍
3. http://www.cnblogs.com/douba/p/a-simple-example-demo-jvm-allocation-and-gc.html
最简单例子图解JVM内存分配和回收
4.http://www.idouba.net/java-gc-policies/
java 垃圾回收策略
5. 虚拟机配置相关参数
JDK7及以前版本:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
JDK8: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
6. java并发之内存模型(JMM)
http://www.idouba.net/topics/jvm/
7.http://www.cnblogs.com/SaraMoring/p/5713732.html
JVM内存堆布局图解分析
8. http://blog.csdn.net/yangzl2008/article/details/43202969
Java中的逃逸分析和TLAB中以及Java对象分配
9. http://blog.csdn.net/zhoufanyang_china/article/details/54601311
不得不了解的对象头

JVM(二)JVM内存布局的更多相关文章

  1. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  2. 基础篇:JVM运行时内存布局

    目录 1 JVM的内存区域布局 2 JVM五大数据区域介绍 3 JVM运行时内存布局和JMM内存模型区别 4 JMM内存模型交互操作 欢迎指正文中错误 关注公众号,一起交流 参考文章 1 JVM的内存 ...

  3. 深入理解JVM(二)Java内存区域

    2.1 C.C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理 2.2 JVM运行时数据区:方法区.堆(运行时线程共享),虚拟机栈.本地方法栈.程序计数器(运行时线程隔离,私有) 1 ...

  4. JVM(二) 栈内存结构

    栈内存是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表.操作数栈.动态链接.返回出口等信息.每一个方法从调用直至执行完成的过程,就对应 ...

  5. jdk源码剖析二: 对象内存布局、synchronized终极原理

    很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...

  6. JVM内存布局及GC知识回顾

    注:本文篇幅较长,且需要有一定的java基础,建议各位看官,备好瓜子.饮料.小板凳,摆个让自己舒服的姿势,慢慢细看^_^, 文中所有素材,均来自互联网,本人只是详细梳理了一遍,形成此文. 一.JVM运 ...

  7. JVM内存布局及GC知识

    一.JVM运行时内存布局 按java 8虚拟机规范的原始表达:(jvm)Run-Time Data Areas, 暂时翻译为"jvm运行时内存布局". 从概念上大致分为6个(逻辑) ...

  8. JVM(十一):内存分配

    JVM(十一):内存分配 在前面的章节中,我们花了大量的篇幅去介绍 JVM 内的内存布局.对象在内存中的状态.垃圾回收的算法和具体实现等.今天让我们探讨一下对象是如何分配内存的. 堆内存划分 前面说过 ...

  9. 深入理解 Java 对象的内存布局

    对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...

  10. c/c++ 对象内存布局

    一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...

随机推荐

  1. ES2017中的async函数

    前面的话 ES2017标准引入了 async 函数,使得异步操作变得更加方便.本文将详细介绍async函数 概述 async 函数是 Generator 函数的语法糖 使用Generator 函数,依 ...

  2. maven单元测试设置代理

    背景 环境需要设置代理才能够访问外部网络,如果只是运行java程序来访问网络,我们可以通过java -jar test.jar -DproxyHost=proxy_ip -DproxyPort=pro ...

  3. Oracle 12C 新特性 - “可插拔数据库”功能

    Oracle 12C加入了一个非常有新意的功能"可插拔数据库"特性,实现了数据库(PDB)在"容器"(CDB)上的拔功能,既能提高系统资源的利用率,也简化大面积 ...

  4. Qt For Android 开发环境配置

    想了想,还是再写一篇关于Qt for Android开发环境配置的教程. 准备:Java jdk,Android sdk,Android adb,Android ndk,Android ant,Qt ...

  5. 带你快速进入.net core的世界

    [申明]:本人.NET Core小白.Linux小白.MySql小白.nginx小白.而今天要说是让你精通Linux ... 的开机与关机.nginx安装与部署.Core的Hello World .. ...

  6. ABP+AdminLTE+Bootstrap Table权限管理系统第十节--AdminLTE模板菜单处理

    上节我们把布局页,也有的临时的菜单,但是菜单不是应该动态加载的么?,所以我们这节来写菜单.首先我们看一下AdminLTE源码里面的菜单以及结构. <aside class="main- ...

  7. Luogu P2966 [USACO09DEC]牛收费路径Cow Toll Paths

    题目描述 Like everyone else, FJ is always thinking up ways to increase his revenue. To this end, he has ...

  8. 浅尝Java(二、代码折叠插件的使用)

    主题:eclipse代码折叠插件的使用. 工作中在使用eclipse开发Java项目时,我们会写很多if,for循环啊什么的,这使得我们的项目代码会有很多很多行.写完后要想检查或者查看,就要从头一行一 ...

  9. python基础===使用switch方法,减少使用if语句

    def jia(x,y): return x+y def jian(x,y): return x-y def cheng(x,y): return x*y def chu(x,y): return x ...

  10. Markdown公式编辑

    一.公式使用参考 1.如何插入公式 行中公式(放在文中与其它文字混编)可以用如下方法表示:$ 数学公式 $ 独立公式可以用如下方法表示:$$ 数学公式 $$ 自动编号的公式可以用如下方法表示: 若需要 ...