0%

JAVA虚拟机

JAVA虚拟机

概念

  • 是可运行java代码的虚拟机,它包括一套字节指令集,一组寄存器,一个垃圾回收,一个方法存储域,一个栈和一个堆.
  • 它是一种软件,运行在操作系统中,与硬件没有直接交互

运行过程

  • Java源文件通过编译器生成对应的.class字节码文件,而字节码文件通过JVM中加载器和解释器生成对应机器上的机器码,从而通过系统被执行
  • 大致过程如下:
    • Java源文件–->编译器–>字节码文件–->JVM–>机器码

内存管理

内存区域

​ 可大致分为以下3大区域

  • 线程私有区域
    • 本地方法区
      • 调用native方法
      • 会发生异常
        • 若线程请求的栈升读大于JVM所允许的深度异常抛出StackOverflowError
        • 允许动态扩展,若无法申请到足够内存异常抛出OutOfMemoryError
      • 和线程的生命周期相同
      • 在一个线程总每调用一个方法会创建一个栈帧,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
        • 栈帧的结构如下
          • 局部变量表 | 操作数栈
          • 动态链接 | 方法出口
      • 会发生异常
        • 若线程请求的栈升读大于JVM所允许的深度异常抛出StackOverflowError
        • 允许动态扩展,若无法申请到足够内存异常抛出OutOfMemoryError
    • 程序计数器
      • 指向虚拟机字节码指令的区域
      • 唯一一个不会有OOM的区域
  • 线程共享区域
    • 堆 (类实例区)
      • 新生代
      • 老年代
      • 异常—-OutOfMemoryError
    • 静态方法区
  • 直接内存
    • 不受jvm gc管理

运行时内存

区域划分大致如下:

新生代
  • Eden
    • Java新对象的出生地.当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收
      • 如果新创建的对象占用内存很大,则直接分配到老年代
  • ServivorFrom
    • 上一次GC的幸存者,做为这次GC的被扫描者
  • ServivorTo
    • 保留了一次MinorGC过程中的幸存者
  • MinorGC 的过程(复制->清空->互换)
    • eden、 servivorFrom 复制到 ServivorTo,年龄+1
      • 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年
        龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 位置不够就放到老年区);
    • 清空 eden、 servivorFrom
      • 然后,清空 Eden 和 ServicorFrom 中的对象;
    • ServivorTo 和 ServivorFrom 互换
      • 最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom
        区。
老年代

主要存放应用程序中生命周期长的内存对象.老年代的对象比较稳定,所以 MajorGC 不会频繁执行。

  • 在进行 MajorGC 前一般都先进行
    了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

  • 当无法找到足
    够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

  • MajorGC 采用标记清除算法:

    耗时比较长,因要扫描再回收。 会产生内存碎片,为了减
    少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。

    当老年代也满了装不下的
    时候,就会抛出 OOM(Out of Memory)异常。

    • 首先扫描一次所有老年代,标记出存活的对象,
    • 然后回收没有标记的对象。
持久代

指内存的永久保存区域,

  • 主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被
    放入永久区域,

  • 它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。

  • 永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常

  • JAVA8 与元数据

    在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

    元空间
    的本质和永久代类似,元空间与永久代之间最大的区别在于:

    元空间并不在虚拟机中,而是使用
    本地内存。默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native
    memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由
    MaxPermSize 控制, 而由系统的实际可用空间来控制。


垃圾回收与算法

  • 哪些内存需要回收?
    • heap堆内存的回收
  • 什么时候需要回收?
    • 当新生代中Eden内存不足时,会触发MinorGC
    • 当新生代中有对象晋升入老年代,空间不足时会触发MajorGC
  • 采用什么方式回收?
    • 通过引用记数法和可达性分析来确定哪些是需要回收的垃圾
    • 通过垃圾收集器根据对应的算法来收集并回收

垃圾确认

引用记数法
  • 算法分析

    • 引用计数是垃圾收集器中的早期策略。
    • 堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。
    • 当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。
    • 任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
  • 优缺点

    • 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
    • 缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。引用计数永远不可能为0。

    是不是很无趣,来段代码压压惊

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ReferenceFindTest {
    public static void main(String[] args) {
    MyObject object1 = new MyObject();
    MyObject object2 = new MyObject();

    object1.object = object2;
    object2.object = object1;

    object1 = null;
    object2 = null;
    }
    }

      这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1object2赋值为null,也就是说object1object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

可达性分析
  • 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用
  • GC Roots的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)
    • 方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)
    • 方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)
    • 本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

  

最终判定

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  • 第一次标记

    • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记。
  • 第二次标记

    • 第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己—-只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

