作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!????

一、前言

晋升本身价值有多重要?

通过了风风雨雨,看过了男男女女。工夫通过的岁月就没有永恒不变的!

在这趟车上有人下、有人上,外在他人给你点评的标签、留下的烙印,都只是这趟车上的故事。只有个人成长了、积攒了、积淀了,才有机会当本人的司机。

可能某个年龄段的你还看不懂,但如果某天你不那么忙了,要思考思考本人的路、本人的脚步。看看这些是不是你想要的,如果都是你想要的,为什么你看起来不开心?

好!加油,走向你想成为的本人!

二、面试题

谢飞机,小记!,中午吃饱了开始发愣,怎么就学不来这些常识呢,它也不进脑子!

谢飞机:喂,面试官大哥,我想问个问题。

面试官:什么?

谢飞机:就是这常识它不进脑子呀!

面试官:这....

谢飞机:就是看了忘,忘了看的!

面试官:是不是没有实际?只是看了就感觉会了,珍藏了就示意懂了?哪哪都不深刻!?

谢飞机:如同是!那有什么方法?

面试官:也没有太好的方法,学习自身就是一件干燥的事件。缩小碎片化的工夫节约,多用在系统化的学习上会更好一些。哪怕你写写博客记录下,验证下也是好的。

三、先入手验证垃圾回收

说是垃圾回收,我不援用了它就回收了?什么时候回收的?咋回收的?

没有看到理论的例子,往往就很难让理科生承受这类常识。我本人也一样,最好是让我看得见。代码是对数学逻辑的具体实现,没有实现过程只看答案是没有意义的。

测试代码

public class ReferenceCountingGC {    public Object instance = null;    private static final int _1MB = 1024 * 1024;    /**     * 这个成员属性的惟一意义就是占点内存, 以便能在GC日志中看清楚是否有回收过     */    private byte[] bigSize = new byte[2 * _1MB];    public static void main(String[] args) {        testGC();    }    public static void testGC() {        ReferenceCountingGC objA = new ReferenceCountingGC();        ReferenceCountingGC objB = new ReferenceCountingGC();        objA.instance = objB;        objB.instance = objA;        objA = null;        objB = null;        // 假如在这行产生GC, objA和objB是否能被回收?        System.gc();    }}

例子来自于《深刻了解Java虚拟机》中援用计数算法章节。

例子要阐明的后果是,互相援用下却曾经置为null的两个对象,是否会被GC回收。如果只是依照援用计数器算法来看,那么这两个对象的计数标识不会为0,也就不能被回收。但到底有没有被回收呢?

这里咱们先采纳 jvm 工具指令,jstat来监控。因为监控的过程须要我手敲代码,比拟耗时,所以咱们在调用testGC()前,睡眠会 Thread.sleep(55000);。启动代码后执行如下指令。

E:\itstack\git\github.com\interview>jps -l106568846438372 org.itstack.interview.ReferenceCountingGC26552 sun.tools.jps.Jps110056 org.jetbrains.jps.cmdline.LauncherE:\itstack\git\github.com\interview>jstat -gc 38372 2000 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.00010752.0 10752.0  0.0   1288.0 65536.0    0.0     175104.0     8.0     4864.0 3982.6 512.0  440.5       1    0.003   1      0.000    0.00310752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.01510752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
  • S0C、S1C,第一个和第二个幸存区大小
  • S0U、S1U,第一个和第二个幸存区应用大小
  • EC、EU,伊甸园的大小和应用
  • OC、OU,老年代的大小和应用
  • MC、MU,办法区的大小和应用
  • CCSC、CCSU,压缩类空间大小和应用
  • YGC、YGCT,年老代垃圾回收次数和耗时
  • FGC、FGCT,老年代垃圾回收次数和耗时
  • GCT,垃圾回收总耗时

留神:察看前面三行,S1U = 1288.0GCT = 0.003,阐明曾经在执行垃圾回收。

接下来,咱们再换种形式测试。在启动的程序中,退出GC打印参数,察看GC变动后果。

-XX:+PrintGCDetails  打印每次gc的回收状况 程序运行完结后打印堆空间内存信息(蕴含内存溢出的状况)-XX:+PrintHeapAtGC  打印每次gc前后的内存状况-XX:+PrintGCTimeStamps 打印每次gc的距离的工夫戳 full gc为每次对新生代老年代以及整个空间做对立的回收 零碎中应该尽量避免-XX:+TraceClassLoading  打印类加载状况-XX:+PrintClassHistogram 打印每个类的实例的内存占用状况-Xloggc:/Users/xiaofuge/Desktop/logs/log.log  配合下面的应用将下面的日志打印到指定文件-XX:HeapDumpOnOutOfMemoryError 产生内存溢出将堆信息转存起来 以便剖析

这回就能够把睡眠去掉了,并增加参数 -XX:+PrintGCDetails,如下:

测试后果

[GC (System.gc()) [PSYoungGen: 9346K->936K(76288K)] 9346K->944K(251392K), 0.0008518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 936K->0K(76288K)] [ParOldGen: 8K->764K(175104K)] 944K->764K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0040034 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] Heap PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eb9e0,0x000000076f500000)  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000) ParOldGen       total 175104K, used 764K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1ebf100,0x00000006cc900000) Metaspace       used 3449K, capacity 4496K, committed 4864K, reserved 1056768K  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K
  • 从运行后果能够看出内存回收日志,Full GC 进行了回收。
  • 也能够看出JVM并不是依赖援用计数器的形式,判断对象是否存活。否则他们就不会被回收啦

