JVM内存模型

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

在这里插入图片描述

1.程序计数器:

程序计数器是一个记录着当前线程所执行的字节码的行号指示器,是一块较小的内存空间。程序执行过程中解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。

这样沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的,并不需要程序计数器。假设程序永远只有一个线程,确实没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。

由于Java 虚拟机的多线程是通过线程轮流切换并分配CPU时间片的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,就必须知道它上次执行到哪个位置,JVM通过程序计数器来记录某个线程的字节码执行位置,因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

程序计数器的特点:

  • 线程隔离性,每个线程工作时都有属于自己的独立计数器。

  • 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计

  • 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。

  • 执行native本地方法时,程序计数器的值为空(Undefined)。

  • 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

2.虚拟机栈:

线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。

动画是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。

Java虚拟机栈可能发生如下异常情况:

  • 如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

  • 如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

HotSpot虚拟机的栈容量是不可以动态扩展的。

3.本地方法栈:

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

4.方法区:

方法区在虚拟机启动的时候被创建,它存储了每一个类的类型信息,这些信息是由类加载器在类加载时从类文件中提取出来的,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。

简单说方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化。而装载后的结果就是由.class文件转变为方法区中的一段特定的数据结构。

方法区可能发生如下异常情况: 如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常.

在HotSpot虚拟机中方法区对应的实现是PermGen space(永久代),抛出的异常为"java.lang.OutOfMemoryError: PermGen space",在JDK 1.8 中,已经没有"PermGen space"这个区间了,取而代之是 Metaspace(元空间)。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。超出该值后报错:“java.lang.OutOfMemoryError: Metaspace”

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

5.堆:

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,是在虚拟机启动的时候被创建,此内存区域的唯一目的就是存放对象实例,在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。

从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以将Java堆划分为“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等,
这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,是对《Java虚拟机规范》里Java堆的一种实现,而非某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》里对Java堆的进一步细致划分。HotSpot里面也有不采用分代设计的新垃圾收集器。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

垃圾回收:

1.哪些内存需要回收

垃圾收集器如何确定某个对象是“垃圾"?

  • 可达性分析:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),
    如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

    在这里插入图片描述

    对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。

    可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:
    如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选

在这里插入图片描述

这里的执行,是指虚拟机会触发这个 finalize 方法,但并不会等待它结束,这是因为 finalize 方法执行缓慢,可能会导致 F-Queue其他对象处于等待,甚至是崩溃。
如果在执行 finalize 时,对象重新和其他对象关联上了,则成功拯救了自己 ——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,
那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

注意:**任何一个对象的 finalize() 只会被系统调用一次,下次不会再执行。**
  • 引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
    引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高。
    主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
2.如何回收
2.1 垃圾回收算法
2.1.1 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,简单说,就是针对不同特征的java对象采用不同的 策略实施存放和回收,自然所用分配机制和回收算法就不一样。
在HotSpot虚拟机中,堆被划分成两个不同的区域:年轻代( Young )、老年代( Old );年轻代 又被划分为三个区域:Eden、From Survivor、To Survivor。

在这里插入图片描述

堆大小 = 年轻代 + 老年代(堆的大小可通过参数–Xms堆的初始容量、-Xmx堆的最大容量来指定,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值)。
其中,年轻代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Eden : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定。)

多数情况下新生成的对象首先都是放在年轻代的,年轻代都是”朝生夕死“生命周期短的对象,JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 既年轻代实际可用的内存空间为 9/10 ( 即90% )。
而大对象需要大量连续内存空间会直接进入老年代,在分配空间它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接进入老年代,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则

System.gc()方法的调用触发Full GC。在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM不一定会执行是,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc()。

2.1.2 标记-清除算法

最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。

这种算法实现简单不需要将对象进行移动,但是标记、清除之后会产生大量不连续的

2.1.3 标记-复制算法

标记-复制算法常被简称为复制算法,,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
但是这种复制回收算法的代价是将可用内存缩小为了原来的一半,浪费了空间。

现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。新生代中的对象有98%熬不过第一轮收集,因此并不需要按照1∶1的比例来划分新生代的内存空间。
HotSpot虚拟机把新生代分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor、To Survivor),默认的大小比例是Eden : from : to = 8 : 1 : 1 。每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。
也即每次新生代中可用内存空间为整个新生代容量的90%,只有一个Survivor空间,即10%的新生代是会被“浪费”的。

当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,没有办法百分百保证每次回收都只有不多于10%的对象存活,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(大多就是老年代)进行分配担保(Handle Promotion)。

2.1.4 标记-整理算法

