解决方案:2 - JVM垃圾回收

优采云 发布时间: 2022-12-01 13:33

  解决方案:2 - JVM垃圾回收

  JVM内存分配与回收介绍 判断对象是否死亡 垃圾回收算法 Garbage collector

  主要逻辑流程:

  内存是如何分配和回收的?--> 什么垃圾需要回收?--> 什么时候回收?--> 如何回收?

  一、JVM内存分配与回收简介

  Java的自动内存管理主要是对象内存的回收和对象内存的分配。同时,Java自动内存管理的核心功能是堆内存中对象的分配和回收。

  Java堆是垃圾采集

器管理的主要区域,因此也称为GC堆(Garbage Collected Heap)。从垃圾回收的角度来看,由于目前的回收器基本采用分代垃圾回收算法,所以Java堆还可以细分为:新生代和老年代:更细化:Eden空间,From Survivor,To Survivor空间等.进一步划分的目的是为了更好的回收内存,或者更快的分配内存。

  Java堆空间的基本结构

  上图中的Eden区、From Survivor0(“From”)区、To Survivor1(“To”)区都属于新生代,Old Memory区属于老年代。

  大多数情况下,对象会先分配到伊甸区。新生代垃圾回收后,如果对象还活着,则进入s0或s1,对象年龄加1(Eden区->Survivor区)。初始age变成1),当它的age增长到一定程度(默认15岁),就会被提升到老年代。可以通过参数-XX:MaxTenuringThreshold 设置将对象提升到Old Age 的年龄阈值。

  新生代一次垃圾回收后,Hotspot遍历所有对象时,按照年龄从小到大的顺序累加它们占用的大小。当累计年龄超过幸存者区域的一半时,将这个年龄和MaxTenuringThreshold中的更新值取一个小值,作为下一次垃圾回收的提升年龄阈值。

  动态年龄计算代码:

  uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {

//survivor_capacity是survivor空间的大小

size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);

size_t total = 0;

uint age = 1;

while (age < table_size) {

total += sizes[age];//sizes数组是每个年龄段对象大小

if (total > desired_survivor_size) break;

age++;

}

uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;

...

}

  一开始,两个幸存者区和伊甸园区都是空的。慢慢的eden区就满了,那就是第一次操作。gc之后,eden区会被清理,活着的对象会被复制到“From”survivor区。因为是从eden区复制过来的,所以使用的是连续空间,没有碎片。然后eden继续添加新的对象,直到eden再次满为止。此时eden区和“From”区都有数据,不为空。然后gc之后,eden区有幸存者,“From”区也净化后,也有幸存者。此时“To”区域为空,然后将“From”和eden区域的幸存者Copy到“To”区域。复制时,“收件人”中的空格 area也是挨着分配的,没有碎片。然后,清空eden区和“From”区,这说明:“总有一个幸存者空间是空的,另一个非空的幸存者空间没有碎片。

  这时候“From”和“To”就会互换角色,即新的“To”就是上次GC之前的“From”,新的“From”就是上次GC之前的“To”。在任何情况下,名为 To 的 Survivor 区域都保证为空。Minor GC会重复这个过程,直到“To”区域被填满,“To”区域被填满后,所有对象都会被移动到老年代。

  1.1 对象先分配在eden区

  目前主流的垃圾采集

器都是采用分代采集

算法,所以需要将堆内存划分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾采集

算法。

  大多数情况下,对象都分配在年轻代的eden区。当eden区没有足够的空间分配时,虚拟机会发起一次Minor GC。

  Minor GC 和 Full GC 有什么区别?

  下面测试一下Minor GC的流程:

  package jvm.gc;

public class GCTest {

public static void main(String[] args) {

byte[] allocation1, allocation2,allocation3,allocation4,allocation5;

allocation1 = new byte[60000*1024];

//allocation2 = new byte[2000*1024];

//allocation3 = new byte[1000*1024];

//allocation4 = new byte[1000*1024];

//allocation5 = new byte[1000*1024];

}

}

  打印的内存占用是:

  内存使用信息.png

  从图中可以看出,eden区基本分配完毕。如果此时为allocation2分配空间,运行结果为:

  分配保障机制的实施

  出现这种情况的原因:

  因为在allocation2分配内存的时候eden区的内存已经分配的差不多了,刚才我们说了当Eden区没有足够的空间可以分配的时候,虚拟机就会发起一次Minor GC。在GC的时候,虚拟机发现allocation1不能存放到Survivor空间,所以需要通过分配保证机制将新生代中的对象提前转移到老年代。old generation中的空间足够存放allocation1,所以不会发生Full GC。Minor GC执行后,如果后面分配的对象可以存在于eden区,内存仍会分配在eden区。

  1.2 大对象直接进入老年代

  大对象是需要大量连续内存空间的对象(例如字符串、数组)。

  为了避免在为大对象分配内存时由于分配保证机制带来的复制而降低效率。

  1.3 长寿对象会进入老年代

  由于虚拟机采用了分代采集

