只要一个对象,被其他变量所引用,引用计数就加一。简单说,这个对象被引用几次,引用计数就是几,没有引用就是0。
弊端:
循环引用问题:
这两个对象,都已经没有了其他引用。但是两个相互引用。那么这两个对象都不会被回收(各自引用计数都是1),导致内存泄露现象出现
首先确定一系列根对象(肯定不能当成垃圾回收的对象----根对象)
在垃圾回收之前,对堆内存中所有对象进行扫描。看对象是不是被 根对象 直接或者间接的引用。如果是,那么就不回收,如果不是,那么就回收。
在当前活动线程执行过程中,局部变量所引用的对象是根对象。当局部变量不再引用,那么此时不再是根对象,可以被回收
市面上面试常问四种,视频老师认为是五种。(常用类型)
实线:代表强引用
设置内存大小为20m的情况下: SoftReference 对象就是软引用对象
这儿我们可以看出,软引用对象并没有被移除。只是软引用 所引用的byte数组对象被回收了。
使用引用队列的软引用----------------------------
结果显示,软引用对象也被回收了。
为什么会使用引用队列:软引用,弱引用,自身也要占用内存,如果要对它们占用的内存进行释放,那么就要借助引用队列。因为它俩还有可能被其他强引用 引用着,只能通过引用队列进行释放内存。
将循环改为10次后测试:
虚引用(直接内存地址)
必须配合引用队列使用。前面软,弱引用可以配合引用队列也可以不配合引用队列。
简单说:在我们虚引用 引用的对象被回收时,虚引用对象就会自动进入引用队列,另一个线程调用虚引用的方法,间接回收直接内存。(虚引用典型用法)
终结器引用
所有对象都会继承Object对象,Object里面有一个 finalize() 方法。
当A4对象不被强引用。并且重写了 finalize 方法,那么就A4对象就可以被回收。
回收步骤:先把终结期引用对象放入引用队列中,然后等一个优先级很低很低的线程 finalizeHandler ,找到A4对象调用finalize方法。调用之后再去回收,终结期引用对象和A4对象。不推荐这种方法引用。
finalize 方法功效效率很低,要很久可能才会被调用。
标记阶段:扫描整个堆内存,只要没有GC Root 直接间接的引用,就可以当做垃圾进行回收
清除阶段:释放资源(这个就涉及计算机底层内存空间管理了)
优点:速度快
缺点:容易产生内存碎片
标记阶段:和标记清除一样的
整理阶段:它会将多个空闲碎片整合成一个大的空闲空间。
优点:没有内存碎片
缺点:整理过程中,涉及对象移动,效率较低。
将内存划分为两个区域。
将FROM区域不回收的对象标记出来,复制到另一个空闲的内存区域TO,进行连续存放。并且把FROM区域全部清空。
FROM清空之后,再更改区域定义,TO永远为空闲的那个内存区域。
优点:不会产生内存碎片
缺点:会占用双倍的内存空间
这三种情况,根据实际情况综合使用
整个堆内存 综的 划分为 两块
长时间使用的对象,放在老年代。用完就丢的对象,放在新生代中。
生命周期长的,和生命周期短的
针对对象不同的生命周期特点,使用不同的垃圾回收机制。例如,新生代的垃圾回收频繁一些,老年代垃圾回收频率低一些。
不同区域采用不同算法,这样效率更高。
分析一下:垃圾回收基本流程
创建一个新的对象时,默认就会采用 伊甸园 内存中。当 伊甸园 空间快满了。就会触发一次垃圾回收。这个垃圾回收被称为Minor GC(新生代的垃圾回收),动作小的垃圾回收。
采用复制算法,将存活对象复制到 TO 区域中。让幸存对象的寿命 +1。然后FROM 和 TO 交换定义位置。
伊甸园空间充足了,继续向里添加对象,当快满的时候,将伊甸园存活的对象和 幸存区From区域的继续存活的对象标记出来。复制到TO区域中。
那么, 从伊甸园新进入的对象寿命+1,幸存区中继续存活的对象 寿命 +1变为2。然后清空伊甸园区域。FROM 和 TO 交换定义区域。
幸存区FROM中的寿命,超过预设值(最大15),那么将该对象 晋升到 老年代中。
当老年代区域都快满了,先触发Minor GC,回收之后空间仍不足,则触发一次 Full GC ,将新生代和老年代整个进行清理。
要是full GC之后还是不够,则报错 OutOfMemeryError
垃圾回收过程中,全部用户线程暂停。因为涉及对象位置迁移。暂停时间:Full GC > Minor GC
老年代采用标记整理算法。
当你存储的对象过大时,会直接晋升到老年代,并且不会触发垃圾回收
垃圾回收时:当新生代内存紧张时,也不会遵从阈值,将部分对象晋升到老年代。
多线程时,当一个线程内存溢出,不会影响java整个进程的结束,只会是这个线程死了(那么这个线程的所有资源进行释放)。
堆初始大小-----------------> -Xms // -Xms20M
堆最大大小-----------------> -Xmx 或 -XX:MaxHeapSize=size
新生代大小-----------------> -Xmn 初始和最大同时指定 或 ( -XX:NewSize=size / -XX:MaxNewSize=size 初始大小 / 最大大小)
幸存区比例-----------------> 动态, -XX:InitialSurvivorRatio=ratio 初始比例 和 -XX:+UserAdaptiveSizePolicy 打开动态
幸存区比例-----------------> -XX:SurvivorRation=ratio // 默认比例为8,比如10M,Eden为8,from 和 to 各1
晋升阈值 -----------------> -XX:MaxTenuringThreshold=大小
晋升详情 -----------------> -XX:+PrintTenuringDistribution // 打印晋升详情
GC详情 -----------------> -XX:+PrintGCDetails -verbose:gc
Full GC 前 Minor GC -----------------> -XX:+ScavengeBeforeFullGC // Full GC之前,先做Minor GC,默认打开的
底层是一个单线程的垃圾回收器,在垃圾回收时其他线程都暂停。
使用场景:堆内存较小,适合个人电脑(CPU核数少的)。
开启串行垃圾回收: -XX:+UseSerialGC=Serial+SerialOld
Serial :工作在新生代 (复制算法) SerialOld:工作在老年代(标记整理)
多线程,适合堆内存较大,需要多核CPU支持
单位时间内,STW时间最短 例如:一小时内发生两次垃圾回收,每次0.2s。多量少次
开启垃圾回收: -XX:+UseParallelGC 新生代(复制) -XX:+UseParallelOldGC 老年代(标记整理)
1.8 默认使用这种回收器,只需要开启一个,另一个自动开启。
-XX:ParallelGCThreads = n // 设置参与垃圾回收的线程数
-XX:+UseAdaptiveSizePolicy // 开启之后,ParallelGC会动态的去调整,新生代的eden区域和survivor区域的比例,包括整个堆的大小,晋升阈值也会受影响
-XX:GCTimeRatio=n // 调整吞吐量的,默认99
有一个计算公式, 1 / (1+ratio) ,如果采用默认,那么等式 = 0.01,意思说:你垃圾回收的时间不能超过总时间的 0.01,比如100分钟之内,只能用1分钟用于垃圾回收,如果达不到这个目标, Parallel GC就会调整堆内存大小,使之达到这个目标。一般是把堆增大,垃圾回收就不会这么频繁,那么总时间增大,在120分钟的时候触发了垃圾回收,单次垃圾回收吞吐量增大(1.2分钟用于垃圾回收)。
-XX:MaxGCPauseMillis=ms // 最大暂停ms数,默认200ms
单次垃圾回收时间最多 200ms,和上面相悖。
垃圾回收器会开启多个线程。
多线程,适合堆内存较大,需要多核CPU支持
注重在尽可能让 单次STW时间最短 例如:一小时内发生 5次垃圾回收,每次 0.1s。多次少量
开启垃圾回收器:
-XX:+UseConcMarkSweepGC (老年代,标记清除,垃圾回收和用户线程可以并发执行,部分阶段,如果并发失败,则退化为 SeriaOld 退化为单线程的 标记整理)
-XX:+UseParNewGC (新生代,复制算法)
工作流程:
老年代内存不足,发生GC。
1、初始标记的时候,还是STW,初始标记很快,只会标记根对象(不会挨个扫描标记)。
2、和用户线程并发执行,根据根对象把其他垃圾线程找出来。
3、重新标记,需要STW,并发标记同时,用户线程可能产生新的对象,对其他对象改变引用。会对垃圾回收造成干扰。所以需要重新标记工作
4、并发清理和用户线程并发执行
细节问题:
-XX:ParallelGCThread=n //并行垃圾回收线程数设置,一般和cpu核数一致
-XX:ConcGCThreads=n // 初始标记,并发标记 线程数设置,一般设置为上面参数的 1/4
在执行并发清理的时候,可能产生新的垃圾,那么在清理的时无法把这些新垃圾清理掉,只能等下次垃圾回收。这些垃圾叫互动垃圾
所以我们需要预留空间保留这些 互动垃圾
-XX:CMSInitiatingOccupancyFraction=65% // 用于设置 老年代 内存占用达到多少,触发垃圾回收,剩余空间就使用来存互动垃圾
-XX:+CMSScavengBeforeRemark // 新生代可能会引用一些老年代的对象,那么我们需要进行一次扫描,而且新生代的对象还有可能是垃圾对象,那么我们扫描就是做无用功。这个参数就是在 重新标记之前对新生代进行一次垃圾回收。减少重新标记压力
特点:使用的是标记清除,容易产生垃圾碎片,在碎片过多情况下。一个大对象来了,新生代和老年代都放不下。这样就会造成并发失败。这样垃圾回收就会退化 SerialOld(单线程的标记整理),这样垃圾回收时间一下就会增高很多!!!这是存在的最大问题!!!
这个不重要,想看就看吧
这个算法好复杂,我晕了。。。。
https://www.bilibili.com/video/BV1yE411Z7AP?p=71&vd_source=f14d0ab6a0fab741200e54ed49c444e6