JVM核心知识(一)

61 篇文章 4 订阅
2 篇文章 0 订阅

JVM知识介绍主要内容包括JVM内存、JMM、垃圾回收算法、JVM调优等知识点。此篇内容整合自《Java编程思想》和《深入理解JVM》,算是我对这两本书JVM相关的一点读书笔记。时间充足的小伙伴可以购买并仔细阅读,相信收获一定很大,如果时间不充裕,可以看看此篇博客,希望有所帮助。

前言

每个对象都有一个引用计数器,当有引用连接至对象时,引用计数器加1.当引用离开作用域或被置为null时,引用计数减1.垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间(引用计数模式通常会在计数值变为0的时候立刻释放对象)。这个做法有个缺陷,如果对象之间存在循环引用,可能会出现 “对象应该被回收,但是引用计数器却不为零”的情况。对于垃圾会收器而言,定位这样的交互自引用的对象组所需的工作量极大。

引用计数通常用来说明垃圾收集的工作方法,似乎从没有被应用于任何一种Java虚拟机实现中。

在一些更快的模式中,垃圾回收器并非基于引用计数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问的过的对象必须都是活的。这就解决了“交互自饮用的对象组”问题,这种对象根本不会被发现,因此也就被自动回收了。

在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。

有一种做法名为停止-复制(stop-and-copy)。显然这意味着,先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,他们是一个个挨着的,所以新堆保持紧凑队列,然后就可以按照前述方法简单、直接的分配新空间了。

当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,他们在遍历的过程中才能被找到(可以想象成有个表格,将旧地址映射至新地址)。

对于这种所谓的“复制式回收器”,效率会降低。

  1. 首先,要有两个堆空间,然后需要在这两个堆空间之间来回复制,从而得维护比实际需要多一倍的空间。某
    些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
  2. 第二个问题在与复制。程序进入稳定状态之后,可能只会产生少量的垃圾,甚至没有产生垃圾。尽管如此,
    复制式回收器仍然会将所有的内存从一处复制到另一处,这很浪费。

为了避免这种情况,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转到另外一种工作模式:标记-清理模式,Sun公司早期的Java虚拟机使用了这种技术。对于一般用途而言,标记-清理模式相当慢,但是当你知道只会产生少量垃圾甚至不会产生垃圾时,它的速度就很快了。

标记-清理所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活的对象,就会给对象一个标记,这个过程不会回收任何对象。只有全部标记工作做完之后,清理动作才会开始。在清理过程中,没标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就能得重新整理剩下的对象。

停止-复制的意思是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序会被暂停。在Sun公司的文档中发现,许多资料将垃圾回收视为低优先级的后台进程,但事实上年垃圾回收在Sun公司早期版本的Java虚拟机中并非以这种方式体现的。当可用内存数量较低时,Sun版本的垃圾回收器会暂停运行程序,同样,“标 记-清理”工作也必须在程序暂停的情况下才能进行。

如前文所说,在这里所讨论的Java虚拟机中,内存分配以较大的块为单位。如果对象较大,它会占有单独的块。严格来说,“停止-复制”要求在释放旧对象之前,必须把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的 代数(generation count)来记录它是否还存回。通常,如果块在某处被引用,其代数会增加。垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。
垃圾回收器会定期进行完整的清理动作–大型对象仍然不会被复制(只会代数增加),内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到标记-清理的方式;同样,Java虚拟机会跟踪标记清理的结果,要是堆空间出现了很多碎片,就会切换回停止-复制方式。这就是自适应技术:自适应的、分代的、停止复制、标记-清理 式的垃圾回收器。

一 运行时数据区

在这里插入图片描述
首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
在这里插入图片描述
底色红色部分为线程私有。

1.程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、程序恢复等基本功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器就是一个核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的程序计数器互不影响,独立存储,我们称这类区域为“线程私有”的内存。

如果线程正在执行的是个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规OutOfMemoryError情况的区域。

2.Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
在这里插入图片描述
经常有人把Java内存分为堆内存(Heap)和栈内存(Stack) ,这种分法比较粗糙,Java内存去的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会讲到。而所指的栈内存就是现在所说的虚拟机栈,或者说是虚拟机栈中的局部变量表部分
指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
局部变量表存放了编译期可知的各种基本数据类型,对象引用类型。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表所需的内存空间在编译期见完成分配,当进入一个方法时,这个方法需要在帧中分配多大局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机锁允许的深度,将抛出StackOverflowError异常;
  • 如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

3.本地方法栈

