JVM(二)JVM内存布局

这几天我再次阅读了《深入理解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+1);
        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 > 0 ) {            // 置零处理
                  memset(to_zero, 0, obj_size * HeapWordSize);
                }
              }         // 如果对象使用偏向锁
              if (UseBiasedLocking) {
                result->set_mark(ik->prototype_header());
              } else {
                result->set_mark(markOopDesc::prototype());
              }        // 内存对齐
              result->set_klass_gap(0);        // 设置对象的类元数据信息
              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, 0);        // 更新PC的指令地址,执行吓一条指令
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
            }
          }
        }
        // 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(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

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

  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
不得不了解的对象头

时间: 2024-09-29 18:28:08

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

JVM(二):内存模型

内存模型 Java堆(Heap) Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块.Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存. Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做"GC堆".如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代:新生代又可细分为 Eden空间.From Survivor空间

深入理解JVM(二)自动内存管理机制

2.1 C.C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理 2.2 JVM运行时数据区:方法区.堆(运行时线程共享),虚拟机栈.本地方法栈.程序计数器(运行时线程隔离,私有) 2.2.1 程序计数器(Program Counter Register):每一个线程都独有一个程序计数器,并且分配了一块线程私有的小块内存,程序运行时,这个计数器会记录字节码文件运行的行数,当线程切换时,则通过这个行数继续执行下面的操作 2.2.2 虚拟机栈(Java Virtual Machine

c/c++ 对象内存布局

一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格式使用: >cl –d1reportSingleClassLayout[classname] test.cpp 而使用 –d1reportAllClassLayout 则可以查看源文件中所有类及结构体的内存布局. 其中,classname 为类名,-d1reportSingleClassLayout

java源码剖析: 对象内存布局、JVM锁以及优化

一.目录 1.启蒙知识预热:CAS原理+JVM对象头内存存储结构 2.JVM中锁优化:锁粗化.锁消除.偏向锁.轻量级锁.自旋锁. 3.总结:偏向锁.轻量级锁,重量级锁的优缺点. 二.启蒙知识预热 开启本文之前先介绍2个概念 2.1.cas操作 为了提高性能,JVM很多操作都依赖CAS实现,一种乐观锁的实现.本文锁优化中用到了CAS,故有必要先分析一下CAS的实现. CAS:Compare and Swap. JNI来完成CPU指令的操作: unsafe.compareAndSwapInt(thi

JVM 对象的创建、内存布局

一.对象的创建过程 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行init方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内

[转帖]精美图文带你掌握 JVM 内存布局

精美图文带你掌握 JVM 内存布局 https://segmentfault.com/a/1190000021424972 前言 本JVM系列属于本人学习过程当中总结的一些知识点,目的是想让读者更快地掌握JVM相关的知识要点,难免会有所侧重,若想要更加系统更加详细的学习JVM知识,还是需要去阅读专业的书籍和文档. 本文主题内容: JVM 内存区域概览 堆区的空间分配是怎么样?堆溢出的演示 创建一个新对象内存是怎么分配的? 方法区 到 Metaspace 元空间 栈帧是什么?栈帧里有什么?怎么理解

JVM——深入分析对象的内存布局

概述 一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的方法存储在方法区中的.Class 本身就是一个对象,都以 KB 为单位,如果 new Integer() 为了表示一个数据就占用KB级别的内存就有点不值了,下面讲解 JVM 是如何做的.为了表示对象的属性.方法等信息,不得不需要结构描述.Hotspot VM 使用对象头部的一个指针指向 Class 区域的方式来找到对象的 Class 描述,以及内部的方法.属性入口.如下图所示: 在 HotSpot 虚拟机中,对象在内存中存储布

【深入理解JVM】:Java对象的创建、内存布局、访问定位

对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含的主要过程包括了类加载检查.对象分配内存.并发处理.内存空间初始化.对象设置.执行ini方法等. 主要流程如下: 1. 类加载检查 JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化过.如果没有,那必须先执行相应的类的加载过程. 2. 对象分配内存 对象所需内存的大小在类加载完成后便完全确定(对象内存布局),

jvm学习记录-对象的创建、对象的内存布局、对象的访问定位

简述 今天继续写<深入理解java虚拟机>的对象创建的理解.这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录. (此文中所阐述的内容都是以HotSpot虚拟机为例的.) 对象的创建 java程序在运行过程中无时无刻都有对象被创建出来,那么创建对象是个怎么样的过程呢?还是看看我自己的理解吧. 判断是否已经执行类加载 当虚拟机遇到一条new指令时 ,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载