相关文章
- Java运行时数据区域
- Java对象的创建和内存布局
- 最全JVM的参数总结
目录
- 相关文章
- 对象的创建
-
从堆内存中分配内存的方式
- 从堆内存中分配内存时,如何保证线程安全?
-
- 对象的内存布局
- 对象的访问定位
对象的创建
一般,java对象是通过new指令创建的。当JVM遇到一个new指令时,会发生什么?
- 首先检查new指令的参数,即类名,能否在常量池中定位到一个Class类型的符号引用。
- 检查这个符号引用代表的类是否被加载,如果没有加载,先进行类的加载。(如果有父类没被加载,需要先加载父类)
- 在类加载完成后,此类的对象大小就完全确定了。下面的工作就是分配一块内存用于存放对象。见堆内存分配
- 分配完后,JVM会将分配到的内存空间都初始化为零值(不包括对象头)。如果使用了TLAB,这个过程可以提前后TLAB分配时进行。(这才是对象的实例字段不赋初值就可以直接使用的原因)
- 设置对象头和指向元数据的指针
- 执行方法(先执行父类的方法)
从堆内存中分配内存的方式
堆内存的分配方式:
- 指针碰撞:如果堆内存是规整的,规整就是没有碎片,也就是说一边全部占用,另一边全部未使用。中间的有个指针作为分界点,划分内存时,直接将移动指针即可。这种分配方式称为“指针碰撞”。
- 空闲列表:如果堆内存是非规整的,JVM就需要维护一个列表记录哪些内存块是可用的,分配时从中找到足够大的内存划分给内存,并更新这个列表,这种方式称为“空闲列表”。
堆内存是否规整是由所采用的垃圾收集器是否带有整理功能决定的。
有整理功能:Serial、ParNew
没有整理功能:CMS
从堆内存中分配内存时,如何保证线程安全?
对象创建是很频繁的,并发时也不是线程安全的。如何解决这个问题呢
- CAS+失败重试
- TLAB(Thread Local Allocation Buffer) 本地线程分配缓冲:为每个线程预先在堆中分配内存,线程需要内存先在TLAB上分配,只有用完后再分配新的TLAB时才需要同步锁定。-XX:+/-UseTLAB来设定。JDK1.7默认开启。如果对象太大会直接在堆中分配。
对象的内存布局
分为三部分:
- 对象头
- Mark Word:存储对象的运行时数据。32位操作系统占4字节,64位操作系统占8字节,开启指令压缩占4个字节。默认是开启的,所以一般说,Mark Word就是占4字节。
- 类型指针:可选的。指向类元数据的指针。如果是句柄访问的话,就不需要这一项。8字节
- 记录数组的长度:可选的。如果对象是数组才有这一项。4字节
- 实例数据:这部分存储顺序受到虚拟机域分配策略参数FieldsAllocationStyle和源代码中定义的顺序有关,有以下几个原则:
- 默认策略为相同宽度的字段总是放在一起,比如long和double放在一起,byte和boolean放在一起。
- 在满足上面的原则情况下,父类的变量在子类之前。
- 如果CompactFields参数为true的话,子类中较小的变量也可能插入到父类变量的空隙中。
- 对齐填充Padding:保证整个对象的内存占用是8字节的倍数,不足就补齐。
Mark Word 详解
为了节省空间,被设计成非固定的存储空间,以便在极小的空间内存储尽量多的信息,通过对象当前的状态复用自己的存储空间。比如:
- 在非锁定的状态:2bit存标记位(01),25bit存哈希码,4bit存对象分代年龄(故分代年龄最大为15),1bit固定为0(未使用)。
- 可偏向状态:2bit存标记位(01),偏向线程ID,偏向时间戳,对象分代年龄
- 轻量级锁状态:2bit存标记位(00),指向锁记录的指针
- 重量级锁状态:2bit存标记位(10),指向重量级锁的指针
- GC标记状态:2bit存标记位(11),空,已经被GC标记了,不需要记录 信息了!
对象的访问定位
JVM规范中只规定了,栈中的引用变量reference是指向对象的引用,具体通过什么方式定位并没有限制,有JVM实现来定,现在主流的访问方式有两种:
- 句柄访问:在堆中划分一块内存作为句柄池,引用reference存的就是对象的在句柄池中的地址。句柄中包含了对象实例数据和对象类型数据。好处是对象移动时,只需要修改句柄池中的实例数据指针,reference本身不需要修改。缺点是需要两次指针定位。
- 直接指针访问:直接指向堆中的对象地址。这就要求对象中必须含有指向类型数据的引用。好处是一次定位,速度快。不知道对象在频繁移动时,线程中的引用是如何快速变更的。
HotSpot JVM采用的是直接指针访问。