本地方法栈与虚拟机栈作用相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机用到的Native方法服务。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflow和OutOfMemoryError异常。

4.堆

对大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆时被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在堆上分配内存。这一点在Java虚拟机规范中描述的是:所有的对象实例以及数组都要在堆上分配,但是JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换等技术使在堆上分配变得不那么“绝对”了。JIT编译器和逃逸分析在以后博客里单独说一下,欢迎关注。

Java堆是垃圾收集器管理的主要区域,因此很多时候被称为GC堆(Garbage Collected Heap) ,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,再细致一点的有 Eden空间,Form Survivor空间、To Survivor空间等。
在这里插入图片描述
从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与内容无关,无论哪个区域都是存储的对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
根据java 虚拟机规范的规定,java堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现固定大小的,也可以是可扩展的,不过当非主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError异常。

5.方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是Java堆区分开。
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定发小或者可扩展外,还可以选择不识闲垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和类型的卸载,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

JDK8已经取代了方法区,改为元空间(Meatspace),本篇不在展开介绍,在以后的博客在介绍jdk7\8\9\11等不同版本的变化,敬请关注。

运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生产的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,运行期间也可能将新的常量放入池中,这种特性运行比较多的就是String类的intern()方法。

直接内存
直接内存并不是Java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
在JDK 1.4中新加入了NIO类。引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用
Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用
进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

二 垃圾收集器与内存分配策略

GC需要完成的3件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

运行时数据区中,程序计数器、虚拟机栈、本地方法栈是线程私有的,同时也和线程的生命周期紧密相关,所以它们随着线程而灭亡。而Java堆和方法区则不一样,一接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不同,我们只有在程序处于运行期间才能知道会创建哪些对象。也就是说这部分的内存分配和回收是动态的,垃圾收集器所关注的就是这部分内存。

对象已经死了吗?

  1. 引用计数法
    很多教科书判断对象是否存活的算法是:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当应用失效时,计数器值就减1;任何时候计数器为0的对象都是不可能在被使用的。
    客观的说引用计数法的实现简单,判断效率也高,在大部分情况下也是不错的算法。但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中主要的原因是它很难解决对象之间相互循环引用的问题。
  2. 可达性分析算法
    在主流的商用程序语言都是称通过可达性分析来判断对象是否存活的。这个算法的基本思路是通过一系列的称
    为“GC-Roots”的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径就是引用链,当一个独享到GC
    Roots 没有任何引用链相连时,则证明次对象是不可用的。
    在Java语言中,可作为GC Roots 的对象包括:
  • 虚拟机栈(栈帧中的局部变量)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
  1. 再谈引用
    在JDK1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹但是很狭隘,一个对象在这种顶一下只有被引用或者没有被引用的两种状态,对于如何描述一些鸡肋的对象,显得无能为力。我们洗完能描述这样一类对象:当内存空间足够时,则能够保留在内存中;如果没有足够的内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,四种引用强度依次减弱。
  • 强引用
    强引用就是指在程序代码中普遍存在的,类似“Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用
    软引用是用来描述一些还有用但是并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
  • 弱引用
    弱引用也是用来描述非必须的对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。
  • 虚引用
    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
  1. 非死不可?
    即使在可达性分析算方法中不可达的对象,也不是非死不可,这时候它们暂时处于缓刑的阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链。那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行**finalize()**方法。当方法没有覆盖finalize()方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
    如果这个对象被判定为有必要执行finalize方法,那么这个对象就会被放置在一个叫做F-Queue的队列中,并在稍
    后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize方法中执行缓慢,或者发生了死循环,将可能导致F-Queue队列中其他对象永久处于等待。甚至导致整个内存回收系统崩溃。
    finalize方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize中重活,只要重新与引用链上的任何一个对象建立关联即可。第二次标记时它将被移除即将回收的集合;如果没有逃脱,将被回收。任何一个对象的finalize方法都只会被系统自动调用一次
  2. 回收方法区(永久代)
    很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾回收的性价比确实比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
    永久代的垃圾回收主要回收:废弃常量和无用的类。
    无用的类的判定:
  • 该类所有的实例都已经被回收
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

由于垃圾收集算法的实现涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不相同,因此我们不过多讨论算法的实现,只是介绍几种算法的思想及发展过程。

1.标记-清除算法