的思想来管理内存,因此在内存回收时必须能够识别出哪些对象应该放在新生代,哪些对象应该放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

  如果对象出生在Eden,在第一次Minor GC后存活下来,并且可以被Survivor容纳,则将其移动到Survivor空间,并将对象的年龄设置为1。每一次对象存活一次MinorGC在Survivor中,年龄增加1年,当它的年龄增加到一定程度(默认15岁),就会晋升到老年代。可以通过参数-XX:MaxTenuringThreshold 设置将对象提升到Old Age 的年龄阈值。

  2.判断对象是否死亡

  几乎所有的对象实例都放在堆中,在堆上进行垃圾回收之前的第一步是确定那些对象已经死了(即无论如何都不能再使用的对象)。

  判断对象是否死亡

  2.1 引用计数

  向对象添加一个引用计数器。每当有对它的引用时,计数器就会加 1;

  这种方法实现简单,效率高,但是目前主流的虚拟机并没有选择这种算法来管理内存。主要原因是很难解决对象之间的循环引用问题。所谓对象之间的相互引用问题如下代码所示:除了对象objA和objB相互引用外,这两个对象之间没有任何引用。但是因为它们相互引用,它们的引用计数器不为0,所以引用计数算法无法通知GC采集

器回收它们。

  package jvm.gc;

public class ReferenceCountingGc {

private Object instance;

public static void main(String[] args) {

ReferenceCountingGc t1 = new ReferenceCountingGc();

ReferenceCountingGc t2 = new ReferenceCountingGc();

t1.instance = t2;

t2.instance = t1;

t1 = null;

t2 = null;

}

}

  2.2 可达性分析算法

  这个算法的基本思想是以一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索。节点经过的路径称为引用链。当一个对象没有任何引用链到 GC Roots 连接时,证明该对象不可用。

  可达性分析

  可以作为 GC Root 的对象包括:

  2.3 参考资料

  无论是通过引用计数的方法判断对象的引用次数,还是通过可达性分析的方法判断对象的引用链是否可达,判断对象的存活与“引用”有关。

  在JDK1.2之前,Java中对引用的定义很传统:如果数据的引用类型中存储的值代表了另一块内存的起始地址,就说这块内存代表了一个引用。

  JDK1.2之后,Java扩展了引用的概念,将引用分为四种:强引用、软引用、弱引用、虚引用(引用强度逐渐减弱)

  引用之间的继承关系

  1.强引用(StrongReference)

  以前用的大部分引用其实都是强引用,也就是最常用的引用。如果一个对象有强引用,它就类似于生活必需品,垃圾采集

器永远不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误导致程序异常终止,也不会通过任意回收强引用对象来解决内存不足问题。

  例子:

  package jvm.gc.reference;

<p>

" />

public class StrongReferenceDemo {

/**

* 输出结果:

* java.lang.Object@135fbaa4

* 能打印出来说明obj2没有被回收

*

* @param args

*/

public static void main(String[] args) {

Object obj1 = new Object();//这样定义就是强引用

Object obj2 = obj1;

obj1 = null;//置空

System.gc();

System.out.println(obj2); //能打印出来说明没有被回收

}

}

</p>

  2. 软引用(SoftReference)

  如果一个对象只有软引用,那么它类似于可有可无的家居用品。如果有足够的内存空间,垃圾采集

器将不会回收它。如果内存空间不够,这些对象的内存就会被回收。只要垃圾采集

器不采集

