08-GC

关注的是黄色部分《内存的分配与回收》

image-20200712084539884
  • Java、CPP区别,就在于GC技术和内存动态分配上,C没有GC技术,需要手动的收集
  • GC不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和GC技术的Lisp语言诞生
  • 关于GC有三个经典问题
    • 哪些内存需要回收?
    • 什么时候回收?
    • 如何回收?
  • GC机制是Java的招牌能力,极大地提高了开发效率。如今,GC几乎成为现代语言的标配,即使经过如此长时间的发展,Java的GC机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对GC提出了新的挑战,也是面试的热点

1. 什么是垃圾?

An object is considered garbage when it can no longer be reached from any pointer in the running program.

  • 垃圾:运行程序中没有任何指针指向的Obj
  • 如果不及时对内存中的垃圾进行清理,这些垃圾Obj所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其它Obj使用,甚至可能导致内存溢出

1. 磁盘碎片整理

机械硬盘需要进行磁盘整理,同时还有坏道

image-20200712090848669

2. why_GC

  • 对于高级语言来说,一个基本认知是如果不进行GC,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样
  • 除了释放没用的Obj,GC也可以进行内存碎片整理。以便JVM将整理出的内存分配给新的Obj
  • 随着应用程序所应付的业务越来越庞大、复杂,用户也越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化

2. 早期GC

在早期的C/C++时代,GC基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放

MibBridge *pBridge = new cmBaseGroupBridge();

// 如果注册失败,使用Delete释放该Obj所占内存区域
if (pBridge -> Register(kDestroy) != NO ERROR)
	delete pBridge;




 

这种方式可以灵活控制内存释放时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的疏忽忘记被回收,那么就会产生内存泄漏,垃圾Obj永远无法被清除,随着系统运行时间的不断增长,垃圾Obj所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃

有了GC机制后:

MibBridge *pBridge = new cmBaseGroupBridge();

pBridge -> Register(kDestroy);

现在,除了Java以外,C#、Python、Ruby等语言都使用了自动GC的思想,也是未来发展趋势。可以说,这种自动化的内存分配和GC方式已经成为了现代开发语言必备的标准

3. Java_GC机制

  • Minor GC:针对新生代的GC
  • Major GC:针对老年代的GC,一般老年代触发GC的同时也会触发Minor GC,也就等于触发了Full GC
  • Full GC:新生代 + 老年代同时发生GC
  • 事实上Full GC本身不会先进行Minor GC,可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多Obj都会引用到新生代的Obj,先进行一次Minor GC可以提高老年代GC速度
    • eg:老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC

1. 优点

  • 自动内存管理,无需开发人员手动参与内存分配与回收,更专注于业务开发,降低内存泄漏、内存溢出的风险
    • 没有GCtor,java也会和CPP一样,各种悬垂指针,野指针,泄露问题让开发头疼不已

2. 缺点

  • 对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,严重弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力
  • 此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,才能够在遇见OutOfMemoryError时,快速地根据错误异常日志定位问题和解决问题
  • 当需要排查各种内存溢出、内存泄漏问题时,当GC成为系统达到更高并发量的瓶颈时,就必须对这些“自动化”的技术实施必要的监控和调节

3. GC主要区域

方法区、堆

image-20200712092427246

GCtor可以对年轻代回收,也可以对老年代回收,甚至是全栈、方法区的回收。其中,Heap是GCtor的工作重点区域

从次数上讲:

  • 频繁收集Young区
  • 较少收集Old区
  • 基本不收集Perm区(元空间)

4. System.gc()

  • 在默认情况下,通过System.gc()Runtime.getRuntime().gc()的调用,会显式触发FullGC,同时对老年代、新生代进行回收,尝试释放垃圾Obj占用的内存
  • 然而System.gc()调用附带一个免责声明,无法保证对GCtor的调用(不能确保立即生效)
  • JVM实现者可以通过System.gc()来决定GC行为。而一般情况下,GC是自动进行的,无须手动触发
    • eg:在一些特殊情况下,正在编写一个性能基准,可以在运行时调用System.gc()