其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。移动则内存回收时会更复杂,不移动则内存分配时会更复杂。
从垃圾收集的停顿(Stop The World)时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。

2.2 垃圾收集器

下图展示了七种作用于不同分代的HotSpot虚拟机收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

HotSpot虚拟机收集器

在谈论垃圾收集器的上下文语境中,并行和并发可以理解为:

  • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
2.2.1 Serial收集器 --串行回收

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择,并作为HotSpot中客户端模式下的默认新生代垃圾收集器。

这个收集器采用标记-复制算法、串行回收和"stop-the-World"机制的方式执行内存回收。

除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial old收集器。Serial old收集器同样也采用了串行回收和"stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。

Serial/Serial Old收集器运行示意图

Serial old是运行在客户端模式下默认的老年代的垃圾回收器,在服务器模式下主要有两个用途:1.与新生代Parallel scavenge收集器配合使用 2.作为老年代CMS收集器的后备垃圾收集方案

相比其他收集器Serial收集器简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC

2.2.2 ParNew收集器 --并行回收

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、标记-复制收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。

Par是Parallel的缩写,New:只能处理的是新生代,所以需要搭配其他老年代的收集器一起工作,除了Serial收集器外,目前只有它能与CMS收集器配合工作。

ParNew/Serial Old收集器运行示意图

对于新生代,回收次数频繁,使用并行方式高效。对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)

2.2.3 Parallel Scavenge收集器 --吞吐量优先

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法,“stop the world”机制实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,它的关注点与其他收集器不同,
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:

吞吐量 = 运行用户代码时间/(运行用户代码时间 + 运行垃圾收集时间)

Parallel old收集器采用了标记-整理算法,但同样也是基于并行回收和"stop-the-World"机制。在Java8中,默认是Parallel Scavenge收集器 + Parallel Old收集器

2.2.4 CMS收集器 --并发低停顿

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。他是基于标记-清除算法实现的,它的运作过程分为四个步骤,包括:

1)初始标记(CMS initial mark)
初始标记仅仅只是标记一下GCRoots能直接关联到的对象,需要“Stop The World”,但是速度很快;

2)并发标记(CMS concurrent mark)
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

3)重新标记(CMS remark)
重新标记阶段也需要“Stop The World”,为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

4)并发清除(CMS concurrent sweep)
并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
Concurrent Mark Sweep收集器运行示意图

CMS收集器在初始标记和重新标记阶段存在“Stop The World”,但时间比较短所以能做到低停顿,但是还存在至少有以下三个明显的缺点:

  1. 对处理器资源非常敏感:
    在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量.

    CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。

  2. 无法处理“浮动垃圾”(Floating Garbage):
    CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。

    同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集(默认92%,可通过参数-XX:CMSInitiatingOccupancyFraction设置),必须预留一部分空间供并发收集时的程序运作使用。

    但是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
    如果有大量的并发失败产生性能反而降低。

  3. 会产生大量空间碎片产生:
    CMS是一款基于“标记-清除”算法实现的收集器,不会移动内存会产生大量不连续的空间碎片。

    为了解决这个问题,CMS收集器提供了一个参数:
    -XX:+UseCMSCompactAtFullCollection开关参数(默认开启,从JDK 9开始废弃):在完成 Full GC 后是否要进行内存碎片整理,

    但频繁Full GC又会使停顿时间又会变长,所以还提供了另外一个参数来控制Full GC频率:
    -XX:CMSFullGCsBeforeCompaction(默认值为0,表示每次进入Full GC时都进行碎片整理,从JDK 9开始废弃),进行几次 Full GC 后就进行一次内存碎片整理

2.2.5 Garbage First收集器(G1收集器)–区域化分代式

G1收集器把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。

让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),
优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

2.3 内存分配与回收策略
2.3.1 对象优先在Eden分配

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

2.3.2 大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串或者元素数量很庞大的数组。

而当复制对象时,大对象就意味着高额的内存复制开销,所以大对象会直接进入老年代减少复制操作。在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。
HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。

老年代没有足够的连续空间来放置大对象,也会引起Full GC

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

2.3.4 动态对象年龄判定

HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

2.3.5 空间分配担保

在执行任何一次Minor GC之前,JVM会先检查一下老年代最大可用的连续空间大于新生代对象总和 或者 老年代可用空间大于"历次Minor GC升入老年代对象的平均大小"
两个条件满足一个,直接进行Minor GC。否则,进行Full GC后再进行Minor GC,Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时会导致"OOM"内存溢出

进行Minor GC后会有几种情况:

  • Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域
  • Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是小于老年代可用内存大小的,此时就直接进入老年代
  • Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,这个时候就会触发一次Full GC。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值