算法 自动采集列表(为什么要根据不同对象区分年轻代和年老代代? )

优采云 发布时间: 2021-11-30 21:12

  算法 自动采集列表(为什么要根据不同对象区分年轻代和年老代代?

)

  基本概念

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

  Java内存分配和回收的机制可以概括为:分代分配和分代回收。对象会根据存活时间分为:年轻代(Young Generation)、老年代(Old Generation)、永久代(Method Area)。再详细一点:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。

  

  上图中的eden区、s0(“From”)区、s1(“To”)区都属于新生代,tentrired区属于老年代。

  为什么要根据对象不同来区分年轻代和老年代?目前主流的垃圾回收器都会采用分代回收算法,所以需要将堆内存划分为年轻代和老年代,以便我们可以根据各个时代的特点选择合适的垃圾回收算法。

  年轻代:当一个对象被创建时,内存分配首先发生在年轻代(大对象可以直接在老年代创建),大部分对象在创建后很快就不再使用,所以很快变得不可达,所以是被年轻代的 GC 机制清理掉(IBM 的研究表明 98% 的对象会很快消亡)。这种 GC 机制称为 Minor GC 或 Young GC。注意,Minor GC 并不意味着年轻代内存不足,它实际上只是在 Eden 区进行 GC。

  年轻代上的内存分配是这样的。年轻代可以分为三个区域:伊甸园区(伊甸园,亚当夏娃吃了禁果和生娃娃的地方,用来表示内存最先分配的区域,比较合适)和两个领域。生存区域(幸存者 0、幸存者 1)

  当Eden区已满时,执行Minor GC清理死对象,将剩余的对象复制到一个生存区Survivor0(此时Survivor1为空,两个Survivor之一为空),以及该对象也会增加1。之后,每次Eden区满时,执行一次Minor GC,将剩余的对象添加到Survivor0;当 Survivor0 也已满时,将还活着的对象直接复制到 Survivor1 中。在Eden区执行Minor GC后,将Survivor1添加到剩余的对象中(此时Survivor0为空)。当两个生存区切换多次时(HotSpot虚拟机默认为15次,由-XX:MaxTenuringThreshold控制,大于这个值进入老年),仍然存活的对象(实际上只是一小部分,

  老年代:如果对象在年轻代存活的时间足够长而没有被清理(也就是存活了几次 Young GC),就会被复制到老年代。空间一般比年轻代大,可以存储更多的对象,老年代发生的GC次数比年轻代少。当老年代内存不足时,会执行Major GC,也称为Full GC。

  如果对象比较大(如长字符串或大数组),Young空间不足,则直接将大对象分配到老年代(大对象可能会触发早期GC,应谨慎使用,应避免使用生命周期短的大对象)

  可能存在老年代对象引用年轻代对象的情况。如果需要进行Young GC,可能需要查询整个old generation来判断是否可以清理,这显然是低效的。解决办法是在老年代维护一个512字节的块——“卡片表”,老年代对象引用新生代对象的所有记录都记录在这里。在 Young GC 中,只在这里检查而不是检查所有的老年代,因此性能大大提高。

  

  Minor GC 和 Full GC 有什么区别?

  当对象即将存储在eden中,但是eden中没有空间,并且正在发生MinorGC,并且S0或S1没有可用空间时,这时候会发生什么?新生代对象通过分配保证机制提前转移到老年代。老年代的空间足以存放allocation1,所以不会有Full GC。

  什么是大物,为什么会直接进入老年?大对象是需要大量连续内存空间的对象(如字符串、数组)。为避免在为大对象分配内存时,由于分配保证机制导致的复制导致效率降低。

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

  如何判断对象已经死亡?

  堆中几乎有所有的对象实例。在堆上进行垃圾回收之前的第一步是确定那些对象已死(即,不能再以任何方式使用的对象)。有两种方法可以识别它们: 1、 引用计数器方法 2、 可达性分析算法

  引用计数器:向对象添加引用计数器。每当有对它的引用时,计数器加 1;当引用无效时,计数器减 1。任何时候计数器为 0 的对象都不能使用。但是目前主流的虚拟机都没有选择这种算法来管理内存。主要原因是难以解决对象之间的循环引用问题。以下循环引用代码:

  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;

}

}

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

  

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

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

  从JDK1.2开始,Java扩展了引用的概念,将引用分为强引用、软引用、弱引用、幻象引用四种(引用的强度逐渐减弱)。

  在实际程序设计中,一般很少使用弱引用和幻像引用,很多情况下使用软引用。这是因为软引用可以加速JVM对垃圾内存的回收,维护系统的安全,防止内存溢出(OutOfMemory)等问题

  

  1.强引用(StrongReference)

  我们使用的大部分引用实际上都是强引用,也就是最常用的引用。如果一个对象有一个强引用,它就像一个必不可少的家庭用品,垃圾采集器永远不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError使程序异常终止,也不会通过随意回收强引用对象来解决内存不足问题。

  public class StrongReference {

public static void main(String[] args) {

new StrongReference().method1();

}

public void method1(){

Object object=new Object();

Object[] objArr=new Object[Integer.MAX_VALUE];

}

  

  但是需要注意的是,当method1结束时,object和objArr已经不存在了,所以它们指向的对象会被JVM回收。如果你想打破强引用和对象之间的关联,你可以显式地将该引用赋值为null,以便JVM在合适的时候回收该对象。

  2.软引用(SoftReference)

  软引用用于描述一些有用但不是必需的对象。它们由 Java 中的 java.lang.ref.SoftReference 类表示。对于软引用关联的对象,JVM 只会在内存不足时回收该对象。所以这一点可以用来解决OOM问题,而且这个特性非常适合实现缓存:比如网页缓存、图片缓存等。软引用可以和引用队列(ReferenceQueue)结合使用。如果软引用所引用的对象被JVM回收,则软引用将被添加到与其关联的引用队列中。

  import java.lang.ref.SoftReference;

public class SoftRef {

public static void main(String[] args){

System.out.println("start");

Obj obj = new Obj();

SoftReference sr = new SoftReference(obj);

obj = null;

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

System.out.println("end");

}

}

class Obj{

int[] obj ;

public Obj(){

obj = new int[1000];

}

  

  当内存足够大时,可以将数组存放在软引用中,取数据时可以从内存中取数据,提高了运行效率

  软引号在实践中有着重要的应用,比如浏览器的后退按钮。后台显示的网页内容可以重新请求或从缓存中检索:

  (1)如果一个网页在浏览结束时被回收,当你按Back查看之前浏览过的页面时,需要重新构建

  (2)如果把浏览过的网页存放在内存中,会造成大量的内存浪费,甚至造成内存溢出,这时候就可以使用软引用了。

  3.弱引用(WeakReference)

  弱引用也用于描述非必要对象。JVM 进行垃圾回收时,无论内存是否充足,都会回收弱引用关联的对象。在 Java 中,它由 java.lang.ref.WeakReference 类表示。

  如果一个对象只有弱引用,它类似于一个可有可无的生活用品。弱引用和软引用的区别在于,只有弱引用的对象生命周期更短。在垃圾采集器线程扫描其管辖的内存区域的过程中,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。但是,由于垃圾采集器是低优先级线程,因此可能不会很快找到只有弱引用的对象。弱引用可以与引用队列(ReferenceQueue)结合使用。如果弱引用所引用的对象被垃圾回收,Java 虚拟机会将弱引用添加到与其关联的引用队列中。

  因此,与软引用关联的对象只有在内存不足时才会被回收,而与弱引用关联的对象在 JVM 进行垃圾回收时总是会被回收。

  

  应用场景:如果一个对象偶尔使用,使用的时候想随时获取,又不想影响这个对象的垃圾回收,应该使用弱引用来记住这个对象。或者你想引用一个对象,但是这个对象有自己的生命周期,你不想干预这个对象的生命周期。这时候就应该使用弱引用。这个引用不会对对象的垃圾回收判断产生任何额外的影响。

  4.幻影参考(PhantomReference)

  幻象引用不同于之前的软引用和弱引用,它不影响对象的生命周期。在 Java 中,它由 java.lang.ref.PhantomReference 类表示。如果一个对象关联了一个幻影引用,就如同没有关联的引用一样,随时可能被垃圾采集器回收。幻象引用主要用于跟踪被垃圾采集的对象的活动。

  幻像引用必须与引用队列关联使用。当垃圾采集器即将回收一个对象时,如果发现它还有一个幻象引用,就会把这个幻象引用添加到与之关联的引用队列中。程序可以通过判断引用队列中是否添加了虚拟引用来判断引用的对象是否会被垃圾回收。如果程序发现引用队列中加入了一个幻象引用,它可以在被引用对象的内存被回收之前采取必要的行动。

  import java.lang.ref.PhantomReference;

import java.lang.ref.ReferenceQueue;

public class PhantomRef {

public static void main(String[] args) {

ReferenceQueue queue = new ReferenceQueue();

PhantomReference pr = new PhantomReference(new String("hello"), queue);

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

}

}

  无法访问的对象必须死吗?即使在可达性分析方法中,不可达对象也不是“必须死”的。要真正声明一个对象死亡,它必须经过至少两个标记过程;可达性分析方法中的unreachable对象是先标记一次,过滤一次,过滤的条件是这个对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用时,虚拟机认为这两种情况都没有必要。

  确定要执行的对象将被放入第二个标记的队列中。除非该对象与引用链中的任何对象相关联,否则它实际上将被回收。

  如何判断一个常量是否为过时常量?1、 在没有任何引用的情况下,如果常量池中存在字符串“abc”,如果当前没有引用字符串常量的String对象,则表示常量“abc”是一个过时的常量。如果此时发生内存回收,如有必要,系统会从常量池中清除“abc”。

  如何判断一个班级没用?方法区主要是回收无用的类,那么如何判断一个类是无用的呢?一个类需要同时满足以下三个条件才能被认为是“无用类”:

  常见的垃圾回收算法

  

  标记清除算法 算法分为“标记”和“清除”阶段:首先标记所有需要回收的对象,标记完成后统一回收所有标记的对象。这种垃圾回收算法会带来两个明显的问题:

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

  

  复制算法 为了解决效率问题,出现了“复制”采集算法。它可以将内存分成大小相同的两块,每次使用其中一块。当这块内存用完时,将幸存的对象复制到另一个块中,然后再次清理已用空间。这样,每次内存回收就是回收一半的内存范围。(复制活着的,清除所有)

  

  标记组织算法是一种基于老年特征提出的标记算法。标记过程仍与“标记-清除”算法相同,但后续步骤不是直接回收可回收对象,而是将所有幸存对象移动到一端。然后直接清理结束边界外的内存。(移动活动,清除死者)

  分代采集算法根据对象的生命周期将内存划分为若干块。一般将java堆分为新生代和老年代,这样我们就可以根据每一代的特点选择合适的垃圾回收算法。比如在新生代中,每次回收都会有大量的对象死亡,所以可以选择复制算法,只需要支付少量的对象复制成本就可以完成每次垃圾回收。对象在老年代存活的概率比较高,没有多余的空间来保证,所以垃圾采集必须选择“mark-sweep”或者“mark-sort”算法。

  垃圾采集器如下:

  串行采集器:“单线程”的含义不仅意味着它只会使用一个垃圾采集线程来完成垃圾采集工作,更重要的是它必须挂起所有其他工作线程(“Stop The World”)直到结束其采集。新生代采用复制算法,老年代采用标记清理算法。优点:当然简单高效(相对于其他采集器的单线程)。Serial采集器没有线程交互开销,自然可以获得很高的单线程采集效率。串行采集器是在客户端模式下运行的虚拟机的不错选择。

  

  ParNew 采集器实际上是 Serial 采集器的多线程版本。除了使用多线程进行垃圾采集外,其余行为(控制参数、采集算法、回收策略等)与Serial采集器完全相同。新生代采用复制算法,老年代采用标记清理算法。它是许多在服务器模式下运行的虚拟机的首选。除了Serial采集器,只有它可以和cms采集器(真正的并发采集器,后面会介绍)一起工作。

  并行和并发概念补充:

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

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

  

  并行 Scavenge 采集器

  -XX:+UseParallelGC 使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC 使用 Parallel 收集器+ 老年代并行

  新生代采用复制算法,老年代采用标记清理算法。它看起来几乎和 ParNew 一样。那么它有什么特别之处呢?Parallel Scavenge 采集器侧重于吞吐量(CPU 的有效使用)。cms 等垃圾采集器更关心用户线程的暂停时间(提升用户体验)。所谓吞吐量,就是在CPU中运行用户代码所用的时间与CPU所消耗的总时间之比。Parallel Scavenge 采集器提供了许多参数供用户找到最合适的暂停时间或最大吞吐量。如果你对采集器的操作不是很了解,如果手动优化有困难,可以选择将内存管理优化交给虚拟机。好的选择。

  

  Serial Old Collector 旧版本的Serial Collector,也是单线程采集器。它有两个主要用途:一是与JDK1.5及之前版本的Parallel Scavenge采集器配合使用,二是作为cms采集器的备份方案。

  Parallel Old 采集器 Parallel Scavenge 采集器的旧版本。使用多线程和“标记和排序”算法。在关注吞吐量和 CPU 资源时,可以优先考虑 Parallel Scavenge 采集器和 Parallel Old 采集器。

  cms 采集器

  

  cms(Concurrent Mark Sweep) 采集器是一个采集器,其目标是获得最短的恢复暂停时间。这是因为cms采集器工作时,GC工作线程和用户线程可以并发执行,达到减少采集暂停时间的目的。

  cms采集器只对老年代的采集起作用。它基于标记扫描算法。其操作过程分为4个步骤:

  主要优点:并发采集,低暂停。但它有以下三个明显的缺点:

  G1采集器G1重新定义了堆空间,打破了原有的分代模型,将堆划分为区域。这样做的目的是不必在整个堆的范围内做采集,这是它最大的特点。区域划分的优势在于它带来了一个可预测暂停时间的采集模型:用户可以指定采集操作将完成多长时间。也就是说,G1 提供了近乎实时的采集特性。. 它具有以下特点:

  G1采集器在后台维护一个优先级列表,每次根据允许的采集时间,先选择恢复值最高的区域(这就是它名字Garbage-First的由来)。这种使用Region来划分内存空间和优先区域回收,保证了GF采集器可以在有限的时间内采集到尽可能高的(内存减少到零)。

  G1 采集器将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然新生代和老代的概念仍然保留,但新生代和老代在物理上不再分离。他们都是其中的一部分。区域的集合(不需要是连续的)。Region的大小是一样的。该值是 1M 到 32M 字节之间的 2 的幂。JVM 会尝试划分大约 2048 个相同大小的 Region。其实这个数字是可以手动调整的,而G1也会根据堆大小自动调整。

  

  G1 采集器之所以能够建立可预测的暂停时间模型,是因为它可以系统地避免在整个 Java 堆中进行垃圾采集。G1 将通过合理的计算模型计算和量化每个 Region 的采集成本。这样,在给定“暂停”时间限制的情况下,采集器始终可以选择一组合适的 Region 作为采集。目标就是让其采集开销满足这个约束,从而达到实时采集的目的。

  对于计划从cms或ParallelOld采集器迁移的应用,根据官方推荐,如果发现满足以下特性,可以考虑更换为G1采集器,以追求更好的性能:

  G1采集的操作流程大致如下:

  堆栈中引用的全局变量和对象可以收录在根集合中,这样在查找垃圾时,可以从根集合中扫描堆空间。在 G1 中,引入了一种可以添加到根集的新类型,即记住集。记忆集(也称为 RSet)用于跟踪对象引用。G1的很多开源都是从Remembered Set衍生出来的,比如它通常占Heap size的20%左右甚至更多。而且,我们在复制对象的时候,因为需要扫描和更改Card Table信息,这个速度影响了复制的速度,进而影响了暂停时间。

  

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线