public class SystemGCTest {

    public static void main(String[] args) {
        new SystemGCTest();
        // 提醒jvm的GCtor执行gc,但是不确定是否马上执行
        // 与`Runtime.getRuntime().gc();`作用一样
        System.gc();

        System.runFinalization(); // 强制调用Obj_finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest.finalize()");
    }

}






 

 



 





5. 手动GC

public class LocalVarGC {

    /**
     * 执行GC,引用未释放,survivor区放不下10m数据,直接放入old区
     * 触发MinorGC,没有回收Obj。然后触发FullGC将该Obj存入Old区
     */
    @Test
    public void localGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB
        System.gc();
    }

    /**
     * 执行GC,释放引用,回收该数组在堆区空间
     * 触发YoungGC时,已经被回收了
     */
    @Test
    public void localGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    /**
     * 执行GC,不会被回收。虽然buffer声明在局部作用域中,但是局部变量表中还占着一个slot槽
     */
    @Test
    public void localGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }

    /**
     * 执行GC,buffer声明在局部作用域中;作用域外重新声明一个变量。这个变量就占用了原buffer位置,所以释放了引用,进行回收
     * 会被回收。out把buffer的slot槽覆盖了
     */
    @Test
    public void localGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];
        }
        int out = 10;
        System.gc();
    }

    /**
     * localGC1出栈,引用释放,数组已经被回收
     */
    @Test
    public void localGC5() {
        localGC1();
        System.gc();
    }

}
[GC (System.gc()) [PSYoungGen: 44489K->13980K(153088K)] 44489K->13996K(502784K), 0.0117254 secs] [Times: user=0.07 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 13980K->0K(153088K)] [ParOldGen: 16K->13690K(349696K)] 13996K->13690K(502784K), [Metaspace: 8481K->8481K(1056768K)], 0.0151797 secs] [Times: user=0.05 sys=0.02, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------

[GC (System.gc()) [PSYoungGen: 44490K->3798K(153088K)] 44490K->3814K(502784K), 0.0040090 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 3798K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3814K->3450K(502784K), [Metaspace: 8442K->8442K(1056768K)], 0.0103119 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------

[GC (System.gc()) [PSYoungGen: 44494K->13996K(153088K)] 44494K->14012K(502784K), 0.0108401 secs] [Times: user=0.07 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 13996K->0K(153088K)] [ParOldGen: 16K->13690K(349696K)] 14012K->13690K(502784K), [Metaspace: 8479K->8479K(1056768K)], 0.0144075 secs] [Times: user=0.06 sys=0.02, real=0.02 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------

[GC (System.gc()) [PSYoungGen: 44490K->3820K(153088K)] 44490K->3836K(502784K), 0.0064538 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 3820K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3836K->3450K(502784K), [Metaspace: 8480K->8480K(1056768K)], 0.0134044 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------

[GC (System.gc()) [PSYoungGen: 44490K->3783K(153088K)] 44490K->3799K(502784K), 0.0074064 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 3783K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3799K->3450K(502784K), [Metaspace: 8488K->8488K(1056768K)], 0.0177234 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
[GC (System.gc()) [PSYoungGen: 0K->0K(153088K)] 3450K->3450K(502784K), 0.0012005 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(153088K)] [ParOldGen: 3450K->3450K(349696K)] 3450K->3450K(502784K), [Metaspace: 8488K->8488K(1056768K)], 0.0160055 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

6. 内存溢出

OOM:OutOfMemoryError

  • 内存溢出相对于内存泄漏来说,更容易被理解,是引发程序崩溃的罪魁祸首之一
  • javadoc中对OOM的解释是,没有空闲内存,并且GCtor也无法提供更多内存
  • GC一直在发展,一般情况下,除非应用程序占用内存增长速度非常快,造成GC已经跟不上内存消耗的速度,否则不太容易出现OOM

首先说没有空闲内存的情况,原因有二:

  1. JVM的堆内存设置不够
    • 可能存在内存泄漏问题
    • 可能堆的大小不合理。eg:要处理比较可观的数据量,但是没有显式指定Heap大小或指定数值偏小。通过参数-Xms, -Xmx来调整
  2. 代码中创建了大量大Obj,并且长时间不能被GC(存在被引用)
    • 对于老版本的Oracle_JDK,因为永久代的大小是有限的,并且JVM对永久代GC(常量池回收、卸载不再需要的类型)非常不积极,所以当不断添加新类型时,永久代出现OOM也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:java.lang.OutOfMemoryError:PermGen space
    • 随着Metaspace的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,异常信息则变成了:java.lang.OutofMemoryError:Metaspace

  • 这里面隐含着一层意思是,在抛出OutOfMemoryError之前,通常GCtor会被触发,尽其所能去清理出空间
    • eg:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的Obj
    • java.nio.BIts.reserveMemory()中,能清楚的看到,System.gc()会被调用,以清理空间
  • 不是在任何情况下GCtor都会被触发的
    • eg:去分配一个超大Obj,类似一个超大数组超过堆的最大值,JVM可以判断出GC并不能解决这个问题,直接抛出OOM

7. 内存泄漏

  • 严格意义:Obj不会再被程序用到,引用没有断开,GC不能回收
  • 宽泛意义:实际情况一些不太好的实践(或疏忽)会导致Obj的生命周期变得很长,甚至导致OOM

尽管内存泄漏并不会立刻引起程序崩溃,但是程序中的可用内存会被逐步蚕食,直至耗尽,最终出现OOM异常,导致程序崩溃(注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小)eg:

  • 单例模式
    • 单例的生命周期和应用程序是一样长的,所以单例持有外部Obj的引用,这个外部Obj不能被回收,导致内存泄漏
  • 一些提供close的资源未关闭导致内存泄漏
    • 数据库连接dataSourse.getConnection() ,网络连接(socket)和IO连接必须手动close,否则不能被回收

8. Stop_The_World

STW(stop-the-world)指GC发生过程中,产生应用程序停顿。整个应用程序线程都会被暂停,没有任何响应,有点像卡死

  • 可达性分析算法中枚举根节点(GC_Roots)会导致STW
    • 分析工作必须在一个能确保一致性的快照中进行。一致性指分析期间整个执行系统看起来像被冻结在某个时间点上
    • 分析过程中Obj引用关系还在不断变化,则分析结果的准确性无法保证
  • STW和采用哪款GCtor无关。哪怕是G1也不能完全避免STW发生,只能说GCtor越来越优秀,回收效率越来越高,尽可能地缩短STW
  • STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
  • 开发中不要用system.gc()会导致STW的发生

9. 并行、并发

1. Concurrent

  • 在OS中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
  • 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
image-20200712202522051

2. Parallel

  • 并行:当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行
  • 决定并行的因素不是CPU的数量,而是CPU的核心数量,一个CPU多个核也可以并行
  • 适合科学计算,后台处理等弱交互场景
image-20200712202822129

3. 并发VS并行

  1. 并发:指的是多个事情,在同一时间段内同时发生
  2. 并行:指的是多个事情,在同一时间点上同时发生
  • 并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的
  • 只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的

4. GC的并行、并发

  1. 串行(Serial)
    • 相较于并行的概念,单线程执行
    • 如果内存不够,则程序暂停,启动GCtor进行GC。回收完,再启动程序的线程
  2. 并发(Concurrent):指用户线程、GC线程并发执行。用户程序继续运行,GC线程运行于另一个CPU上
    • eg:CMS、G1
image-20200712203815517
  1. 并行(Parallel)
    • 指多条GC线程并行工作,产生STW
    • eg:ParNew、Parallel_Scavenge、Parallel_Old
image-20200712203607845

10. 安全点、安全区域

1. Safe_Point

程序执行时并非在任意地方都能停顿下来进行GC,只有在特定的位置才能停顿下来

  • Safe_Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。eg:方法调用、循环跳转和异常跳转等

如何检查所有线程都跑到最近的安全点停顿下来呢?

  1. 抢先式中断(Preemptive Suspension):(目前没有VM采用了)
    • 首先中断所有线程。如果还有线程不在Safe_Point,就恢复线程,让线程跑到Safe_Point
  2. 主动式中断(Voluntary Suspension):
    • 设置一个中断标志,各线程运行到Safe_Point时主动轮询这个标志,如果中断标志为真,则将线程中断挂起(有轮询的机制)

2. Safe_Region

  • 背景
    • Safe_Point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safe_Point。但是,线程处于Sleep、Blocked,无法响应JVM的中断请求,“走”到Safe_Point,JVM也不可能等待线程被唤醒
  • 安全区域是指在一段代码片段中,Obj的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。Safe_Region看做是被扩展了的Safe_Point
  • 执行流程
    1. 当线程进入到Safe_Region,进行标识,这段时间内发生GC,JVM会忽略被标识的线程
    2. 当线程即将离开Safe_Region,检查是否已经完成GC,如果完成了,则继续运行;否则必须等待,直到收到可以安全离开Safe_Region的信号

11. Reference

描述这样一类Obj:当内存足够时,能保留在内存中;如果进行GC后内存还很紧张,则可以抛弃这些Obj

  • 这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到
    • Reference子类中只有FinalRef是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用
image-20200712205813321
  1. 强引用(Strong_Reference):最传统的“引用”的定义,StrongRef关系还存在,GC就永远不会回收掉强引用的Obj
  2. 软引用(Soft_Reference):在OS发生内存溢出之前,将会把SoftRef_Obj进行第二次回收。如果GC后还没有足够的内存,才会抛出内存溢出异常
  3. 弱引用(Weak_Reference):WeakRef_Obj只能生存到下一次GC之前。GC即回收
  4. 虚引用(Phantom_Reference):PhantomRef完全不会对Obj生存时间构成影响,也无法通过PhantomRef来获得Obj的实例。唯一目的就是能在Obj被GC时收到一个系统通知

1. StrongRef

  • 在Java程序中,最常见的默认引用类型(普通系统99%以上都是强引用
  • 强引用的Obj是可触及的,GC永远不会回收掉被引用的Obj
  • 对于普通Obj,如果没有其他引用关系,只要超过了引用的作用域或者显式地将强引用赋值为null,该Obj即为垃圾
  • 相对的,软、弱、虚引用的Obj是软可触及、弱可触及、虚可触及。在一定条件下,都是可以被回收的。强引用是造成Java内存泄漏的主要原因之一
// 局部变量str指向StringBuffer实例所在Heap,通过str可以操作该实例,那么str就是实例的强引用
StringBuffer str = new StringBuffer("hello mogublog");
StringBuffer str1 = str;

// 将`str = null;`则Heap中的Obj也不会被回收,因为还有其它引用指向StringBuffer实例

StrongRef特点:

  • 可以直接访问目标Obj
  • 所指向的Obj在任何时候都不会被GC,JVM宁愿抛出OOM
  • 可能导致内存泄漏

2. SoftRef

一句话概括:内存不够时,才回收

  • 用来描述一些还有用,但非必需的Obj(内存溢出前不可达的Obj)。只被SoftRef关联着的Obj,在系统将要发生内存溢出异常前,会对SoftRef_Obj进行第二次回收,如果回收后还没有足够的内存,才会抛出内存溢出异常
  • 清理软引用,可选地把引用存放到一个引用队列(Reference_Queue)
  • 通常用来实现内存敏感的缓存
    • eg:高速缓存就用到软引用。如果还有空闲内存,就可以暂时保留缓存;当内存不足时,则清理掉。这样就保证了使用缓存的同时,不会耗尽内存

JDK1.2之后,提供了SoftReference类来实现软引用

// 声明强引用
Object obj = new Object();
// 创建SoftRef
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; // 销毁强引用,不然会存在强引用和软引用



 

3. WeakRef

GC即回收

  • 描述非必需Obj,WeakRef_Obj只能生存到下一次GC发生为止
  • WeakRef、SoftRef,在构造引用时,也可以指定一个引用队列。这个队列可以跟踪Obj的回收情况
  • WeakRef、SoftRef都非常适合来保存那些可有可无的缓存数据

JDK1.2之后,提供了WeakReference类来实现弱引用

// 声明强引用
Object obj = new Object();
// 创建WeakRef
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; // 销毁强引用,不然会存在强引用和弱引用



 

4. PhantomRef

也称为“幽灵引用”、“幻影引用”,是所有引用类型中最弱的一个

  • PhantomRef不会决定Obj的生命周期。随时可能被GC掉
  • 不能单独使用,PhantomRef无法获取Obj,get()取Obj时,总是null
  • 存在唯一目的:跟踪GC过程(PhantomRef_Obj被GC时,收到一个系统通知)
    • 在创建时必须提供一个Reference_Queue作为参数。当GC一个Obj时,将PhantomRef加入Queue,通知应用程序该Obj的回收情况
    • 由于PhantomRef可以跟踪Obj的回收,因此,可以将一些资源释放操作放置在PhantomRef中执行和记录

JDK1.2之后,提供了PhantomReference类来实现虚引用

// 强引用
Object obj = new Object();

// 引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 虚引用(传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;




 

 

结合虚引用,引用队列,finalize()进行讲解

/**
 * 虚引用的测试
 */
public class PhantomReferenceTest {
    public static PhantomReferenceTest obj;                             // 当前类Obj的声明
    static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;    // 引用队列

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while (true) {
                if (phantomQueue != null) {
                    PhantomReference<PhantomReferenceTest> objt = null;
                    try {
                        objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (objt != null) {
                        System.out.println("追踪GC过程:PhantomReferenceTest实例被GC了");
                    }
                }
            }
        }
    }

    // finalize()只能被调用一次!
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类的finalize()方法");
        obj = this; // 复活Obj
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new CheckRefQueue();
        t.setDaemon(true); // 设置为守护线程:当程序中没有被守护线程时,守护线程也就执行结束
        t.start();

        phantomQueue = new ReferenceQueue<>();
        obj = new PhantomReferenceTest();
        // 构造了PhantomReferenceTestObj的虚引用,并指定了引用队列
        PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);

        // 不可获取phantomRef_Obj
        System.out.println("phantomRef.get() = " + phantomRef.get());

        System.out.println("=====>>>>> 第 1 次 gc");
        obj = null;
        // 第一次进行GC,由于Obj可复活,GC无法回收该Obj
        System.gc();
        Thread.sleep(1000);
        if (obj == null) {
            System.out.println("obj是null");
        } else {
            System.out.println("obj可用");
        }

        System.out.println("=====>>>>> 第 2 次 gc");
        obj = null;
        System.gc(); // 一旦将Obj回收,就会将此phantomRef存放到引用队列中
        Thread.sleep(1000);
        if (obj == null) {
            System.out.println("obj是null");
        } else {
            System.out.println("obj可用");
        }
    }

}














 




 

























 




 









 









phantomRef.get() = null
=====>>>>>1 次 gc
调用当前类的finalize()方法
obj可用
=====>>>>>2 次 gc
追踪垃圾回收过程:PhantomReferenceTest实例被GC了
obj是null

5. FinalRef

  • 用于实现Obj的finalize() ,也可以称为终结器引用
  • 无需手动编码,其内部配合引用队列使用
  • 在GC时,终结器引用入队。Finalizer线程通过终结器引用找到被引用Obj,调用其finalize()