它,该对象就可以被程序使用。软引用可用于实现对内存敏感的缓存。

  软引用可以与引用队列(ReferenceQueue)结合使用。如果软引用引用的对象被垃圾回收,JAVA虚拟机会将软引用添加到与其关联的引用队列中。

  软引用可以加快JVM对垃圾内存的回收,维护系统的安全,防止内存溢出(OutOfMemory)等问题。

  适用场景

  假设一个应用需要读取大量的本地图片:

  使用软引用解决了这个问题。

  设计思路:

  使用HashMap保存图片的路径与对应图片对象关联的软引用的映射关系。当内存不足时,JVM会自动回收这些缓存的图片对象占用的空间,避免OOM问题。

  Map imageCache = new HashMap();

  3.弱引用(WeakReference)

  如果一个对象只有弱引用,它类似于可有可无的家居用品。弱引用和软引用的区别在于只有弱引用的对象生命周期更短。在垃圾回收线程扫描其管辖内存区域的过程中,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。然而,由于垃圾采集

器是一个非常低优先级的线程,只有弱引用的对象可能无法快速找到。

  弱引用可以与引用队列(ReferenceQueue)结合使用。如果弱引用引用的对象被垃圾回收,Java虚拟机会将弱引用添加到与其关联的引用队列中。

  例子:

  package jvm.gc.reference;

import java.lang.ref.WeakReference;

public class WeakReferenceDemo {

/**

* 输出结果为:

* java.lang.Object@135fbaa4

* java.lang.Object@135fbaa4

* =========================

* null

* null

*

* 这就体现了:

* 在垃圾回收器线程扫描它所管辖的内存区域的过程中,

* 一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

* @param args

*/

public static void main(String[] args) {

Object o1 = new Object();

WeakReference weakReference = new WeakReference(o1);

System.out.println(o1);

System.out.println(weakReference.get());

o1 = null;

System.gc();

System.out.println("=========================");

System.out.println(o1);

System.out.println(weakReference.get());

}

}

  适用场景

  WeakHashmap的应用:

  代码:

  package jvm.gc.reference;

import java.util.HashMap;

import java.util.WeakHashMap;

public class WeakHashmapDemo {

public static void main(String[] args) {

myHashmap();

System.out.println("========================");

myWeakHashmap();

}

private static void myWeakHashmap() {

WeakHashMap map = new WeakHashMap();

//HashMap map = new HashMap();

Integer key = new Integer(1);

String value = "HashMap";

map.put(key,value);

System.out.println(map);

key = null;

System.out.println(map);

System.gc();

System.out.println(map+"\t"+map.size());

}

private static void myHashmap() {

//WeakHashMap map = new WeakHashMap();

<p>

" />

HashMap map = new HashMap();

Integer key = new Integer(1);

String value = "HashMap";

map.put(key,value);

System.out.println(map);

key = null;

System.out.println(map);

System.gc();

System.out.println(map+"\t"+map.size());

}

}

</p>

  4.幻影参考(PhantomReference)

  “Phantom reference”,顾名思义,是没有用的。与其他类型的引用不同,幻象引用不决定对象的生命周期。如果一个对象只收录

虚引用,就好像它没有引用一样,可以随时被垃圾回收。

  幻影引用主要用于跟踪被垃圾采集

的对象的活动。

  幻影引用与软引用和弱引用的区别之一是幻影引用必须与引用队列(ReferenceQueue)结合使用。当垃圾回收器要回收一个对象时,如果发现它还有一个虚引用,就会把这个虚引用添加到与之关联的引用队列中,然后再回收该对象的内存。程序可以通过判断引用队列中是否加入了幻引用来获知被引用对象是否会被垃圾回收。如果程序发现引用队列中加入了虚引用,则可以在被引用对象的内存被回收之前采取必要的动作。

  2.4 不可达对象不是“必死”

  即使是可达性分析方法中的不可达对象也不是“必死”的。对属性分析不可达的对象进行第一次标记,筛选一次,筛选条件为是否需要对该对象执行finalize方法。当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,虚拟机认为这两种情况不需要执行。

  判断需要执行的对象会被放入队列中进行二次标记,除非该对象与引用链上的任何对象相关联,才会真正被回收。

  2.5 判断常量是废弃对象

  运行常量池主要是回收废弃的常量。

  常量是否为废弃常量的判断标准: 如果常量池中存在字符串“abc”,如果当前没有String对象引用该字符串常量,则说明常量“abc”为废弃常量。如果此时发生内存回收 必要时,“abc”会被系统从常量池中清除。

  2.6 判断一个类是无用类

  方法区主要是回收无用的类。

  判断一个类是否为“无用类”,一个类需要同时满足以下三个条件才能被认为是“无用类”:

  虚拟机可以回收满足以上三个条件的无用类。这里说的只是“可以”,并不是像对象一样不使用就会被回收。

  3. 垃圾采集

