JVM内存分配
对象创建流程
- 加载检查
1.1 没有加载则加载 - 分配内存
- 初始化
- 设置对象头(object header)
- 执行 <init> 方法
分配内存
- 指针碰撞(默认):在可使用临界的指针后分配空间给对象使用 然后挪动指针
- 空闲列表:有集合维护空闲的内存空间 看哪些空间能放下对象
并发分配对象空间问题
- CAS加上失败重试
- TLAB(默认):线程可配分对象空间独享,默认大小是E区的1/100,如果放不下则直接存放入E区,降级为方式1
设置对象头
对象在内存中大概有3个部分组成:对象头、示例数据、对齐填充(保证对象是8个字节的整数倍,效率原因)
对象头有3部分组成
- 标记字段(Mark word):包含一些Hash 分代年龄 锁信息
- 类型指针(Klass Pointer):对象存储在堆中 对象头中有一个指针指向方法区内的类元信息(具体代码)(类元信息抽象的对象也是放在堆内,以便Java开发调用 如 类名、方法名等信息,JVM操作类元信息则直接操作方法区)
- 数组长度:数组对象使用
指针压缩
虽然64位os的内存地址是64位的,但是64位其实远远超过目前计算机的物理内存大小,所以实际64位表示的内存地址中存在大量无用的位数,所以可以进行压缩,jvm采用的是把64位的内存地址压缩成32位(4字节),在存储的时候使用32位的地址缩小存储容量,使用的时候解码成64位的真实地址去内存中查找
基于这个则
- 堆内存分配小于4G以下,其实不需要启用指针压缩,jvm会直接抹除高32位的地址(区别于不使用指针压缩 还大于4个G内存的场景)
- 堆内存大于32G时候,指针压缩算法失效,从而引发性能下降
不启用指针压缩
对象的内存占用会多出1.5倍左右,会影响GC效率
执行<init>方法
这个方法是JVM虚拟机执行 c++中的<init>方法,
- 给对象中的成员变量真正的赋值
- 执行对象的构造方法
64位机器中对象的内容
没有成员变量的对象
- 标记字段8字节
- klass point:4字节(压缩)
- 对齐填充(12字节不够8的倍数所以补充4个字节)
数组对象
- 标记字段8字节
- klass point:4字节(压缩)
- 数组长度4字节
- 没有对齐填充(因为已经够8的倍数)
含有成员变量的对象
- 标记字段8字节
- klass word 4字节
- 每个成员变量(字段)占用空间(4的倍数)
3.1 int 4字节、byte 1字节(补充3字节)、String 4字节、对象类型4字节(地址指针::压缩) - 对其字节 (不够8的倍数则补充 够了算了)
对象内存分配详细分析
详细流程
- 判断是否能栈上分配*
- 如果不能栈上分配则分配到堆上
- 判断是否能存放入Eden区
- 如果不能则直接分配到老年代(大对象)
- 如果可以则尝试TLAB 分配内存
判断是否能栈上分配
对对象进行逃逸分析
逃逸分析是指 对象是否只是在方法内创建和使用,如果返回出去则说明对象逃逸
如果没有逃逸的对象就可以尝试栈上分配具体是栈帧内部
但是栈帧一般容量非常小,无法提供存储对象的连续的内存空间
如果逃逸分析后不逃逸 会接着分析栈帧内部剩余空间还能都存储下对象的成员变量 如果可以,则把这些成员变量分开存在栈帧内存空间中(可不连续) 并不在创建真实的对象了 这就是 标量替换
好处:随着方法调用结束栈帧销毁的时候直接销毁栈内对象则减小GC压力
大对象
大对象可以直接进入老年代,并且超过多少大小算是大对象在Serial和ParNew两个垃圾收集器下是可以指定的 -XX:PertenureSizeThreahold=1000(单位字节)
为什么要设置这个机制?
因为大对象的区域间复制消耗比较大
分代年龄
对象的分代年里是放在对象头里边的4字节表示 所以最大只能到15代
如果系统中大量对象要么很快销毁 要么很慢销毁的时候可以尝试减少进入老年代的分代年龄 让很慢销毁的提前进入老年代 节约年轻代的空间减少GC
对象动态年龄判断
如果young GC 后 Survivor区域使用的超过总容量的 5%则出发此判断
计算保存的对象中 年龄从1,2,3,4…n 的容量依次累加直到第一次超过总容量的50%为止,则把 年龄在n和大于n的对象直接挪到老年代
老年代空间分配担保
yuong gc前会判断
年轻代中所有对象的大小是否大于老年代剩余空间
如果可以 则直接 yuong GC
如果不能 则检查担保参数: -XX:HandlePromotionFailure
如果没有配置则 full GC
如果配置了 则判断老年代剩余空间是否小于每次挪入老年代的平均大小
如果 小于平均值则 full gc 完成后再做 young GC
如果 大于平均值则 young GC
也就是说 打开了担保 有可能先做full gc 再做 young GC
估算内存空间模型
根据目标的调用量分析单台机器1秒内承接多少请求
- 元空间
因为原空间存放代码所以根据代码量的大小可以多设置一些512M 避免频繁GC - 堆
2.1 评估对象占用内存大小
在上边分析对象内容哪里可以看出来 一个对象的大小和其包含的字段数量相关
每个字段占用4字节 对象头12字节 数组类型再加4字节 不够8的倍数补齐
根据这个规则大致评估每个对象占用多少内存
但是一般线上的应用比较复杂还是建议使用监控工具分析出每秒多少对象创建
对象的内存的回收
full GC
会回收堆 和 元空间
确定一个对象是否不能被回收
- 引用计数器算法
对象被引用则 计数 +1
不在引用则 计数-1
存在问题:互相引用的时候容易漏计算 - 可达性分析算法
引用类型
非强引用再GC时 如果还是空间不足 则直接释放
- 强引用
new 出来的对象 - 软引用
new 出来的对象放在 SoftReference对象中 再被引用
可有可无的对象就可以使用软引用 如上一次访问的缓存
finalize()方法
在某些GC处理器的会标记 不可达对象
这个时候会给对象标记一次是否实现 finalize()(过滤)
然后再把这些对象标记一次执行finalize()(执行)
再下一次GC的时候
判断一个类时无用的类
方法区回收的是无用的类
同时满足3个条件
- 该类的所有的对象实例都被回收
- 加载该类的ClassLoader已经被回收
这个条件比较苛刻,应该可以认为只有自定义加载器会给回收
ClassLoader被引用且包含的所有类都已经被回收才能回收ClassLoader - 对应的java.lang.Class对象没有被引用,无法通过反射访问该类的方法
内存泄漏
注意如果对象不再使用应该及时淘汰,比如一直往Map里塞东西作为JVM级别的缓存,导致越来越大且JVM无法清理,最终导致频繁Full GC
常量池
Sting常量池
String 类型对象在创建的时候会先去常量池寻找是否存在对应值已有的对象
有则直接返回 没有则创建字面量对象 没有则创建后存入常量池
具体是 字面量对比 比如下边的 “ABC”
String s1 = “ABC”
但是
new String(“ABC”)
先判断字面量 “ABC” 常量池有没有 有了直接返回
没有则创建 存入常量池
但是 new String 会在堆中再次创建一个对象 返回引用
可以使用 String 的 interln 方法拿到常量池中的对象引用
两个基本上不是一个对象
但是如果常量池中没有则把堆中的对象引用放在常量池中
什么样的情况下会没有呢 比如两个 new String 相加后
String s1 = new String(“ABC”) + new String(“DEF”);
//此时 调用 s1的interln就触发没有的逻辑
String s2 = si.interln;
1.6之前 放在方法区
1.7开始 放在堆中 开辟的 常量池中
基本数据类型包装类的对象池
包装类都在创建的时候缓存了一部分对象 详见 valueOf()
这个是代码层面的缓存 和 String的常量池 不一样
作用一样
参考资料
对象头源码级别分析:https://www.cnblogs.com/zgq7/p/16250149.html
对象实例字段分析:https://blog.csdn.net/weixin_45203607/article/details/126055516