标记-清除(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清理”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前面对象标记判定时已经介绍过了。之所以说它是最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进得到的。它的主要不足有两个:

  • 效率问题,标记和清理两个过程的效率都不高
  • 另一个是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行
    过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

在这里插入图片描述

2. 复制算法(新生代)

复制算法,它可以将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后在把使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要一动堆顶指针,按顺序分配内存即可,实现简单,运行高效,只是这种算法的代价是将内存缩小为原来的一半

IBM 研究表明,新生代中的对象98%都是“.朝生夕死”的,所以并不需要按照1:1的比例进行划分空间,而是将内存分为一块较大的Eden和 两块较小的Survivor空间,每次使用Eden和其中一块Survivor,两个Survivor一个称为Form区,一个称为To区。HotSpot虚拟机默认Eden:Survivor=8:1,Eden:Survivor From : Survivor To = 8:1:1.

当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor,最后清理刚才使用的空间。当Survivor空间不够用时,需要依赖其他内存空间(老年代)进行分配担保,这些对象直接进入老年代。

HotSpot实现复制算法流程:

  • 当Eden区满的时候,会除服第一次Minor gc,把还活着的对象拷贝到Survivor form区;当Eden区再次触发Minor gc 的时候,会扫描Eden区和Form区,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和Form区域清空。

  • 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空

  • 部分对象会在From和To区域中来回复制,如此交换15次,(由JVM参数maxTenuringThreshold决定,默认值为15),最终如若还是存活,就存入到老年代。

在这里插入图片描述

Minor GC:新生代GC,指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以MinorGC非常频繁,一般回收速度也比较快
Major GC/ Full GC:老年代GC,指发生在老年代的GC,出现了MajorGC,经常会伴随着至少一次的MinorGC(并非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 Major GC的速度一般比Minor GC慢10倍以上

3. 标记-整理算法

复制算法在对象存活率较高时就要进行比较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都存活的极端情况,所以在老年代中不直接选用这种算法。
标记整理算法,标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
在这里插入图片描述

4. 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”。根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,
新生代:每次垃圾收集时都发现大批的对象死去,只有少量存活,那么就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法进行回收。

垃圾收集器

  • Serial收集器
    最基础的收集器,单线程的收集器,必须暂停其他所有的工作线程,直到它收集结束。
    新生代:复制算法,暂停所有用户线程
    老年代:标记整理算法,暂停所有用户线程
  • ParNew收集器
    是Serial收集器的多线程版本。
    新生代:复制算法,暂停所有用户线程
    老年代:标记整理算法,暂停所有用户线程
  • Parallel Scavenge收集器
    吞吐量优先的收集器,停顿时间短,响应速度快,适合后台运算而不需要太多交互的任务。,自适应调节策
  • Serial Old收集器
    Serial收集器的老年代版本,单线程收集器。
  • Parallel Old 收集器
    Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
  • CMS收集器
    是一种以获取最短回收停顿时间为目标的收集器,适合B/S系统的服务端,互联网站。
  • G1收集器
    G1(Garbage-First)收集器是当前收集器技术发展的前沿成果。G1是一款面向服务端应用的垃圾收集器。
    G1具有以下特点:
    • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(核心)来缩短停顿的时间,
      部分其他收集器原本需要停顿的Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序
      继续执行。
    • 分代收集:虽然G1收集器可以不需要其他收集器配合就能独立管理整个GC堆,但它能用不同的方式去
      处理新创建的对象和已经存活一段时间的对象,以及熬过多次GC的旧对象以获取更好的收集效果。
    • 空间整合:G1从整体上看是基于“标记-整理”算法实现的收集器,从局部上看是基于“复制”算法实现,但
      是无论如何,这两种算法都意味着G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。有利于长时间运行程序,分配大对象时不会因为无法找到连续内存空间而提前出发下一次GC。
    • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒,这几乎已经是实时Java的垃圾收集器的特征了。

内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发生一次Minor GC。

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,例如很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能够识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每经历一次Minor GC后被复制到Survivor区中,则将年龄加1岁,当它的年龄增加到一定程度(默认为15),就将会被放置到老年代中。

动态对象年龄判定

为了更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须到达阈值的时候才能晋升到老年代,如果在Survivor空间中幸存所有对象大小之和大于Survivor空间的一半,年龄大于等于此年龄的对象就可以直接进入老年代,无须等到最大年龄值(MaxTenuringThreshold)中要求的年龄。

空间分配担保(触发Young GC)
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代中所有对象空间,如果这个条件成立,那么Minor GC可以确保是安全的,直接执行MinorGC。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果不允许,则直接FullGC.如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时就改为进行一次Full GC。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aotulive

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值