距离第一篇博客有段时间了,这么久才出第二篇。首先呢,这段时间有些小加班。回到家,跑个步健个身(希望自己练出腹肌,哈哈)就没啥时间。但是写博客还是一直放在心上,也会继续坚持下去滴。毕竟,学习和健身一个道理:坚持才是王道。这篇是无忌来到九阳神功第一层。望与君共勉
对象的创建
在Java程序员里面有句经典的话:现实中没有对象?没关系,咱们自己在程序中创建一个对象就好了。这也就是java中的new一个对象。在new对象的时候,康康JVM都干了些啥:自己画了一个流程图(还挺好看的)
两种内存分配方式:
- 指针碰撞 (根据个人理解画了个图,形象理解一下)
若Java堆中内存是绝对规整的,所有被使用过的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲方向挪动一段与对象大小相等的距离。
- 空闲列表
若Java堆中的内存并不是规整的,已经被使用的内存个空闲内存相互交错在一起,则虚拟机就需要维护一个列表。记录哪些内存块是可用的,在内存分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 结论
选择哪种内存分配方式是由Java堆是否规整所决定。指针碰撞的分配方式相较于空闲列表既简单又高效。但是在多线程并发情况下,指针碰撞分配方式是线程不安全的。当对象A分配内存时,指针还没来得及修改,对象B又同时使用原来的指针来分配内存。从而导致内存分配错误。
解决办法有两种:1、虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
2、每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓冲(TLAB)。虚拟机可通过-XX:+/-UserTLAB参数来设定。
对象的内存布局
对象在内存中的存储布局主要三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头
虚拟机中对象头部分主要包括两类信息:
(1)第一类用于存储对象自身的运行时数据。如:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
存储内容 | 标志位 / 状态 |
---|---|
对象哈希码、对象分代年龄 | 01 / 未锁定 |
指向锁记录的指针 | 00 / 轻量级锁定 |
指向重量级锁的指针 | 10 / 膨胀 |
空,不需要记录信息 | 11 / GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 / 可偏向 |
(2)另一类是类型指针,即对象指向它的类型元数据的指针。Java虚拟机通过这个指针来确定改对象是哪个类的实例。
- 实例数据
这部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机的分配策略参数(-XX:FieldsAllocationStyle参数)和字段Java源码定义顺序的影响。虚拟机默认的分配顺序为:long/doubles、ints、shorts/chars、bytes/booleans、oops。
- 对齐填充
对齐填充起的是占位符的作用,不是必然存在的。其只要保证对象的大小是8字节的整数倍即可。
对象的访问定位
对象的访问方式是由虚拟机实现而定的,主流的访问方式主要有:句柄和直接指针
- 句柄访问
Java堆中会划分出一块内存作为句柄池,句柄中包含了对象实例数据与类型数的具体地址信息。
- 直接指针访问
顾名思义,自接指针访问突出一个字:“直接”,所以访问速度比句柄快。属于房东直租,节省了中介的开销。
- 区别
这两种对象访问方式各有千秋。句柄访问最大好处就是reference中储存的是句柄地址,因此在对象被移动是只会改变句柄中的实例数据指针,而reference本身不需要被修改。直接指针访问就是突出一个快,节省了一次指针定位的时间开销。HotSpot主要使用直接指针访问对象。
结言
以上内容是本人自己看《深入理解Java虚拟机》和其他博客大佬总结所得。当然有些彩图是来源于别人。如有不足,还请指教。相互学习,相互成长。感谢阅览,感谢点赞,感谢关注。