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+);
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内存布局的更多相关文章
- 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用
欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...
- 基础篇:JVM运行时内存布局
目录 1 JVM的内存区域布局 2 JVM五大数据区域介绍 3 JVM运行时内存布局和JMM内存模型区别 4 JMM内存模型交互操作 欢迎指正文中错误 关注公众号,一起交流 参考文章 1 JVM的内存 ...
- 深入理解JVM(二)Java内存区域
2.1 C.C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理 2.2 JVM运行时数据区:方法区.堆(运行时线程共享),虚拟机栈.本地方法栈.程序计数器(运行时线程隔离,私有) 1 ...
- JVM(二) 栈内存结构
栈内存是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表.操作数栈.动态链接.返回出口等信息.每一个方法从调用直至执行完成的过程,就对应 ...
- jdk源码剖析二: 对象内存布局、synchronized终极原理
很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...
- JVM内存布局及GC知识回顾
注:本文篇幅较长,且需要有一定的java基础,建议各位看官,备好瓜子.饮料.小板凳,摆个让自己舒服的姿势,慢慢细看^_^, 文中所有素材,均来自互联网,本人只是详细梳理了一遍,形成此文. 一.JVM运 ...
- JVM内存布局及GC知识
一.JVM运行时内存布局 按java 8虚拟机规范的原始表达:(jvm)Run-Time Data Areas, 暂时翻译为"jvm运行时内存布局". 从概念上大致分为6个(逻辑) ...
- JVM(十一):内存分配
JVM(十一):内存分配 在前面的章节中,我们花了大量的篇幅去介绍 JVM 内的内存布局.对象在内存中的状态.垃圾回收的算法和具体实现等.今天让我们探讨一下对象是如何分配内存的. 堆内存划分 前面说过 ...
- 深入理解 Java 对象的内存布局
对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...
- c/c++ 对象内存布局
一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...
随机推荐
- 用css3的cursor:zoom-in/zoom-out实现微博看图片放大镜效果
1.前言 CSS3的出现解决了很多让人头疼的问题,至少我想很多童鞋都这样认为.css3的cursor属性大家用的应该是非常的多的,我想用的比较多的像cursor:pointer;cursor:help ...
- Eclipse、maven项目常见问题
阿里云maven仓库地址: <mirror> <id>nexus-aliyun</id> <mirrorOf>*</mirrorOf> &l ...
- 红帽 Red Hat Linux相关产品iso镜像下载【百度云】(转载)
不为什么,就为了方便搜索,特把红帽EL 5.EL6.EL7 的各版本整理一下,共享出来.正式发布 6.9 :RedHat Enterprise Server 6.9 for x86_64:rhel-s ...
- 简陋的 ASP.NET CORE 单页Web应用程序“框架”
我对ASP.NET CORE了解不多,不知道是不是重复造轮子,也或者什么也不是,这个Demo是这样的: 1.非常简单或者说原始:2.将单页Web应用增加了一个页(Page)概念(相当于MVC的 Vie ...
- null == undefined ?
最近在看<JavaScript高级程序设计>一书,书中讲到相等操作符(==)时说,要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,但要记住 null == u ...
- web项目中图标的前端处理方案
工程中用到图标是常事,那这些图标我们前端一般是怎么解决的呢?这几种方案有什么优缺点呢? 第一种: SVG Sprite SVG sprite其实就是svg的集合.SVG即可缩放矢量图形 (Scalab ...
- python基础===随机打印txt文件中的某一行
def find(): txt = open(r'F:\send1.txt','rb') data = txt.read().decode('utf-8') #python3一定要加上这句不然会编码报 ...
- 剑指OFFER——合并两个有序的链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则. 再做这道题就是想不起来书上怎么做的了,但是最近看STL里面全是这种基础的合并啊,比较啊.就算不会也看会了. ...
- noi2015 day1 T2软件包管理器
noi2015 软件包管理器 Description Linux用户和OSX用户一定对软件包管理器不会陌生.通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软 ...
- 【MySQL】 清除等待连接
由于MySQL突然新增了很多连接,超出了my.cnf所设置的最大连接数,MySQL服务无法访问,这里通过Shell脚本来删掉Sleep连接 方式1 清除连接进程 #!/bin/bash #------ ...