JVM-内存区域(三)

本文是JVM-内存模型浅谈的最后一篇了,因为前两篇重点是介绍java运行时数据区和内存溢出,有些跟java内存有关的内容要么一带而过,要么就没介绍了,因此本文主要是java内存相关的补充介绍了,如堆上对象的内存划分,布局,访问定位以及分配策略,都是java堆那块的内容,毕竟这是java内存模型中最大的一块。

       先来说说对象创建,创建一个对象的方法有好几种,如new、clone、反序列化、反射等等,以最常见的new为例吧,new一个对象就是在java的堆内存中划分一块区域给它并完成对象的初始化,顺便提一句,在new对象之前,要先检查这个对象的类是否被JVM加载、解析和初始化过,如果没有,JVM需要利用类加载机制先完成这一步。那java堆是怎么给new出来的新生代对象分配内存呢?虚拟机提供了两种方式去分配堆内存,一种称为“指针碰撞”,一种称为"空闲列表"。所谓指针碰撞就是划分给对象的内存区域都是一块一块完整的,并且是连续的。但是如果划分的内存区域和空闲的内存区域是相互交错的,那就没办法使用指针碰撞的方式来划分堆内存了,这时就使用空闲列表。空闲列表是虚拟机维护的一个列表,上面记录了哪块内存是可用的,给对象分配内存时就从这个列表中去寻找。划分内存还有个问题,就是并发,如果一个对象正在分配一块内存,指针还没来得及修改或空闲列表还没来得及更新时,又有另一个对象要分配内存,那么这两个对象分配的内存区域是不能有重合的,不然会有一个对象全部或部分数据被覆盖掉,这就不行了。一般解决这个问题有两种方案,一种是对分配这个动作做同步处理,另一种是被称为本地线程分配缓冲的方法。第一种是虚拟机采用CAS加失败重试的方法来使分配操作是原子性。第二种就是说并发时,每个线程先预划分一块内存,每个线程在自已预先划分的一块内存中为自己线程新建的对象划分区域,这样就在空间上把每个线程隔离开了,但是你可能会问了,每个线程预先划分一块内存要是重合了呢?这个疑问是对的,其实这个问题也还是要靠同步锁定来解决的。

       那么一个对象在堆内存中又有哪些数据组成的呢?在HotSpot中,对象在堆内存中存储的数据分三块,分别为对象头、实例数据和对齐填充。其中对象头又分为两个部分,第一部分储存了对象的身份信息,包括对象的哈希码、GC分代年龄、锁状态标志等等,这些信息虚拟机用32位或64位bit数据来存储,这部分数据也称为Mark Word。对象头还有一部分就是类型指针,我们知道,类信息(或称类元信息)都存在方法区,这个类型指针就是堆中对象指向方法区其所属类的地址。还有一个值得提的就是数组,之前也说过数组也是存储在堆内存里的,java对象可以通过其类元信息获知其大小,但是数组不能通过其数组元数据确定,所以对于一个数组对象来说,对象头里面有一块记录数组长度的数据。对象的实例数据就是你代码里定义的各种类型的字段,这个好理解,但是需要知道的是,这些字段包括从父类继承下来的,还有子类定义的。对象的第三块部分不是必须的,对齐填充在计算机世界底层的二进制位数处理中很常见的,因为二进制,我们特别喜欢是2的倍数的数字,比如Base64编码,你会发现经Base64编码过的一连串字符串的末尾经常会有一个或两个“=”号,这就是因为填充的原因。HotSpot虚拟机要求对象的大小必须是8个字节的整数倍,所以如果对象的对象头和实例数据大小之后不是8个字节的长度,就通过对齐填充补足长度,如果是,那对齐填充这部分就不需要了。

       通过前文我们知道,java程序在运行时,对象引用数据是放在虚拟机栈上面的,其对象数据是存放在虚拟机堆上的,那栈上的引用是如何访问到对象的呢?主流的访问方式有两种,一种是直接指针,另一种就是使用句柄。其实很简单,举个例子,不知道你们家还有没有座机,我记得小时候打电话都是用座机,那时候基本没有手机。我想用座机打电话给我同学,如果我知道他家电话号码我就直接拨号了,如果我不知道,我会去查电话本,这个电话本是电信局发的,里面记录了我们这个镇所有村,所有生产队,每户人家的电话号码,我可以通过这个电话本找到我同学家电话号码,然后打过去。计算机世界的寻址跟这场景很类似,电话本就是句柄(或句柄池),我直接记住的电话号码就是直接指针。

       内存的分配策略是针对堆内存而言的,在我的第一篇内存区域划分文章中就已经介绍了,就是对象的新生代与老年代,大家可以往前看,就不重复叙述了。这里介绍下第一篇内存区域划分文章中一带而过的空间分配担保。我们都知道java堆内存的回收使用的是分代收集,其中新生代使用的是复制收集算法,依照sun公司的研究结论(前文有介绍)最终定义了一个To Survivor空间用于复制备份,如果Eden区和From Survivor区进行了Minor GC后,存活的对象的大小超过了ToSurvivor的空间大小,怎么办,这时就可以考虑用老年代的空间来备份这些存活对象,前提是老年代有足够的连续空间来存。这种设计就叫分配担保。但是细想一下,这么搞好像也有问题,因为Minor GC能回收多少空间还剩多少对象只有概率上的结果,具体到每次MInor GC老年代是不知道我的空间够不够用于备份,如果老年代的可用连续空间大小大于Eden区和From Survivor区空间总和,那没问题,放心Minor GC吧,即使Minor GC一个对象都没回收掉,老年代也能复制整个Eden区和From Survivor对象。但是如果老年代的可用连续空间大小小于Eden区和From Survivor区空间总和,那么此时就一定不能Minor GC,要Full GC吗(Full GC比Minor GC慢十倍)?这也未必。虚拟机毕竟是人创造的嘛!人有个特点就是喜欢依赖经验去做事,这个特点也被用在虚拟机上,虚拟机依据每一次回收晋升到老年代的所有对象的大小值取平均值作为经验值跟老年代可用连续空间大小值做比较,如果小于,做Minor GC,如果大于做Full GC。

       本文到这儿,关于jvm的内存区域我就介绍完了,但是学习JVM,这才刚刚开始,后面我还会继续写,我也会思考怎么把后面的内容写好。学习虚拟机有点像苦行僧,唯有坚持,别无他方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值