有了这个例子,咱们再接着看看JVM垃圾回收的常识框架!

四、JVM 垃圾回收常识框架

垃圾收集(Garbage Collection,简称GC),最早于1960年诞生于麻省理工学院的Lisp是第一门开始应用内存动态分配和垃圾收集技术的语言。

垃圾收集器次要做的三件事:哪些内存须要回收什么时候回收、怎么回收。

而从垃圾收集器的诞生到当初有半个世纪的倒退,当初的内存动态分配和内存回收技术曾经十分成熟,所有看起来都进入了“自动化”。但在某些时候还是须要咱们去监测在高并发的场景下,是否有内存溢出、透露、GC工夫过程等问题。所以在理解和通晓垃圾收集的相干常识对于高级程序员的成长就十分重要。

垃圾收集器的外围常识项次要包含:判断对象是否存活、垃圾收集算法、各类垃圾收集器以及垃圾回收过程。如下图;

原图下载链接:http://book.bugstack.cn/#s/6jJp2icA

1. 判断对象已死

1.1 援用计数器

  1. 为每一个对象增加一个援用计数器,统计指向该对象的援用次数。
  2. 当一个对象有相应的援用更新操作时,则对指标对象的援用计数器进行增减。
  3. 一旦当某个对象的援用计数器为0时,则示意此对象曾经死亡,能够被垃圾回收。

从实现来看,援用计数器法(Reference Counting)尽管占用了一些额定的内存空间来进行计数,然而它的实现计划简略,判断效率高,是一个不错的算法。

也有一些比拟闻名的援用案例,比方:微软COM(Component Object Model) 技术、应用ActionScript 3的FlashPlayer、 Python语言等。

然而,在支流的Java虚拟机中并没有选用援用技术算法来治理内存,次要是因为这个简略的计数形式在解决一些相互依赖、循环援用等就会非常复杂。可能会存在不再应用但又不能回收的内存,造成内存透露

1.2 可达性分析法

Java、C#等支流语言的内存管理子系统,都是通过可达性剖析(Reachability Analysis)算法来断定对象是否存活的。

它的算法思路是通过定义一系列称为 GC Roots 根对象作为起始节点集,从这些节点登程,穷举该汇合援用到的全副对象填充到该汇合中(live set)。这个过程教过标记,只标记那些存活的对象 好,那么当初未被标记的对象就是能够被回收的对象了。

GC Roots 包含;

  1. 全局性援用,对办法区的动态对象、常量对象的援用
  2. 执行上下文,对 Java办法栈帧中的部分对象援用、对 JNI handles 对象援用
  3. 已启动且未进行的 Java 线程