算法 3.1 Mark-Sweep 算法

  该算法分为“标记”和“清除”两个阶段:首先标记所有不需要回收的对象,标记完成后统一回收所有未标记的对象。它是最基本的采集算法,后续的算法都是通过改进它的缺点得到的。这种垃圾采集

算法产生了两个明显的问题:

  效率问题空间问题(标记清除后会产生大量不连续的碎片)

  标记扫描算法

  3.2 复制

  为了解决效率问题,出现了“复制”采集

算法。它可以将内存分成大小相同的两块,一次使用其中的一块。当这块内存用完后,将存活的对象复制到另一块内存中,然后一次性清理已用空间。这样每次内存回收就是回收一半的内存范围。

  复制算法

  3.3 标记整理算法

  根据老年代的特点提出的一种标记算法。标记过程还是和“mark-clear”算法一样,只是后面的步骤不是直接回收可回收对象,而是将所有存活的对象移到一端,然后直接清理。超出字节序边界的内存。

  标记算法

  3.4 分代采集

算法

  目前虚拟机的垃圾回收采用的是分代回收算法。这个算法没有什么新意,只是根据对象生命周期的不同,把内存分成若干块。一般java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾回收算法。

  比如在新生代中,每次回收都会有大量对象死亡,所以可以选择复制算法,只需要付出少量的对象复制成本就可以完成每次垃圾回收。对象在老年代存活的概率比较高,没有额外的空间来保证它的分配,所以我们必须选择“mark-clear”或者“mark-compact”算法进行垃圾回收。

  4.垃圾采集

  如果说回收算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。

  当我们比较采集器

时,并不是要挑出最好的。因为到目前为止还没有最好的垃圾采集

器,更不用说通用的垃圾采集

器了,我们能做的就是根据具体的应用场景选择适合自己的垃圾采集

器。

  除G1外,年轻代都是copy算法,老年代是mark-clean或mark-compact方式。

  老年代和新生代垃圾采集

器分类

  垃圾采集

器的组合

  4.1 串行采集

  串行采集

  虚拟机的设计者当然知道Stop The World带来的糟糕的用户体验,所以在后续的垃圾采集

器设计中不断缩短停顿时间(停顿还是有的,寻找最佳垃圾采集

器的过程还在继续) .

  Serial采集

器对应的参数

  4.2 ParNew 采集

  ParNew 采集

器实际上是 Serial 采集

器的多线程版本。除了使用多线程进行垃圾采集

外,其余行为(控制参数、采集

算法、回收策略等)与Serial采集

器完全相同。

  ParNew采集

  ParNew采集

器的参数

  它是许多以服务器模式运行的虚拟机的首选。除了Serial采集

器,只有CMS采集

器(真正的并发采集

器,后面会介绍)可以配合使用。

  添加了并行和并发概念:

  4.3 并行清除采集

  并行清除采集

  Parallel Scavenge 采集

器侧重于吞吐量(CPU 的有效使用)。CMS 等垃圾采集

