JVM系列七——堆

一、堆内存划分

在这里插入图片描述

  • 堆被划分为新生代和老年代,默认比例为1:2
  • 新生代被划分为一个Eden和两个 Survivor,默认比例为8:1:1
  • 细粒度划分堆内存是为了垃圾回收,垃圾回收针对不同情况的对象,回收策略(回收算法)是不同的的。而通过内存的划分,可以将不同的算法在不同的区域中进行使用。
  • 年轻代中的对象,生命周期很短,基本上是很快就死了,也就是被GC了。
  • 老年代中的对象,都是一些老顽固,都是多次回收的对象或者大对象才存到老年代中 。

二、对象创建过程

在这里插入图片描述

1.一个对象new出来经过逃逸分析,如果为发生逃逸,判断线程栈是否能分配下

  • 如果能分配下,直接分配在栈中。
  • 如果分配不下则进入堆中。

2.判断对象是否足够大(大对象一般指的是很长的字符串或数组

  • 如果够大,直接进入老年代
  • 不够大,则进入Eden
  • 如果Eden空间不足,则进行一次MinorGC

3.判断创建对象的线程的**本地线程缓冲区(TLAB)**空间是否足够

  • 如果足够,直接分配在TLAB中
  • 不足,则进入Eden其他区域

4.MinorGC清除,未被清除的对象进入Survivor1区,年龄加1
5.再次MinorGC清除,未被清除的对象进入Survivor2区,年龄加1,当年龄达到15时进入老年代
6.老年代空间不足进行FullGC,清除对象

2.1 内存分配方式

内存分配方式有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)

分配方法说明收集器
指针碰撞适用于内存空间规整的情况。当我们分配内存时,只需要指针向右移动与对象所需内存相同大小的距离即可,所以称为“指针碰撞”Serial 和 ParNew 收集器
空闲列表适用于内存空间不规整的情况。当空闲内存区域和已使用内存区域相互交错时,虚拟机就需要维护一个列表,用来记录空闲内存块。当需要分配内存的时候,则需要找到一块足够大的内存区域分给对象实例,并更新表中的记录。CMS 收集器和 Mark-Sweep 收集器

在这里插入图片描述

2.2 内存分配安全问题

在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。
在JVM中有两种解决办法:

  • CAS,比较和交换(Compare And Swap): CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB,本地线程分配缓冲(Thread Local Allocation Buffer即TLAB): 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

三、对象的内存布局

在这里插入图片描述

3.1 对象头

  • 对象一部分是用于存储对象自身的运行数据,如 哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
  • 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。在不开启对象指针压缩的情况下是8字节。压缩后变为4字节,默认压缩。
  • 当对象是一个java数组的时候,那么对象头还必须有一块用于记录数组长度的数据,因此虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。

3.2 实例数据

存储的是对象真正有效的信息。

3.3 对齐填充

这部分并不是必须要存在的,没有特别的含义,在jvm中对象的大小必须是8字节的整数倍,而对象头也是8字节的倍数,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.4 Object o = new Object()在内存中占了多少字节?

markword 8字节,因为java默认使用了calssPointer压缩,classpointer 4字节、padding 4字节,因此是16字节;如果没开启classpointer默认压缩,markword 8字节、classpointer 8字节、padding 0字节也是16字节。
详细分析:https://blog.csdn.net/u011727756/article/details/106546178

四、对象的访问方式

对象访问在Java语言中无处不在,即使最简单的访问也涉及Java栈、Java堆、方法区这三个重要的内存区域中。
例:Object obj = new Object();
  Object obj 反映到Java栈(Java VM Stack)的本地变量表,作为一个reference类型数据出现。
  New Object() 反映到Java堆中,形成了一块存储了Object类型的所有实例数据值的结构化内存。根据具体对象类型以及虚拟机实现对象内存局部表的不同,这块内存的长度是不固定的。同时Java堆中还包含查找此对象信息的地址信息。
通过reference类型如何访问Java堆中的对象?主流的访问方式有两种:句柄访问方式和直接指针访问方式

4.1 句柄访问方式

在这里插入图片描述
 Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据和类型数据各自的具体地址。

4.2 直接指针访问方式

在这里插入图片描述
 Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象堆地址。

4.3 优势对比

句柄访问方式:reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只要修改句柄中的实例数据指针,而reference本身不需要被修改。
直接指针访问方式:最大好处就是速度快,它节省了一次指针定位的时间开销。HotSpot虚拟机使用的就是这种方式。

五、数组的内存分析

5.1 一维数组

在这里插入图片描述

  • int[] arr1 = new int[3];先把 arr1 压进栈,然后在堆空间中开辟一个空间,并把值初始化为0(arr1为引用变量,但是内部数据是int类型,默认值为 0),最后把开辟的堆空间地址赋值给arr1。
  • int[] arr2=arr1;把 arr1 中的地址赋值给 arr2,此时 arr2 和 arr1 指向同一块空间。
  • arr2[0]=20;此时,arr1[0] 值也为 20。

5.2 二维数组

在这里插入图片描述
1.int[][] array = new int[3][];
这条语句会先把 array 压栈,然后在堆中开辟一个空间,初始值为 null(array为引用变量,第一维同样是引用类型),最后把开辟的堆空间地址赋值给 array。
2.array[0][] = new int[1];
这条语句会在堆空间中开辟一个 只有一个 int 类型大小的空间,并初始化为 0 ,然后把自己的地址赋值给array[0][]。
3.array[1][] = new int[2]; array[2][] = new int[3];这两条语句和上一条意义一样,就不再做解释

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值