两大问题

  1. 误报:已死亡对象被标记为存活,垃圾收集不到。多占用一会内存,影响较小。
  2. 漏报:援用的对象(正在应用的)没有被标记为存活,被垃圾回收了。那么间接导致的就是JVM奔溃。(STW能够确保可达性分析法的准确性,防止漏报)

2. 垃圾回收算法

2.1 标记-革除算法(mark-sweep)

  • 标记无援用的死亡对象所占据的闲暇内存,并记录到闲暇列表中(free list)。
  • 当须要创立新对象时,内存治理模块会从 free list 中寻找闲暇内存,调配给新建的对象。
  • 这种清理形式其实非常简单高效,然而也有一个问题内存碎片化太重大了。
  • Java 虚拟机的堆中对象,必须是间断散布的,所以极其的状况下可能即便总残余内存短缺,但寻找间断内存调配效率低,或者重大到无奈分配内存。重启汤姆猫!
  • 在CMS中有此类算法的应用,GC暂停工夫短,但存在算法缺点。

2.2 标记-复制算法(mark-copy)

  • 从图上看这回做完垃圾清理后间断的内存空间就大了。
  • 这种形式是把内存区域分成两份,别离用两个指针 from 和 to 保护,并且只应用 from 指针指向的内存区域分配内存。
  • 当产生垃圾回收时,则把存活对象复制到 to 指针指向的内存区域,并替换 from 与 to 指针。
  • 它的益处很显著,就是解决内存碎片化问题。但也带来了其余问题,堆空间节约了一半。

2.3 标记-压缩算法(mark-compact)

  • 1974年,Edward Lueders 提出了标记-压缩算法,标记的过程和标记革除算法一样,但在后续对象清理步骤中,先把存活对象都向内存空间一端挪动,而后在清理掉其余内存空间。
  • 这种算法可能解决内存碎片化问题,但压缩算法的性能开销也不小。

3. 垃圾回收器

3.1 新生代

  1. Serial

    1. 算法:标记-复制算法
    2. 阐明:简略高效的单核机器,Client模式下默认新生代收集器;
  2. Parallel ParNew

    1. 算法: 标记-复制算法
    2. 阐明:GC线程并行版本,在单CPU场景成果不突出。罕用于Client模式下的JVM
  3. Parallel Scavenge

    1. 算法:标记-复制算法
    2. 阐明:指标在于达到可控吞吐量(吞吐量=用户代码运行工夫/(用户代码运行工夫+垃圾回收工夫));

3.2 老年代

  1. Serial Old

    1. 算法:标记-压缩算法
    2. 阐明:性能个别,单线程版本。1.5之前与Parallel Scavenge配合应用;作为CMS的后备预案。
  2. Parallel Old

    1. 算法:标记-压缩算法
    2. 阐明:GC多线程并行,为了代替Serial Old与Parallel Scavenge配合应用。
  3. CMS

    1. 算法:标记-革除算法
    2. 阐明:对CPU资源敏感、进展工夫长。标记-革除算法,会产生内存碎片,能够通过参数开启碎片的合并整顿。根本已被G1取代

3.3 G1

  1. 算法:标记-压缩算法
  2. 阐明:实用于多核大内存机器、GC多线程并行执行,低进展、高回收效率。

五、总结

  • JVM 的对于主动内存治理的常识泛滥,包含本文还没提到的 HotSpot 实现算法细节的相干常识,包含:平安节点、平安区域、卡表、写屏障等。每一项内容都值得深刻学习。
  • 如果不仅仅是为了面试背题,最好的形式是实际验证学习。否则这类常识就像3分以下的过电影一样,很难记住它的内容。
  • 整个的内容也是小傅哥学习整顿的一个过程,后续还会一直的持续深挖和分享。感兴趣的小伙伴能够一起探讨学习。

六、系列举荐

  • 认知本人的技术栈盲区
  • 为了搞清楚类加载,居然手撸JVM!
  • JVM故障解决工具,应用总结
  • Thread.start() ,它是怎么让线程启动的呢?-%E5%AE%83%E6%98%AF%E6%80%8E%E4%B9%88%E8%AE%A9%E7%BA%BF%E7%A8%8B%E5%90%AF%E5%8A%A8%E7%9A%84%E5%91%A2.html)
  • ThreadLocal 你要这么问,我就挂了!