方法区如何判断是否需要回收

  方法区存储内容是否需要回收的判断可就不一样咯。方法区主要回收的内容有:

​ 废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

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

流程图如下:

垃圾回收器

串行收集器
  • 串行收集器组合 Serial + Serial Old

    开启选项:-XX:+SerialGC

    • 串行收集器是最基本、发展时间最长、久经考验的垃圾收集器,也是client模式下的默认收集器配置。
    • 串行收集器采用单线程stop-the-world的方式进行收集。
      • 当内存不足时,串行GC设置停顿标识,待所有线程都进入安全点(Safepoint)时,应用线程暂停,串行GC开始工作,采用单线程方式回收空间并整理内存。
    • 单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优势。串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。
并行收集器
  • 并行收集器组合 Parallel Scavenge + Parallel Old

    开启选项:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)

    • 并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代Parallel Scavenge收集器上。
      • 并行收集器与串行收集器工作模式相似,都是stop-the-world方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。
      • 关注吞吐量主要指年轻代的Parallel Scavenge收集器,通过两个目标参数-XX:MaxGCPauseMills和-XX:GCTimeRatio,调整新生代空间大小,来降低GC触发的频率。
    • 并行收集器适合对吞吐量要求远远高于延迟要求的场景,并且在满足最差延时的情况下,并行收集器将提供最佳的吞吐量。
并发标记清除收集器
  • 并发标记清除收集器组合 ParNew + CMS + Serial Old

    开启选项:-XX:+UseConcMarkSweepGC

    • 并发标记清除(CMS)是以关注延迟为目标、十分优秀的垃圾回收算法,开启后,年轻代使用STW式的并行收集,老年代回收采用CMS进行垃圾回收,对延迟的关注也主要体现在老年代CMS上。
      • 年轻代ParNew与并行收集器类似,而老年代CMS每个收集周期都要经历:
        • 初始标记:以STW的方式标记所有的根对象,多线程执行;
        • 并发标记:同应用线程一起并行,标记出根对象的可达路径;
        • 重新标记:在进行垃圾回收前,CMS再以一个STW进行重新标记,标记那些由mutator线程(指引起数据变化的线程,即应用线程)修改而可能错过的可达对象;
        • 进行回收:最后得到的不可达对象将在并发清除阶段进行回收。
    • CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
    • 缺点:
      • 由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,
        • CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间;
        • 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。
      • CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

上面的垃圾收集器组合,都有几个共同点:

  • 年轻代、老年代是独立且连续的内存块;

  • 年轻代收集使用单eden、双survivor进行复制算法;

  • 老年代收集必须扫描整个老年代区域;

  • 都是以尽可能少而块地执行GC为设计原则。

Garbage First (G1)收集器

开启选项:-XX:+UseG1GC

G1垃圾收集器以关注延迟为目标、服务器端应用的垃圾收集器,虽然G1也有类似CMS的收集动作:

初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:

  • G1的设计原则是”首先收集尽可能多的垃圾(Garbage First)”。

    • G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。
    • G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。

    • 由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
  • G1也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。

    • G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
  • G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。

    • 即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

G1的内存模型

分区概念

  • G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。
  • 在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;
  • 每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
  • 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。

通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;

G1对内存空间的浪费较高,但通过首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

垃圾收集算法

  • 标记-清除算法

    采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收

    标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

复制算法

  复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

标记-整理算法

  标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。

标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。具体流程见下图:

分代收集算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

  • 年轻代(Young Generation)的回收算法

    • 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
    • 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)
      • 大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
    • 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
    • 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
  • 年老代(Old Generation)的回收算法

    • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。可以认为年老代中存放的都是一些生命周期较长的对象。
    • 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
  • 持久代(Permanent Generation)的回收算法

  用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区

常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器,图是借用过来滴:

  • Serial收集器(复制算法)
    新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
  • Serial Old收集器(标记-整理算法)
    老年代单线程收集器,Serial收集器的老年代版本。
  • ParNew收集器(停止-复制算法) 
    新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
  • Parallel Scavenge收集器(停止-复制算法)
    并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
  • Parallel Old收集器(停止-复制算法)
    Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
  • CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
    高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

GC是什么时候触发的

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

  • 年老代(Tenured)被写满;

  • 持久代(Perm)被写满;

  • System.gc()被显示调用;

  • 上一次GC之后Heap的各域分配策略动态变化;

引用类型

  无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在Java语言中,将引用又分为强引用、软引用、弱引用、虚引用4种,这四种引用强度依次逐渐减弱。

  • 强引用

  在程序代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。引用计数算法还是可达性分析算法都是基于强引用而言的。

  • 软引用

  用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用

  也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用

  也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。