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