器更关注用户线程暂停时间(改善用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU消耗的总时间的比值。Parallel Scavenge 采集

器提供了很多参数供用户找到最合适的暂停时间或最大吞吐量。如果对采集

器的运行不太了解,手动优化有难度,可以选择将内存管理优化交给虚拟机来完成。好的选择。

  4.4 系列老采集器

  Serial采集

器的老年代版本,也是单线程采集

器。它主要有两个用途:一是在JDK1.5及更早版本中与Parallel Scavenge采集

器配合使用,二是作为CMS采集

器的备份方案。

  4.5 并行旧采集

  Parallel Scavenge 采集

器的老一代版本。使用多线程和“标记和排序”算法。在吞吐量和 CPU 资源很重要的地方,可以优先考虑 Parallel Scavenge 采集

器和 Parallel Old 采集

器。

  4.6 CMS 采集

  气相色谱仪

  CMS 的四个步骤和优缺点

  CMS(Concurrent Mark Sweep)采集

器是一种旨在获得最短恢复停顿时间的采集

器。非常适合用在注重用户体验的应用上。

  CMS(Concurrent Mark Sweep)采集

器是HotSpot虚拟机第一个真正意义上的并发采集

器。这是垃圾采集

线程和用户线程(基本上)同时工作的第一次。

  从名字中的Mark Sweep这两个字可以看出,CMS采集

器是通过“标记-清除”算法实现的,其运行过程比以往的垃圾采集

器都要复杂。整个过程分为四个步骤:

  主要优势:

  - 并发采集

,低暂停

  缺点:

  - 对CPU资源敏感

  - 无法处理漂浮垃圾

  - 它使用的回收算法——“标记和清除”算法会导致采集

结束时产生大量空间碎片

  4.7 G1 采集

  G1采集

  G1采集

器核心理念

  G1标志回收流程

  G1(Garbage-First)是一个面向服务器的垃圾采集

器,主要针对配备多处理器和大容量内存的机器。在大概率满足GC停顿时间要求的同时,还具有高吞吐量的性能特点。

  G1采集

器的特点:

  G1采集

器的几个步骤:

  G1采集

器在后台维护一个优先级列表,每次根据允许的采集

时间,优先回收价值最高的Region(这就是它名字Garbage-First的由来)。这种使用Region划分内存空间和优先区域回收的方式保证了G1采集

器在有限的时间内尽可能多地采集

(通过将内存打零)

  参考:%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md

  解决方案:搞定JVM垃圾回收就是这么简单

  回顾以上:

  写在这部分前面的常见面试问题:

  文中提到了问题的答案

  这篇文章的*敏*感*词*

  当需要排查各种内存溢出问题,当垃圾回收被称为系统达到更高并发的瓶颈时,我们需要对这些“自动化”技术进行必要的监控和调整。

  Java 程序员必读的文档

  哈哈皮啦!本人开源的一个Java学习指导文档。一本涵盖大多数Java程序员需要掌握的核心知识的书正在逐步完善中,期待您的参与。Github 地址:. 看看吧,我想你不会后悔的,如果可以的话,可以给个Star鼓励一下!

  1 揭开JVM内存分配与回收的神秘面纱

  Java的自动内存管理主要是对象内存回收和对象内存分配。同时,Java自动内存管理的核心功能是堆内存中对象的分配和回收。

  JDK1.8之前的堆内存*敏*感*词*:

  从上图可以看出,堆内存分为新生代、老年代和永久代。新生代又分为:Eden区+Survior1区+Survior2区。值得注意的是,在JDK 1.8中整个永久代被移除,取而代之的是一个叫做Metaspace的区域(永久代使用JVM的堆内存空间,而元空间使用物理内存,直接受物理内存限制机)。

  1.1 对象先分配在eden区

  目前主流的垃圾采集

器都是采用分代采集

算法,所以需要将堆内存划分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾采集

算法。

  大多数情况下,对象都分配在年轻代的eden区。当eden区没有足够的空间分配时,虚拟机会发起一次Minor GC。下面我们来做一下实际测试。

  在测试之前,我们先来看看Minor Gc和Full GC的区别?

  测试:

  public class GCTest {

public static void main(String[] args) {

byte[] allocation1, allocation2;

allocation1 = new byte[30900*1024];

//allocation2 = new byte[900*1024];

}

}

  复制

  运行它:

  添加参数:-XX:+PrintGCDetails

  运行结果:

  从上图可以看出,eden区的内存几乎已经全部分配完毕(即使程序什么都不做,新生代也会使用2000k多的内存)。如果我们为 allocation2 分配内存会怎样?

  allocation2 = new byte[900*1024];

  复制

  简单解释一下为什么会这样:因为分配内存到allocation2的时候eden区的内存已经分配的差不多了,刚才我们说了当Eden区没有足够的空间分配时,虚拟机就会发起一次Minor GC。在GC的时候,虚拟机也发现allocation1不能存放在Survior空间,所以不得不通过allocation guarantee机制,提前将新生代中的对象转移到老年代。old generation中的空间足够存放allocation1,所以不会发生Full GC。Minor GC执行后,如果后面分配的对象在eden区可以存在,内存仍然会分配在eden区。代码验证可以如下进行:

  public class GCTest {

public static void main(String[] args) {

byte[] allocation1, allocation2,allocation3,allocation4,allocation5;

allocation1 = new byte[32000*1024];

allocation2 = new byte[1000*1024];

allocation3 = new byte[1000*1024];

allocation4 = new byte[1000*1024];

allocation5 = new byte[1000*1024];

}

}

  复制

  1.2 大对象直接进入老年代

  大对象是需要大量连续内存空间的对象(例如字符串、数组)。

  为什么一定要这样?

  为了避免在为大对象分配内存时由于分配保证机制带来的复制而降低效率。

  1.3 长寿对象会进入老年代

  由于虚拟机采用了分代采集

的思想来管理内存,因此在内存回收时,它必须能够识别出哪些对象应该放在新生代,哪些对象应该放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

  如果对象出生在Eden,在第一次Minor GC后存活下来,并且可以被Survivor容纳,则将其移动到Survivor空间,并将对象的年龄设置为1。每一次对象存活一次MinorGC在 Survivor 中,age 增加 1 年,当它的 age 增加到一定程度时(默认是 15 岁),就会被提升到老年代。可以通过参数-XX:MaxTenuringThreshold 设置将对象提升到Old Age 的年龄阈值。

  1.4 动态对象年龄判定

  为了更好地适应不同程序的内存情况,虚拟机并不总是要求对象年龄必须达到一定的值才能进入老年代。如果Survivor空间中所有同龄对象的大小之和大于Survivor空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代,无需达到要求的年龄。

  2 对象死了?

  

" />

  几乎所有的对象实例都放在堆中,在堆上进行垃圾回收之前的第一步是确定那些对象已经死了(即无论如何都不能再使用的对象)。

  2.1 引用计数

  向对象添加一个引用计数器。每当有对它的引用时,计数器就会加 1;

  这种方法实现简单,效率高,但是目前主流的虚拟机并没有选择这种算法来管理内存。主要原因是很难解决对象之间的循环引用问题。所谓对象之间的相互引用问题如下代码所示:除了对象objA和objB相互引用外,这两个对象之间没有任何引用。但是因为它们相互引用,它们的引用计数器不为0,所以引用计数算法无法通知GC采集

器回收它们。

  public class ReferenceCountingGc {

Object instance = null;

public static void main(String[] args) {

ReferenceCountingGc objA = new ReferenceCountingGc();

ReferenceCountingGc objB = new ReferenceCountingGc();

objA.instance = objB;

objB.instance = objA;

objA = null;

objB = null;

}

}

  复制

  2.2 可达性分析算法

  这个算法的基本思想是以一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索。节点经过的路径称为引用链。当一个对象没有任何引用链到 GC Roots 连接时,证明该对象不可用。

  可达性分析算法

  2.3 更多参考资料

  无论是通过引用计数的方法判断对象的引用次数,还是通过可达性分析的方法判断对象的引用链是否可达,判断对象的存活与“引用”有关。

  在JDK1.2之前,Java中对引用的定义很传统:如果引用类型数据中存储的值代表了另一块内存的起始地址,就说这块内存代表了一个引用。

  JDK1.2之后,Java扩展了引用的概念,将引用分为四种:强引用、软引用、弱引用、虚引用(引用强度逐渐减弱)

  1. 强引用

  我们之前使用的大部分引用其实都是强引用,也就是最常用的引用。如果一个对象有强引用,它就类似于生活必需品,垃圾采集

器永远不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误导致程序异常终止,也不会通过任意回收强引用对象来解决内存不足问题。

  2. 软引用(SoftReference)

  如果一个对象只有软引用,那么它类似于可以购买的商品。如果有足够的内存空间,垃圾采集

器将不会回收它。如果内存空间不够,这些对象的内存就会被回收。只要垃圾采集

器不采集

它,该对象就可以被程序使用。软引用可用于实现对内存敏感的缓存。

  软引用可以与引用队列(ReferenceQueue)结合使用。如果软引用引用的对象被垃圾回收,JAVA虚拟机会将软引用添加到与其关联的引用队列中。

  3.弱引用(WeakReference)

  如果一个对象只有弱引用,那么它类似于可以购买的商品。弱引用和软引用的区别在于只有弱引用的对象生命周期更短。在垃圾回收线程扫描其管辖内存区域的过程中,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。然而,由于垃圾采集

器是一个非常低优先级的线程,只有弱引用的对象可能无法快速找到。

  弱引用可以与引用队列(ReferenceQueue)结合使用。如果弱引用引用的对象被垃圾回收,Java虚拟机会将弱引用添加到与其关联的引用队列中。

  4.幻影参考(PhantomReference)

  “Phantom reference”,顾名思义,是没有用的。与其他类型的引用不同,幻象引用不决定对象的生命周期。如果一个对象只收录

虚引用,就好像它没有引用一样,可以随时被垃圾回收。

  幻影引用主要用于跟踪被垃圾采集

的对象的活动。

  幻影引用与软引用和弱引用的区别之一是幻影引用必须与引用队列(ReferenceQueue)结合使用。当垃圾回收器要回收一个对象时,如果发现它还有一个虚引用,就会把这个虚引用添加到与之关联的引用队列中,然后再回收该对象的内存。程序可以通过判断引用队列中是否加入了幻引用来获知被引用对象是否会被垃圾回收。如果程序发现引用队列中加入了虚引用,则可以在被引用对象的内存被回收之前采取必要的动作。

  特别要注意的是,弱引用和幻引用在编程中很少用到,软引用经常用到。这是因为软引用可以加快JVM对垃圾内存的回收,维护系统的安全,防止内存溢出。(OutOfMemory) 等问题。

  2.4 不可达对象不是“必死”

  即使是可达性分析方法中的不可达对象也不是“必死”的。对属性分析不可达的对象进行第一次标记,筛选一次,筛选条件为是否需要对该对象执行finalize方法。当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,虚拟机认为这两种情况不需要执行。

  判断需要执行的对象会被放入队列中进行二次标记,除非该对象与引用链上的任何对象相关联,才会真正被回收。

  2.5 如何判断一个常量是废弃常量

  运行时常量池主要回收废弃的常量。那么,我们如何判断一个常量是一个过时常量呢?

  如果常量池中存在字符串“abc”,如果当前不存在引用该字符串常量的String对象,则说明常量“abc”为废弃常量。如果此时发生内存回收并且有必要,“abc”就会被系统从常量池中清除掉。

  注意:我们也说过,JDK1.7及以后版本的JVM把运行时常量池移出了方法区,在Java堆(Heap)中开辟了一块区域来存放运行时常量池。

  2.6 如何判断一个类是无用类

  方法区主要是回收无用类,那么如何判断一个类是否为无用类呢?

  判断一个常量是否为“废弃的常量”比较简单,但是判断一个类是否为“无用类”的条件就比较苛刻。一个类需要同时满足以下三个条件才能被认为是“无用类”:

  虚拟机可以回收满足以上三个条件的无用类。这里说的只是“可以”,并不是像对象一样不使用就会被回收。

  3 垃圾回收算法

  垃圾采集

算法

  3.1 标记-扫描算法

  该算法分为“标记”和“清除”两个阶段:首先标记所有需要回收的对象,标记完成后统一回收所有标记的对象。它是最基本的采集

算法,效率很高,但是会带来两个明显的问题:

  效率问题空间问题(标记清除后会产生大量不连续的碎片)

  标记扫描算法

  3.2 复制算法

  

" />

  为了解决效率问题,出现了“复制”采集

算法。它可以将内存分成大小相同的两块,一次使用其中的一块。当这块内存用完后,将存活的对象复制到另一块内存中,然后一次性清理已用空间。这样每次内存回收就是回收一半的内存范围。

  复制算法

  3.3 标记整理算法

  根据一种特殊的基于老年代特点的标记算法,标记过程还是和“标记-清除”算法一样,只是后面的步骤不是直接回收可回收对象,而是移动所有存活的对象到一个section,然后直接清理掉end boundary之外的Memory。

  标记整理算法

  3.4 分代采集

算法

  目前虚拟机的垃圾回收采用分代回收算法。这个算法没有什么新意,只是根据对象生命周期的不同,把内存分成若干块。Java堆一般分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾回收算法。

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

  采访延伸问题:HotSpot为什么分新生代和老年代?

  根据上面对分代采集

算法的介绍回答。

  4 垃圾采集

  如果说回收算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。

  当我们比较采集器

时,我们并不是要挑选一个最好的采集器

。因为我们知道目前没有最好的垃圾采集

器,更谈不上万能的垃圾采集

器,我们能做的就是根据具体的应用场景选择适合自己的垃圾采集

器。试想一下:如果有一个完美的采集

器适用于四海之内的任何场景,那么我们的HotSpot虚拟机就不会实现这么多不同的垃圾采集

器。

  4.1 串行采集

  串行(serial)采集

器 采集

器是最基本也是最古老的垃圾采集

器。大家看名字就知道这个采集

器是单线程采集

器。它的“单线程”的含义不仅仅意味着它只会使用一个垃圾采集

线程来完成垃圾采集

工作,更重要的是它在执行垃圾采集

工作时必须暂停所有其他工作线程(“Stop The World”)直到采集

完毕。

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

  串行采集

  虚拟机的设计者当然知道Stop The World带来的糟糕的用户体验,所以在后续的垃圾采集

器设计中不断缩短停顿时间(停顿还是有的,寻找最佳垃圾采集

器的过程还在继续) .

  但是串行采集

器与其他垃圾采集

器相比有什么优势吗?当然有,简单高效(相对单线程的其他采集

器)。Serial采集

器由于没有线程交互开销,自然可以获得很高的单线程采集

效率。Serial 采集

器是在客户端模式下运行的虚拟机的不错选择。

  4.2 ParNew 采集

  ParNew 采集

器实际上是 Serial 采集

器的多线程版本。除了使用多线程进行垃圾采集

外,其余行为(控制参数、采集

算法、回收策略等)与Serial采集

器完全相同。

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

  ParNew采集

  它是许多以服务器模式运行的虚拟机的首选。除了Serial采集

器,只有CMS采集

器(真正的并发采集

器,后面会介绍)可以配合使用。

  添加了并行和并发概念:

  4.3 并行清除采集

  Parallel Scavenge 采集

器类似于 ParNew 采集

器。那么它有什么特别之处呢?

  -XX:+UseParallelGC

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

-XX:+UseParallelOldGC

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

  复制

  Parallel Scavenge 采集

器侧重于吞吐量(CPU 的有效使用)。CMS等垃圾采集

器的关注点更多的是用户线程的停顿时间(提升用户体验)。所谓吞吐量就是CPU中运行用户代码所花费的时间与CPU总消耗时间的比值。Parallel Scavenge 采集

器提供了许多参数供用户找到最合适的暂停时间或最大吞吐量。如果你不太了解采集

器的运行,如果存在手动优化,你可以选择将内存管理优化交给虚拟机来完成。这也是一个不错的选择。

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

  ParNew采集

  4.4.系列老采集器

  Serial采集

器的老年代版本,也是单线程采集

器。它主要有两个用途:一是与JDK1.5及更早版本的Parallel Scavenge采集

器一起使用,二是作为CMS采集

器的备份解决方案。

  4.5 并行旧采集

  Parallel Scavenge 采集

器的老一代版本。使用多线程和“标记和排序”算法。在注重吞吐量和CPU资源的情况下,可以优先考虑Parallel Scavenge采集

器和Parallel Old采集

器。

  4.6 CMS 采集

  CMS(Concurrent Mark Sweep)采集

器是一种旨在获得最短恢复停顿时间的采集

器。非常适合用在注重用户体验的应用上。

  CMS(Concurrent Mark Sweep)采集

器是HotSpot虚拟机第一个真正意义上的并发采集

器。这是垃圾采集

线程和用户线程(基本上)同时工作的第一次。

  从名字中的Mark Sweep这两个字可以看出,CMS采集

器是通过“标记-清除”算法实现的,其运行过程比以往的垃圾采集

器都要复杂。整个过程分为四个步骤:

  CMS 垃圾采集

  从它的名字就可以看出它是一个优秀的垃圾采集

器,主要优点:并发采集

,低暂停。但它有以下三个明显的缺点:

  4.7 G1 采集

  G1(Garbage-First)是一个面向服务器的垃圾采集

器,主要针对配备多处理器和大容量内存的机器。大概率满足GC停顿时间要求,同时具有高吞吐量的性能特点。

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

  G1采集

器的运行大致分为以下几个步骤:

  G1采集

器在后台维护一个优先级列表,每次根据允许的采集

时间选择回收值最高的Region(这就是它名字Garbage-First的由来)。这种使用Region划分内存空间和优先区域回收的方法保证了GF采集

器在有限的时间内(通过将内存打零)采集

尽可能多的内存。

  参考:

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线