Java堆
概念
Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存。
Java堆是垃圾回收的主要区域,而且主要采用分代回收算法。堆进一步划分主要是为了更好的回收内存或更快的分配内存。
存储内容
Java虚拟机规范的描是:所有的对象实例以及数组都要在堆上分配。
不过随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
存储方式
堆内存空间在物理上可以不连续,逻辑上连续即可。
堆内存划分
- 新生代
- Eden空间
- From Survivor空间
- To Survivor空间
- 老年代
堆大小 = 新生代 + 老年代。
堆的大小可通过参数 –Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定
- 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分
别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。
即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。 - JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
- 新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
对象创建
Student stu = new Student();
内存的分配原则
- 1:优先在 Eden 分配,如果 Eden 空间不足虚拟机则会进行一次 MinorGC
- 2:大对象直接接入老年代,大对象一般指的是很长的字符串或数组
- 3:长期存活的对象进入老年代,每个对象都有一个age,当age到达设定的年龄的时候就会进入老年代,默认是15岁。
内存分配方式
内存分配的方法有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)
分配方法 | 说明 | 收集器 |
---|---|---|
指针碰撞 | 内存地址是连续的 | Serial 和 ParNew 收集器 |
空闲列表 | 内存地址不连续 | CMS 收集器和 Mark-Sweep 收集器 |
内存分配安全问题
在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。
在JVM中有两种解决办法:
- 1:CAS,比较和交换(Compare And Swap): CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟 机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB,本地线程分配缓冲(Thread Local Allocation Buffer即TLAB): 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
对象的内存布局
对象在内存中存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。
-
1:对象头
对象头包括两部分信息:
一部分是用于存储对象自身的运行数据,如 哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的 锁,偏向线程ID,偏向时间戳 等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。当对象是一个java数组的时候,那么对象头还必须有一块用于记录数组长度的数据,因此虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。
-
2:实例数据
存储的是对象真正有效的信息。 -
3:对齐填充
这部分并不是必须要存在的,没有特别的含义,在jvm中对象的大小必须是8字节的整数倍,而对象头也是8字节的倍数,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象访问方式
方式 | 优点 |
---|---|
句柄 | 稳定,对象被移动只要修改句柄中的地址 |
直接指针 | 访问速度快,节省了一次指针定位的开销 |
-
句柄访问对象
-
指针访问对象
数组的内存分析
- 一维数组
int[] arr1 = new int[3];
//先把 arr1 压进栈,然后在堆空间中开辟一个空间,并把值初始化为0(arr1为引用变量,但是
//内部数据是int类型,默认值为 0),最后把 开辟的堆空间地址 赋值给arr1
int[] arr2 = arr1;
// 把 arr1 中的 地址 赋值给 arr2,此时 arr2 和 arr1 指向同一块空间。
arr2[0] = 20;
//此时,arr1[0] 值为 20。
- 二维数组
int[][] array = new int[3][];
//这条语句会先把 array 压栈,然后在堆中开辟一个空间,初始值为 null(array为引用变量,
//第一维同样是引用类型),最后把开辟的堆空间地址赋值给 array。
array[0][] = new int[1]
//这条语句会在堆空间中开辟一个 只有一个 int 类型大小的空间,并初始化为 0 ,
//然后把自己的地址赋值给array[0][]
array[1][] = new int[2];
array[2][] = new int[3];
这两条语句和上一条意义一样,就不再做解释