JVM_02:对象的分配及垃圾回收机制
-
JVM中对象的创建过程:
-
检查加载:将类加载到JVM的运行时数据区
- 没有加载就加载
-
分配内存
- 划分内存的方式
- 并发安全问题(Java的多线程背景)
-
初始化
- 置零处理
-
设置
- 对象头
-
对象初始化
- 构造方法
-
-
教案图示:对象的创建过程
-
类加载:
- new出对象后,将其放到 JVM运行时数据区中的方法区
-
检查加载
-
检查类是否已经加载过
- 没有:重新加载
-
检查符号引用能否变成直接引用:如果不行,那么就回退到前面的初始化,解析
-
直接引用:真实的地址(这个地址是操作系统提供的)
-
符号引用:不知道真实的地址
-
存在于常量池中:
-
这个其实就是一个字面量
-
应用场景:
在加载类A的时候,A的常量池对类B进行了一次引用;但是,此时类B尚未加载进来,那么久使用一个符号来指定类B;本质上还是一个引用
-
-
-
-
分配内存:
-
new出一个对象后,需要为其分配内存;就是在堆中指定一块区域用于存储(实质上,对象不一定百分百在堆上进行分配)
-
由于 Java的多线程背景,存在并发冲突控制与内存分配两个问题
-
解决并发冲突:启动多个线程,进行内存分配
-
解决办法:
- CAS+失败重试
- TLAB(本地线程分配缓冲)
-
为什么会造成并发安全问题?
- 堆是线程共享的
- 两个线程都想在同一块内存区域上进行划分(分配对象),造成覆盖
-
-
解决内存分配问题:取决于堆空间是否规整
-
指针碰撞:挨着分配,指针位于上一次分配的末尾
-
适用对象:堆空间规整,效率高(不需要查表),采用指针分配模式;每次分配均会产生一个偏移量
-
堆空间图示:划了三个出去
-
分配前:
-
分配后
-
-
-
空闲列表:记录每块空间是否空闲,在为对象分配空间的时候,找一段连续的给他
-
处理空间碎片(零散的可用空间),适用于堆空间零散,这个有点慢,要去查个表 什么表?
-
堆空间图示:
-
-
-
-
CAS+失败重试机制:乐观的,不加锁的机制;但是实际上还是有锁
-
核心:比较交换
-
执行流程:线程中对其即将访问的对象旧状态事先进行了进行保存,等待触发。当时机成熟时,线程对对象进行访问(将线程中保存的对象状态的旧值与此时对象的状态值相比较)
-
相同:访问成功并修改对象的状态值(将空闲变成繁忙),这次操作就是CAS(比较交换),类似于精子与卵细胞的融合过程
-
不同:访问失败,重试(已经有线程占据这个对象了,因为对象一般保存在堆中,而堆属于线程共享区域,所以不同的线程可以进行访问)
- 再次访问就行
-
这个过程就叫CAS;
-
-
CAS在底层上算作CPU指令,即为原子操作,即保证了线程安全;这个相当于自带了一个锁;对多核CPU来说,有个叫Look Modify(复合指令)的东西也可以保证只有一个CPU
-
CAS:每个对象只要被线程访问就都有个这个,类似于朴素版Dijjistra算法中的时间复杂度估计,这个是从整体角度进行分析的,虽然这个是CPU指令(快),但,从整体上看,就慢下来了;
-
Java由于并发编程背景,本质上是不推荐加锁的,加了影响效率,相当于在同一时刻只有一个线程在执行了(其他的线程会空转),但是如果不加以控制,会导致异步问题;(引入TLAB)
-
-
TLAB(本地线程缓冲机制):为每一个线程分配一个线程缓冲,空间换时间
- 实现原理:一个新的对象创建时,一般创建在Eden区;那么就在Eden区中给这个线程分配一块空间(Eden区的百分之一),拿给这个线程去分配对象;那么,当又来了一个线程,那就再给它划出一块区域,让新线程在这个空间里面分配对象;这样,在线程分配对象的时候各自在Eden区中的独立部分就解决了并发冲突的问题
- JVM默认解决并发访问冲突手段,但是这个是可以关掉的,关了就用CAS+重试;Java8中有个UseTLAB选项(默认开启),默认使用TLAB,关掉就用CAS+重试
-
内存空间初始化
- 成员变量(基本都是零值):基本数据类型---->0/false
- 引用数据类型---->null
- 对数据均有一个初始值,为了在 Java中可以直接进行使用
-
设置:主要就是设置对象头
-
对象的内存布局示意图
-
对象头
-
Mark word:存储在运行时对象自身的东西(数组与非数组都有)
- 哈希码:任何一个对象都有HashCode
- GC分代年龄:在GC中会用这个
- 锁状态特征:加锁的问题
- 线程私有的锁
- 偏向锁ID
- 偏向时间戳
-
类型指针(只有数组才有)
- 指向方法区中的类,(对象需要知道它属于那个类)
- 源码分析:JVM/openjdk/hotspot/src/share/vm/oops/markopp.hpp(这个里面就有详细的类型问题)
-
对象数组:记录数据长度的数据(这个东西只针对于对象中含有数组)
-
-
实例数据
-
对象填充(并不是必须的)
- (保证对象头+实例数据+[对象填充])%8个字节 = 整数
- 为什么是8个字节嘛,猜测 Java中广泛采用Unicode编码
-
-
对象初始化:
- 根据类的构造方法对对象进行初始化
- 到这里对象的创建过程结束
-
虚拟机栈的内容
-
栈帧
-
局部变量表
- 八大基本数据类型
- 对象的引用,指向对象(直接引用)
- 返回地址
-
操作数栈
-
动态链接
-
完成出口
-
-
-
对象的访问定位
-
示意图:
-
直接引用
- 虚拟机栈中的引用直接指向堆中对象
-
使用句柄:句柄池
-
原理:Java栈本地变量表中存放的引用指向 Java堆中的句柄池中的一条数据,这条数据再指向对象的实例数据;做了一个二次转换
-
句柄池中的内容(到对象实例数据的指针,到对象类型数据的指针),其中到对象实例数据的指针再指向堆中对象实例数据,到对象类型数据的指针,指向方法区中的对象类型数据;存在重定位机制
-
优点:
- 仅需修改句柄池中的句柄,无需修改具体数据即可达到栈指向的东西更新;
- 在gc中,对对象的处理也有好处,此时不要需要做具体的修改操作,只要修改句柄了;
-
缺点:
- 由于有二次转换,所以效率会比较低
-
-
直接指针:引用里面保存的就是堆空间中对象的地址,主流方式
-
原理:Java栈本地变量表中存放的引用变量指向 Java堆中的实例数据与到对象类型数据的指针(后者包含在前者之中),其中到对象类型数据的指针进而指向方法区中的对象类型数据
-
优点:
- 快,没有二次转换
-
缺点:
- 当对象发生改变(被修改,被删除)具体的线程里面的东西也要进行更新
-
大多数虚拟机采用这项技术,Hotspot
-
-
为什么方法区总是被指向的?
- 因为在对象头中有类型指针,说明了这个对象属于哪个类;类是放在方法区的
-
-
各种语言的垃圾问题
- java:自动化的垃圾回收
-
c:malloc与free
- c++:new与delete
-
手动化的垃圾回收会带来什么问题?
- 忘记回收,导致内存泄漏
- 在多线程背景下,可能重复回收
-
判断对象是否存活
-
示意图:
-
引用计数算法
-
原理:在对象头上给他加上一个数,当有引用指向此对象时,这个数就加一。有几个就加几个,当引用失效时就减掉这个引用,引用为零就是垃圾
-
Python中就是用的这个
-
问题:无法处理循环引用这种垃圾,两个对象互相进行引用;这个算垃圾啊,因为这样的对象,无法进行利用啊,总不能直接去干到人家的地址里面去嘛;
-
Python处理循环引用问题
- 再额外启动一个线程,专门去扫这种循环引用,看到了就干掉它;
- 其实这样效率会很低,将问题复杂化
-
-
可达性分析(根可达)
-
-
可达性分析(根可达)
-
什么是根?是一系列根的集合
-
GC Roots:Garbage Collection RootSet,跟垃圾回收有关的根
- 静态变量
- 线程栈变量
- 常量池
- JNI(指针),这个是用来处理本地方法的
- 内部引用:class对象,异常对象(Exception),类加载器,
- 同步锁:synchronized修饰的对象也算根
- 内部对象:JMX中的Bean,本地缓存,回调带来的东西,
- 临时对象:跨代引用(在分代年龄哪里),这个在gc中也可以作为根
-
-
原理:从根出发去找对象,那种不可达的就是垃圾,从根本上解决了循环引用问题
-
-
证明可达性分析算法解决了循环引用问题
-
代码:
-
打印GC日志,VMpotion:-XX:+PrintGC
-
开启垃圾回收参数:-XX:+PrintGC 打印垃圾回收日志,这个东西默认是关闭的
当执行这段程序并开启打印垃圾回收日志后,可以看到回收了近20M的数据,这个就说明了JVM采用的是根可达,垃圾回收
-
System.gc() :
- 手动进行gc
- 一般情况下是等到空间满了,它自己会进行gc,
-
-
JVM在空间满了,才进行垃圾回收
- 垃圾回收会影响效率,手动触发可以强制GC
-
JVM不只是回收堆,其他的也会
-
-
根可不可以回收?:Class回收条件(静态变量和常量会跟着Class回收一起走,因为他们都是在类加载的时候同步创建的):都要满足才能进行类对象的回收
-
由这个Class new 出来的对象全部都要被回收掉:因为对象会指向类
-
类加载器也要被回收掉:类是从类加载器哪里来的
-
类,Java.lang,class对象,在任何地方都没有被引用
-
在任何地方都没有被引用,并且不能通过反射调用类中的方法
-
参数控制问题:
-Xnoclassgc:禁用类的垃圾回收,这个参数需要关掉才能GC,,这可以节省部分原有的GC时间,从而缩短应用程序运行期间的中断
在启动指定时,应用程序中的类对象在GC期间保持不变,并始终被认为时活动的,这有可能导致更多的内存被永久占用,若不慎启用,极易造成内存不足从而引发异常情况
方法区的GC条件严苛,所以GC一般都是在堆中的
-
-
Finalize:进行筛选(一个对象是不是垃圾要进行两次筛选,根可达为第一次,Finalize是第二次)
-
执行级别比较低,并且这个方法只能执行一次(用了一次以后,再将对象=null,即使主线程休眠,这个方法也不会再执行了)
-
为什么优先级别比较低?
-
重写这个方法时(在里面写点代码,不能空)
-
那么在会在Finalizer的源码中就会new出一个对象,这个对象被压入队列,并且重新启动一个线程进行处理(遍历一个链表)finalize(JVM 内部反射使用的)
/* Invoked by VM */ static void register(Object finalizee) { new Finalizer(finalizee); }
-
-
-
替代:try-catch-finally(在这个地方finalize)
-
缺点:
- 不能很好控制线程的优先级别
- 并且这个方法不能二次使用
-
代码示例:
public class FinalizeGC { //这个是静态的,这个对象在方法区 public static FinalizeGC instance = null; //如果对象还可以调用这个方法,证明对象还活着 public void isAlive(){ System.out.println("I am still alive!"); } //任何一个类都可以覆盖这个方法,因为在Object中有一个 protected void finalize() throws Throwable { } @Override protected void finalize() throws Throwable{//不推荐使用 super.finalize(); System.out.println("finalize method executed"); FinalizeGC.instance = this;//把引用接上 } public static void main(String[] args) throws Throwable { instance = new FinalizeGC(); //对象进行第1次GC instance =null;//将引用置空,那么,这个对象就死掉了,因为没有引用指向它了 System.gc(); Thread.sleep(1000);//Finalizer方法优先级很低,需要等待 if(instance !=null){ instance.isAlive(); }else{ System.out.println("I am dead!"); } } } -
运行截图(主线程休眠):
-
运行截图:将主线程休眠注释掉
-
-
各种引用分析
-
强引用:用 = 来干的
- JVM回收不了这种跟GC Roots有强引用的东西,就是GC Roots直接赋值的那种,
- 干掉强引用:将其置空即可(赋值为null)
-
软引用:
-
虽然与GC Roots具有可达性关系,但是不是直接可达;
-
当系统即将OOM的时候,会考虑干掉软引用,以求避免系统崩溃,有时候就真的只差那么一点;
-
应用场景:在 Java中会将图片保存在缓存中
- 图片占据空间大
- 即使被回收了,依然可以从数据库,云端进行调取
-
补充:手动OOM
- 调整堆的大小(-Xms 20m -Xmx 20m)将堆的默认值与最大值均设置为20m
- 不断向一个List中添加东西
-
代码示例
public class TestSoftRef { //对象 public static class User{ public int id = 0; public String name = ""; public User(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; } } // public static void main(String[] args) { User u = new User(1,"King"); //new是强引用 SoftReference<User> userSoft = new SoftReference<User>(u);//软引用 u = null;//干掉强引用,确保这个实例只有userSoft的软引用 System.out.println(userSoft.get()); //看一下这个对象是否还在 System.gc();//进行一次GC垃圾回收 千万不要写在业务代码中。 System.out.println("After gc"); System.out.println(userSoft.get()); //往堆中填充数据,导致OOM List<byte[]> list = new LinkedList<>(); try { for(int i=0;i<100;i++) { //System.out.println("*************"+userSoft.get()); list.add(new byte[1024*1024*1]); //1M的对象 100m } } catch (Throwable e) { //抛出了OOM异常时打印软引用对象 System.out.println("Exception*************"+userSoft.get()); } } } -
运行结果:软应用消失
-
-
弱引用
-
简单gc即可实现回收,一般来说活不到下一次gc
-
应用场景:
- setLock
- wakedHashMap
-
代码示例
public class TestWeakRef { public static class User{ public int id = 0; public String name = ""; public User(int id, String name) { super(); this.id = id; this.name = name; } @Override public String toString() { return "User [id=" + id + ", name=" + name + "]"; } } public static void main(String[] args) { User u = new User(1,"King"); WeakReference<User> userWeak = new WeakReference<User>(u); u = null;//干掉强引用,确保这个实例只有userWeak的弱引用 System.out.println(userWeak.get()); System.gc();//进行一次GC垃圾回收,千万不要写在业务代码中。 System.out.println("After gc"); System.out.println(userWeak.get()); } }
-
-
虚引用
-
随时都有可能被回收,
-
应用场景:
-
可以在 JVM的内部进行使用虚引用证明gc正常工作:因为gc也是一个线程(是线程就存在死锁,循环,阻塞等问题)
-
在NIO中的DirectByteBuffer(直接分配堆外内存)中的源码就有这个虚引用
- 就有一个cleaner
- 在处理直接内存的时候,使用上者是可以的,毕竟他是openJDK提供的,JVM会进行回收的;内部就是有这个机制的
-
补充知识: 在处理直接内存的时候,使用上者是可以的,毕竟他是openJDK提供的,JVM会进行回收的,但是Unsafe就真的是完全不一样了。
-
-
-
-
对象的分配策略
-
示意图:
-
分配原则:几乎所有的对象都在堆中进行分配(new出的对象:堆中分配或者栈上分配)
-
对象优先在Eden区进行分配,不进行栈上分配
-
空间分配担保
-
大对象直接进入老年代:
-
动态对象年龄判断
-
-
是否走栈上分配:所以对象不只是在堆中分配
-
条件:当代码代码执行很多次,后端中一般是一万多次,就会触发JIT并且经过JVM内置逃逸分析技术检验,两者都符合那么就采用栈上分配
-
逃逸分析:看这个对象能不能逃出这个方法
- 为什么要走逃逸分析:满足逃逸分析,就证明其他的线程调用不到这个代码块;那么就相当于线程私有了,栈就是线程私有的
- 那么就将这个对象拆分,塞到栈里面(如果在堆中分配,那么肯定会执行gc)
-
本地线程缓冲技术
-
-
逃逸分析:只有 Java HotSpot Server VM 支持逃逸分析
- 判断此时这个对象n能否逃出这个方法
- 这个是可以关掉的(Hotspot Service有个参数:-DoEscapeAnalysis,默认开启,关掉就不走栈上分配了)
-
触发JIT(热点数据)
- 在后端中当方法、循环次数达到一万次及以上,此时触发逃逸分析,看能否逃出这个循环,逃不出,就相当于其他的线程就调用不到这个方法;此方法仅供当前线程使用,这样就可以放到栈上面,栈是线程私有的
-
为什么要进行栈上分配?
- 减少gc次数
-
-
大对象直接进入老年代:大于4兆+垃圾回收器特定的两种
- 对垃圾回收器有要求,只能是Serial、Parnew
- 参数设置:-XX:PretenureSizeThreshold=4m(默认值)
- 触发逃逸分析:看能否逃出这个方法,逃不出,其他线程调用不到这个对象;直接分配在栈上
- 将对象拆分放进局部变量中;之前都是解释执行的,现在是热点编译;
- 若将大对象放到新生代,会导致大对象频繁进入老年代,影响执行效率
-
不是大对象,在Eden区进行分配
-
几乎所有的对象都在堆中进行分配(还有栈上分配的情况)
-
堆中存在的分代模型,JVM在回收的时候是分开回收的
- 年轻代:Eden、From、To、
- 老年代:Tenured
-
使用本地线程分配缓冲,进行对象分配
-
-
在Eden区域的对象没有死,长期存活的对象进入老年代
-
对象在一次gc中存活下来,年龄加一,Hotspot最大值为15(CMS垃圾回收器最大值为6),年龄由4位二进制存放(Hotspot中)这个是可以进行设置的
-
参数控制(-XX:MaxTenuringThreshold=阈值)
- 适用于自适应gc大小调整的最大任期阈值,最大为15(4位二进制存储);并行(吞吐量)收集器的默认值为15,CMS默认值为6
-
大于15就进入老年代
-
-
也不一定要等到15,动态对象年龄判断
-
关于对象年龄设置
-
过大:占用新生代过多
设置为15,即经过15次gc后对象才变为老年代
太大了,就一直在新生代中进行分配,知道新生代装不下,还是会放到老年代里面去(这个时候,年龄的设置就没有什么用了)
-
过小:对象不能很好地在新生代中进行gc,导致老年代内卷,员工倒挂;
-
-
为了更好的适应:做了一个适配(from区和to区同时只有一个对象)
- 假设交换区(from区)是10MB,当交换区中age=3的对象有1MB,age=4的有4MB,age=5的有1MB,那么就不找了,直接将age>=3的对象全部晋入老年代
-
-
空间分配担保
-
一般情况下,老年代的绝大部分是从新生代过来的;在回收的时候是分代回收的(新生代--->YGC,老年代--->FGC)
- YGC后老年代可能放不下:进行一次YGC就将FGC清一下;但是这个是不行的,所以就存在空间分配担保
-
空间分配担保实现:
- JVM统计历次老年代回收的空间取出一个平均值,假设为10MB;那么,当YGC释放8MB,那么就不用搞FGC了,直接先过来;要是,不行,那么就再搞一次gc
- 担保,只管分,放不下,调用一次gc就行了
-
分配阈值:统计历次进行老年代回收空间(FGC),取个平均值,这个作为空间偿还能力评估
-
-
-
垃圾回收基础:
-
理论图:FGC肯定伴随YGC
-
GC:垃圾回收,这个就是一个动作
-
新生代+老年代:组成一个堆
-
新生代:Eden,from(s0),to(s1)
- s:幸存者空间
- 8:1:1
- 注意from区与to区,只有一个能用,会预留一个
-
-
-
垃圾回收算法 :
-
复制算法:这个是JVM中效率最高的,只是说会预留空间而已
-
示意图:
-
执行流程:
- 先预留出一半的空间
- 根据可达关系,找出可达的对象,垃圾先不管
- 将对象复制到预留的地方去,对原来的那块空间进行一次格式化,并将其作为新的预留空间
-
特点:
-
实现简单,运行高效
- 对象都是朝生夕死的,垃圾占大多数(因为更新频繁),要复制的较少
-
没有内存碎片:后面去分配对象的时候就可以用指针碰撞(比空闲列表高)
-
但是,空间利用率就比较低,只有50%;那么就引出了Appel式的复制回收
-
-
-
Appel式的复制回收:加强版的复制算法,用于新生代
-
示意图:
-
跟之前的那个的区别就在于,预留区域有两个,各占对的10%,整体上是8:1:1
-
整体利用率在90%
-
-
标记清除算法:处理老年代
-
示意图:
-
执行流程:
- 标记垃圾
- 清除垃圾
-
特点:
-
缺点:位置不连续,会产生内存碎片(对象需要占用连续的空间,碎片多了,就可能导致还有7个单位的可用内存但分配不下一个3个单位的对象)
-
优点:可以做到不暂停
- 在垃圾回收的时候有两个线程:业务线程,垃圾回收线程
- 垃圾对象在业务线程中用不到(因为它是垃圾,用的到,就是根可达的),并且在标记-清除的过程中并不会移动垃圾的位置
-
-
-
标记-整理算法:
-
示意图:
-
执行流程:
- 先标记:哪些不是垃圾
- 接着就整理(移动对象,导致业务线程暂停),将不是垃圾的对象整理成一段连续的
- 将剩下的一次性清掉(整体清除)
- 没有内存碎片
为什么移动对象会导致业务线程暂停?
- 因为在虚拟机栈的局部变量表中有对象的引用,这个引用就指示了对象到底在哪里;对象移动的时候,局部变量表要更新,所以会暂停
-
-
-
基础知识补充:
-
JVM,Java中都是自动化的内存管理
C语言 malloc free C++ new delete//C/C++,忘了就导致内存泄漏;同时在多线程中多次回收可能产生覆盖问题 Java new 自动gc -
Java触发gc,首先判断哪些为垃圾,就是哪些对象存活
-
细节:
- 可达性分析是很快的
- 移动对象就很慢,移动了,hash值就会变
-