Java基础_虚拟机2:(JVM垃圾回收)

目录

1:那些内存需要回收

2:什么是时候回收

2.1:根据对象的状态回收(新生代 老年代)

2.2:如何回收堆内存(主要指回收对象)

2.3:再谈引用(虚引用主要用来跟踪对象被垃圾回收的活动。)

2.4:如何回收方法区(永久代)

3:垃圾回收算法

3.1:为什么对象划分为新生代和老年代

3.2:垃圾收集算法

3.2.1:标记清除算法(效率不高并且产生碎片化内存)

3.2.2:复制算法(内存利用效率很低,用于新生代)

3.2.3:标记整理算法(适用于老年代)

3.2.4:分代整理算法

3.3:新生代和老年代垃圾回收详细解释

4:垃圾回收器

4.1 Serial 收集器(单个GC线程收集垃圾)

4.2 ParNew 收集器(多个GC线程收集垃圾)

4.3 Parallel Scavenge 收集器(并行清除,jdk1.8的新生代垃圾收集器,采用复制算法实现)

4.4.Serial Old 收集器(老年代单线程收集器,已淘汰)

4.5 Parallel Old 收集器(JDK1.8的老年代垃圾收集器,并行老年代收集器)

4.6 CMS 收集器(适合大型网站,GC停顿时间很短)

4.7 G1 收集器(最新版的垃圾收集器)

5:收集器代码实现和案例


1:那些内存需要回收

垃圾回收器关注的是堆中的内存(包含堆内存和方法区),理由如下:

      其中程序计数器、java虚拟机栈、和本地方法栈都是属于线程私有的,随着线程的生而生随着线程的死而死。但是堆内存和方法区则是所有线程共享的,一个接口可以有多个实现类,一个方法不同的逻辑会创建不同的对象。所以这部分内存只有在程序运行发的时候才能知道创建了那些对象。这些内存的分配是动态的,所以垃圾回收器主要关注的是堆的内存。

JDK 1.8 :

2:什么是时候回收

2.1:根据对象的状态回收(新生代 老年代)

由于我们知道了垃圾回收器关注的是堆中的内存,而堆中内存主要存储的是java对象,要想回收这些对象,最主要的就是要知道这些对象的状态是否还有用,是活着还是死亡了。策略就是给对象划分状态,根据不同的状态在决定是回收这些对象

2.2:如何回收堆内存(主要指回收对象)

判断对象存活:

  • 1:引用计数器法:

在对象上维护一个计数器,用地方引用就加一,引用失效就减一。当0的时候就标识对象已经没人引用了,判定为死亡,FullGC回收。

这种方法缺陷是:难以解决对象之间互相引用的问题。

  • 2:可达性分析法:(hotport虚拟机采用这种策略)

以一个GC Root对象作为根节店,对象与GC Root之间建立引用关系,只要对象与根节点之间有引用链,就判断对象存活,否则判断对象不可达,为死亡,则回收对象。

2.3:再谈引用(虚引用主要用来跟踪对象被垃圾回收的活动。)

无论是引用计数法还是可达性分析算法,要想判断对象的状态,都与引用存在着紧密的联系。而且引用类型的数据中存在的值代表内存中的起始地址。在jdk1.2之前对象只有被引用和没有被引用两种状态。对于一些我们在内存充足的时候我们希望保留,内存不足的时候希望清除的对象不要试用。所以引入四大引用类型:

1.强引用(new关键字产生的对象)

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

2.4:如何回收方法区(永久代)

我们知道方法区主要存放类信息和常量池。那么回收也是主要分为类的回收和常量

回收类:

而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

回收常量:

以常量池的字面量为例:一个字符串abc进入常量池,但是没有任何一个String对象叫做abc,也没有其他的地方引用到了这个字面量。那么这个abc这个常量就会被系统清理出常量池。

3:垃圾回收算法

在了解垃圾回收算之前,有一个疑问?我么知道堆中的对象分为新生代、老年代。但是为什么有新生代和老年代呢?是为了区别管理,对于对象规划生命周期来管理。

通过算法来逐步理解这个问题

