一、对象创建
1.1 符号引用
new 创建一个对象,需要在JVM创建对象。
符号引用:目标对象采用一个符号表示,类A加载的时候,如果成员变量类B还没有被加载进来,采用一个符号(字面量)来表示,这种引用就称为符号引用。
直接引用:真实地址。
检查加载的时候,检查类B是否加载,已加载的话,将符号引用修改为直接引用。
1.2 JVM创建对象过程
1)检查加载,new指令创建对象,首先需要检查对象对应的Class类是否已经加载。未加载需要先加载类。
2)分配内存:在堆空间划出一块确定的内存,分配给对象。因为JAVA支持多线程,分配对象的需要考虑并发安全性。
3)内存空间初始化:对象创建以后,成员初始值赋"零"值。
4)设置:
5)对象初始化
1.3 划分内存的方式
划分内存有两种方式:指针碰撞和空闲列表。
指针碰撞:如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"。
指针碰撞适用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
新生代通常使用指针碰撞进行内存分配。
空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"。
1.3.1 比较
指针碰撞效率比较高,采用指针碰撞的决定性因素:堆空间是否规整。
1.4 线程并发对象创建安全
Java支持多线程并发,因此划分内存分配对象的时候,一定存在并发安全问题。
CAS
线程分配内存的时候,首先查询空闲位置,然后通过CAS操作,采用CAS操作向空闲内存位置申请 对应对象大小的空间,如果申请成功,则分配成功;如果申请失败,则说明此空闲位置已经被别的线程占据,此位置已经不是空闲位置。继续上面的操作,查找下一个可用的空闲位置进行分配。
CAS是一种无锁机制,CPU指令,原子指令。
TLAB
指本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
TLAB的目的是在为新对象分配内存空间时,让每个Java应用线程能使用自己专属的分配指针来分配空间,减少同步开销。
TLAB只是让每个线程拥有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB。空间换时间
开启参数 -XX+UseTLAB
1.5 对象的内存空间分布
对象在内存中的分配,包括 对象头(MarkWork 8字节+ 类型指针4字节),实例数据(对象成员)+对齐位。
1.5.1 对齐填充
对象要求是8个字节的整数倍, 当对象分配空间不足8的倍数时,自动填充补齐。
1.5.2 数组对象的空间分布
数组对象:对象头(MarkWord 8字节+类型指针4字节)+数组长度(4字节)+对齐位。
数组对象与普通对象相比,多了一个数组长度的字段,标记数组长度。
1.6 对象的访问定位
1.6.1 使用句柄访问对象
引用存储的是一个地址,该地址是句柄的地址,而句柄是一种结构,分别存储 实例指针和类型指针 这两种指针,(实例指针是指向堆中的对象实例,而类型指针指向的是在方法区中该对象所属类型)。当要访问对象时,先通过引用访问句柄,再通过句柄访问对象实例以及对象类型信息。句柄是存储在堆中的,如果使用这种方式,那么就会从堆中分出一块内存用作句柄池。
1.6.2 使用直接指针访问对象
引用存储的是对象实例在堆中的地址,通过引用可以直接访问对象实例。
比较
1)直接指针访问对象的优点:效率高,一次就可以找到对象。缺点是垃圾收集器移动对象时需要修改引用,因为垃圾回收涉及对象移动,对象的实际地址会有变化。
2)句柄访问模式的优点:对象经过多次移动时,虚拟机只需要修改句柄中的指向对象实例的指针即可,不用修改引用。垃圾回收时效率高。缺点是需要额外维护对象池,访问对象效率低,需要两个步骤才能得到对象实例。
目前虚拟机主要使用的直接指针访问的方式。因为访问对象的频率要远高于垃圾回收的频率。
1.7 对象存活判断
在JVM中,对象是自动化的回收机制,用户只需要管对象的创建,不要手动去释放对象,JVM垃圾收集器负责判断哪些对象是垃圾,并且对垃圾对象的回收。
1.7.1 垃圾对象判断算法
1.7.1.1 引用计数法
引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。引用数为0时,说明对象没有被其他对象引用,可回收。
实现方式:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是可被回收的对象。
优点:简单、高效
缺点:很难处理循环引用,相互引用的两个对象则无法释放。因此目前主流的Java虚拟机都摒弃掉了这种算法。
python使用的引用计数法。对于循环引用怎么解决?通过开启一个额外线程检测是否存在循环应用,村换引用的对象特殊处理。
1.7.1.2 可达性分析算法
又叫根可达算法,从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。引用树上的节点,都是可达的,需要存活。
1.7.1.2.1 GC Roots
GC Roots包括四类:静态变量,虚拟机栈变量,常量池,JNI指针(JNI创建的对象)
这种情况下,即使存在循环引用,但是不在引用树中,可以回收。
除了这四种GC Roots,还存在其他的吗?
内部引用:Class对象、异常对象、类加载器
内部锁:synchronized对象
内部对象:JMXBean
临时对象:跨代引用
1.8 Class对象回收的条件
Class对象回收的条件比较苛刻。满足所有的条件:
1)Class new出的对象都要被回收掉;
2)对应的类加载也要被回收;
3)没有通过反射使用Class类
4)没有 up-level 依赖的类(即正在加载的类引用了正在初始化的超类或接口)。没有被其他Class类引用。
5)参数控制允许回收Class对象。
-Xnoclassgc: 应用类GC
当您在启动时指定-Xnoclassgc时,应用程序中的类对象在GC期间将保持不变,并且将始终被视为活动的。这可能会导致更多的内存被永久占用,如果不小心使用,会引发内存不足异常。
1.9 对象的自我拯救Finalize
1.10 各种引用
强引用:存在引用时,不会被会受到,即使内存不足时,会抛出OutOfMemary
软引用:内存不足时可以被回收掉
弱引用:只要执行GC就可以被回收掉
虚引用:随时被回收掉。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
二、对象分配原则
几乎所有的对象都在堆中分配。
2.1 栈上对象分配
两个条件:热点代码+逃逸分析
2.1.1 逃逸分析
对象的存活声明周期能不能逃出这个方法。
分配在栈中的对象,不能被其他线程共享。
开启开关: -XX:+DoEscapeAnalysis
启用逃逸分析。默认是开启的。只有Java HotSpot Server VM支持这个选项。
2.2 大对象直接进入老年代
只有Serial和ParNew这两款垃圾收集器才生效。
启动条件:-XX:PretenureSizeThreshold=4m,
new的对象超过4M直接进入老年代。
2.3 长期存活的队形进入老年代
对象在新生代创建以后,结果一次垃圾收集,仍然存活的对象,age=age+1,年龄增长1岁。当年龄达到15时,此对象需要进阶老年代。
允许设置最大年龄,不能超过15岁。
-XX:MaxTenuringThreshold=threshold
2.4 动态年龄判断
最大年龄太小的话,导致对象不能尽可能的回收,快速进入老年代,导致老年代臃肿;
年龄太大的话,长期存活的对象反复进行回收检测,浪费效率;
当前存放对象的Surnvivor区域里(其中一块区域,存放对象的那块s区),一批对象的总大小大于这块Sunvivor区域内存大小的50%(由-XX:TargetSurvivorRatio
参数指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。
这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。
对象动态年龄判断机制一般是在minor gc之后触发的。
2.5 空间分配担保
为什么要设置老年代空间分配担保机制?
- 空间担保分配是指在发生Minor GC之前
- 只要老年代的连续空间 大于 新生代对象总大小或者历次晋升的平均大小 就会进行Minor GC,否则将进行Full GC。
内存分配是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是**>=Eden区大小的一半**,如果是那么直接把该对象放入老生代,否则才会启动担保机制。
2.6 分代垃圾收
2.6.1 什么是垃圾回收?
JAVA特有功能,垃圾收集意味着程序不再需要的对象是无用信息,这些信息将被丢弃回收。
2.6.2 分代垃圾回收
MinorGC/YoungGC: 新生代垃圾回收
MajorGC: 老年代垃圾回收,只有CMS
FullFC: 堆区域全部垃圾回收(新生代和老年代)
2.7 垃圾回收算法
垃圾回收有三种主要算法:复制算法,
2.7.1 复制算法
原理:把内存空间一份为二,一半用于保存对象,一半用于预留空间。当进行垃圾回收时,将所有存活对象从工作空间复制到预留空间。复制完成后,对工作空间整体清理。
新生代绝大部分对象都是朝生夕死。
优化复制算法,划分Eden区。Eden区:S1:S2=8:1:1,空间利用率能够达到90%。
加强版的复制算法。
2.7.2 标记清除算法
原理:采用可达性分析算法,标记存货对象,清理掉垃圾对象。这种清理算法会产生不连续的空闲内存区域。
2.7.3 标记-整理算法
原理:采用可达性分析算法标记存活对象,对存活对象进行整理,然后清理垃圾对象。
对象移动后,哈希值变化吗?
会变化。