JVM内存分配机制
对象的创建
类加载
在new对象时,首先会检查判断类是否被加载,如果未被加载则会先对类进行加载。
内存分配
类被加载完毕时就可以知道该类的对象占用多大的内存空间,那么下一步则是该划分内存了,Java划分内存分为两种,一种是指针碰撞,该方法是利用空闲指针来分配内存的,另一种是空闲列表法,该方法需要维护一个空闲列表。
指针碰撞(默认)
JVM默认是使用指针碰撞法来分配内存的,假设堆空间是工整的,指针指向空闲内存与已分配内存的分界线,因为在类加载完毕后已经可以知道创建对象需要多少内存大小了
只要将指针移动到新对象大小的空闲空间处即可,之后再将前面的空间用来分配新对象就行了。
空闲列表
如果堆空间不是工整的,那么需要空闲列表法来分配内存,空闲列表法需要维护一张表,该表中记录堆空间中的空闲内存情况,当创建对象时,比对空闲列表中的空闲内存,碰到合适的空闲内存分配即可,分配完内存之后,将空闲列表进行修改即可。
并发问题
CAS
采用抢占 + 重试的方式分配内存,多个对象抢占内存,谁抢到算谁的,之后抢占失败的对象则进行重试,重新抢占下一块内存。
本地线程缓冲(TLAB)
在堆内存中分配一块线程专用内存,当线程进行分配时,在那块内存中开辟一块空间即可。
通过XX:+/ UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
初始化
在分配完内存之后会对内存初始化零值,并为成员变量进行赋默认值。
设置对象头
对象头的格式如下所示
在Java虚拟机中,对象在内存中的各部分可以分为对象头、实例数据以及对齐填充,如果对象是数组的话,还有数组长度属性。
对象头分为两部分第一部分为mark word部分,该部分定义了对象的发呢带年龄、锁状态、以及线程id等。另一部分为类型指针部分,该部分指向了对象的class元数据,也就是存储在方法区中的类模板。
分代年龄为4bit,也就是说对象如果躲过2^4 = 16 次轻GC还没有被清除时,那么对象就会进入到老年代。
mark word占在32位的系统中占4字节,在64位系统中占8字节。
klass Pointer开启指针压缩占4字节,关闭指针压缩占8字节。
对齐填充为了保证对象占内存大小能被8整除,这样做可以提高查找效率。
数组长度占4字节。
指针压缩
从JDK1.6开始,在64位系统中支持指针压缩
启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩: XX: UseCompressedOops
堆内存大于32G指针压缩会失效,原因是指针压缩最大寻址为32G,运算公式:首先是指针压缩到了4字节也就是32位,然后是mark word的8个字节,64位
使用指针压缩的好处:
- 大号指针因为被压缩了,所以指针在缓存与内存中移动数据也就效率变高了。
- 内存变大1.5倍,增大了内存存储对象的空间。
- 指针压缩时将指针32位以上的地址压缩到32位,在堆内存中是以32位进行存储的,CPU读写时再解压缩成32位以上再进行使用,增大了存储容量。
开启指针压缩class word占4字节
关闭指针压缩占8字节
执行init方法
在该过程中会对成员变量赋值并调用构造方法。
对象内存分配
对象并不一定存储到堆空间,还有可以存储到栈空间中,JVM通过逃逸分析来分析对象不会被外部访问,如果对象不被外部访问那么就将对象分配在栈空间中,当方法执行完毕后,清空栈内存空间,分配的对象也会一并清除,这样做的好处是减轻了GC压力以及节约了堆空间。
public User getUser() {
User user = new User();
// 巴拉巴拉执行一系列功能
return user;
}
public class Main {
public static void main(String[] args) {
User user = new User();
User user1 = user.getUser();
}
}
如上代码所示,getUser方法是不会通过逃逸分析的,因为getUser方法返回的对象会被调用该方法的地方所引用,作用域是无法确定的,所有无法通过逃逸分析处理。
public void getUser() {
User user = new User();
// 巴拉巴拉执行一系列功能
}
如上代码是可以通过逃逸分析的,因为方法中的对象已经确定在方法的作用域中。
JDK7以后默认开启逃逸分析
标量替换:通过逃逸分析后,如果栈帧空间中没有足够的连续空间来存储对象,那么如果对象是可分解的,JVM将不会创建对象,会将对象的方法以及成员变量分解,存储在非连续的栈帧内存空间中。
标量与聚合量:标量就是不可分解的量,比如基本数据类型int short long等,而与标量相对的是聚合量,聚合量可以进一步分解。
eden 区分配
兑现先在eden区进行分配,当eden区满了,则回进行一次Minor GC,GC过后没被清理的对象则会移动到survivor区,当eden区慢了又会进行一次minor GC,GC清理后的对象则会移动到另一块空的survivor区中。
因为新生代的对象存活时间较短,所有尽量让eden区大点,survivor区够用即可。
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变 化可以设置参数-XX:-UseAdaptiveSizePolicy
以下对象在分配内存时会进入Eden区
public class Main {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024]);
}
}
内存分配如前如下图所示
内存分配后如下图所示
如果minor GC清理完后剩余对象中有对象大小占用内存空间大于survivor区则会直接将对象移动至老年代中
比如我们再分配10M空间到eden区,eden区空间不够用会进行一次minor GC,但是new byte[1024 * 1024 * 10]这个对象是大于survivor区,则会将对象移动到老年代。
public class Main {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024 * 10]);
list.add( new byte[1024 * 1024]);
list.add( new byte[1024 * 1024 * 10]);
}
}
大对象会直接进入老年代
大对象是指占用大量内存空间的对象,在Serial和ParNew这两个垃圾收集器可以使用以下VM参数进行设置大对象的大小,超过设置的大小则会直接进入老年代。
-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC
这样做的好处是,假如大对象是经常需要的,最终会进入老年代的对象,那么直接移动到老年代更好些,频繁的移动大对象耗时会长些,效率也低些,如果是不经常用的用的对象也不建议放到年轻代中,因为年轻代中保存的对象是朝生夕死的,大对象占用空间过大则会频繁出现minor GC,还可能minor GC之后剩余的对象占用空间过大而survivor区不足以存储这些空间,则会直接放入到老年代中,这样做会提前做full GC。
动态年龄判断机制
年轻代进行GC完毕后剩余对象要进入survivor区域,此时会将survivor中的对象的年龄从小加到大,直到大于survivor区域的50%,此时就会把大于等于该年龄的对象直接放入老年代。可以设置使用-XX:MaxTenuringThreshold命令来设置计算时对象占比大小。
老年代空间分配担保机制
在进行轻minor GC之前,会判断老年代剩余空间大小是否大于年轻代所有对象空间大小,如果大于则直接进行minor GC,如果小于则会判断是否配置了-XX:-HandlePromotionFailure了参数,如果未配置则进行full GC,如果配置了则会判断老年代剩余大小是否小于历史每次minor GC进入老年代的空间平均大小,如果小于则进行minor GC,如果大于则进行Full GC。
对象内存回收
引用计数法
在对象中添加一个计数器,每有个对象引用则计数器 + 1,引用失效则 -1, 计数器器为0表示是垃圾对象,则会被清除,但是这种发送因为无法解决循环引用的问题,所有用的比较少。
可达性分析法
可达性分析法会从根节点对象向下搜索对象,将所有被搜索到的对象进行关联,没有被关联的对象则是垃圾对象。
常见引用类型
强引用
最常用的对象引用就是强引用,无论如何都不不会被回收的对象引用,即便内存满了,报OOM了也不会被清除。
User user = new User();
软引用
软也难以一般不会被回收,当GC完毕后,还是释放不出新的对象来,则会清除软引用的对象。
软引用可以用来实现高速缓存。
若引用和虚引用
可能随时被回收,很少用到。
Finalize()方法最终判断对象是否存活
即便可达性分析没有联系到的对象也不会立即被清除,这是会将所有未联系的对象进行标记。如果对象没有覆盖finalize方法,那么对象直接会被回收。
如果对象覆盖了Finalize方法那么就会调用Finalize方法,如果对象成功自救,也就是与其他对象进行联系,那么对象则不会被回收,需要注意的是,finalize方法只会执行一次。
如何判断一个类是无用类
- 当该加载该类的类加载器被回收时。
- 当该类的对象没有在任何地方引用以及没有任何敌法通过反射访问该类的方法。
- 类中没有存在该类的任何实例。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2021-08-15 图片自动审核
2021-08-15 阿里OSS