文章目录
一、堆内存划分
- 堆被划分为新生代和老年代,默认比例为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];
这两条语句和上一条意义一样,就不再做解释