3.1:为什么对象划分为新生代和老年代

     不分代策略: 

       所有的对象和数组都存放在堆中,这个时候回收对象那个和数组每次都需要扫描整个堆,把要回收的对象和数组回收。下一次接着全局扫描,并且我们知道堆中的对象。并且垃圾回收这个动作是频繁的,所以就需要频繁的扫描真个全部的堆内存,但是有的对象是存活周期很短的,有的很长,这种方法做不到分类管理,精细化管理。效率很低需要全部内存扫描,存在缺陷。

     分代策略:

      所有就把在堆中划分了新生代、老年代把不同生命周期的对象存放到不同的内存中。由于新生代和老年代中存在的对象年龄不一样导致扫描频率不一样,所有有不同的垃圾清除算法和策略在不同的内存空间中实现,也就是说老年代和新生代各自有不同的算法实现的对应的垃圾清除器。

3.2:垃圾收集算法

具体算法如图所示:

3.2.1:标记清除算法(效率不高并且产生碎片化内存)

算法分为“标记”和“清除”阶段:首先标记所有需要回收的对象,然后统一回收所有的对象。是一个基础算法,效率不高。但是有缺陷,碎片化

  1. 效率问题(频繁扫描全部内存标记对象状态)
  2. 空间问题(标记清除后会产生大量不连续的碎片,新的大对象可能没有连续空间分配内存了)

3.2.2:复制算法(内存利用效率很低,用于新生代)

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

3.2.3:标记整理算法(适用于老年代)

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

3.2.4:分代整理算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

延伸面试问题: HotSpot 为什么要分为新生代和老年代?

3.3:新生代和老年代垃圾回收详细解释

新生代和老年代比例是:  1比3,老年大,新生代小

  • 大对象直接进入老年代
  • 新生代的垃圾回收操作:Maior GC
  • 老年代的垃圾回收操作:Major GC
  • 整个堆的垃圾回收收操作:Full GC                   

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

4:垃圾回收器

我们说回收算法是垃圾回收的方法论。那么垃圾回收器就是内存回收的具体实现。不同版本的虚拟机、不同厂商的虚拟机都会存在很大的差异。

GC停顿:

         所有的垃圾回收器在GC的时候都会停顿,停顿的原因的是要保证系统在通过可达性算法的时候,所有对象的引用关系都不能在发生变化了,要不然没法确定对象的状态,所以要求所有的线程暂停,这就是GC停顿的重要原因。

4.1 (单个GC线程收集垃圾)

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束

新生代采用复制算法,老年代采用标记-整理算法。 

虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。

4.2 ParNew 收集器(多个GC线程收集垃圾)

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。 

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

并行和并发概念补充:

  • 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

4.3 Parallel Scavenge 收集器并行清除,jdk1.8的新生代垃圾收集器,采用复制算法实现

Parallel Scavenge 收集器类似于 ParNew 收集器。 那么它有什么特别之处呢?

-XX:+UseParallelGC 

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。这叫做自适应调节

新生代采用复制算法,老年代采用标记-整理算法。 

4.4.Serial Old 收集器(老年代单线程收集器,已淘汰)

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

4.5 Parallel Old 收集器(JDK1.8的老年代垃圾收集器,并行老年代收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

4.6 CMS 收集器(适合大型网站,GC停顿时间很短

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的老年代回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

4.7 G1 收集器(最新版的垃圾收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GF 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

 

5:收集器代码实现和案例

输出结果如下:

----------------------根据结果分析如下------------------------------

垃圾收集器切换配置

 例如切换到G1垃圾收集器命令如下:   -XX:+UseG1GC

年轻代内存 =eden区(3072K)+ from区(512K) + to区(512K)=4096K=4M

堆内存(10M)=年轻代(4M)+老年代(6M)

PSYoungGen(代表年轻代采用的垃圾回收器是Parallel Scavenge 收集器)

ParOldGen (代表老年的垃圾收集器是Parallel Old 收集器)

Metaspace(方法区内存,在JDK1.8之后跟堆内存分离)

文章参考:https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md#44serial-old-收集器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值