java虚拟机

   java    java  jvm

java虚拟机

image

运行时数据区域

Java程序在运行时,会为JVM单独划出一块内存区域,而这块内存区域又可以再次划分出一块运行时数据区,运行时数据区域大致可以分为五个部分:

image

从上面的图中,有两种颜色不同的区域,红色的是线程共享区域,绿色的是线程私有区域。

堆(Heap)
  • 存储的是我们new来的对象,不存放基本类型和对象引用。
  • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。
  • 线程共享区域,因此是线程不安全的。
  • 能够发生内存溢出,主要有OutOfMemoryError和StackOverflowError。

虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常

线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常

Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

image

虚拟机栈(VM Stack)
  • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。
  • 存放基本数据类型以及对象的引用。
  • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。
  • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。
  • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
本地方法栈(Native Method Stack)

本地方法栈其实可以和Java虚拟机栈进行对比理解,
唯一不同的是本地方法栈是Java程序在调用本地方法的时候创建栈帧的地方。
和JVM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError。

方法区(Method Area)
  • 线程共享区域,因此这是线程不安全的区域。
  • 方法区也是一个可能会发生OutOfMemoryError的区域。
  • 方法区存储的是从Class文件加载进来的静态变量、类信息、常量池以及编译器编译后的代码。

常量池可以分为Class文件常量池以及运行时常量池,Java程序运行后,Class文件中的信息被字节码执行引擎加载到了方法区,从而形成了运行时常量池。

方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。不过Java 8以后就没有永久代这个说法了,元空间取代了永久代。

程序计数器(Program Counter Register)

进程是资源分配的最小单位,线程是CPU调度的最小单位,一个进程可以包含多个线程, Java线程通过抢占的方法获得CPU的执行权。现在可以思考下面这个场景。

某一次,线程A获得CPU的执行权,开始执行内部程序。但是线程A的程序还没有执行完,在某一时刻CPU的执行权被另一个线程B抢走了。后来经过线程A的不懈努力,又抢回了CPU的执行权,那么线程A的程序又要从头开始执行?

这个时候程序计数器就粉墨登场了,它的作用就是记录当前线程所执行的位置。 这样,当线程重新获得CPU的执行权的时候,就直接从记录的位置开始执行,分支、循环、跳转、异常处理也都依赖这个程序计数器来完成。此外,程序计数器还具有以下特点:

  • 线程私有,每一个线程都有一个程序计数器,因此它是线程安全的。
  • 唯一一块不存在OutOfMemoryError的区域,可能是设计者觉得没必要。

对象的创建与访问

对象的创建

当虚拟机遇到字节码new指令时,就会去运行时常量池寻找该实例化对象相对应的类是否被加载、解析和初始化。如果没有被加载,就会先加载该类的信息,否则就为新生对象分配内存。

分配内存无非有两种方法:

  • 指针碰撞:通过一个类似于指针的东西为对象分配内存,前提是堆空间是相对规整的。
  • 空闲列表:堆空间不规整,使用一个列表记录了哪些空间是空闲的,分配内存的时候会更新列表。

以上是两种不同的方法,至于虚拟机使用哪一种方法,这个就取决虚拟机的类型了。

对象的内存布局

对象在堆中的存储布局可以分为三个部分:

  • 对象头

    • 第一类信息:存储对象自身的运行时数据,例如哈希码、GC分代年龄、锁状态标志等等。
    • 第二类信息:指针类型,Java虚拟机通过这个指针来确定该对象是那个类的实例。
  • 实例数据:对象真正存储的有效信息。

  • 对齐填充:没有实际的意义,起着占位符的作用。

对象的访问定位

对象实例存储在Java堆中,通过这个对象引用我们就可以找到对象在堆中的位置。但是,对于如何定位到这个对象,不同的Java虚拟机又有不同的方法。

  • 使用句柄访问,通常会在Java堆中划分一块句柄池。
  • 使用直接指针,这样Java虚拟机栈中存储的就是该对象在堆中的地址。

image

image

这两种访问对象的方法各有优势。使用直接指针进行访问,就可以直接定位到对象,减小了一次指针定位的时间开销(使用句柄的话会通过句柄池的指针二次定位对象),最大的好处就是速度更快。但是使用句柄的话,就是当对象发生移动的时候,可以不用改变栈中存储的reference,只需要改变句柄池中实例数据的指针。

垃圾收集算法

论对象已死?

判断一个对象是否被销毁有两种方法:

  • 引用计数算法: 为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。但计数器为0的时候,就表白该对象没有被引用。
  • 可达性分析算法: 通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。

image

就像上图的那样,绿色部分的对象都在GC Roots的引用链上,就不会被垃圾回收器回收,灰色部分的对象没有在引用链上,自然就被判定为可回收对象。

下面列举可以作为GC Roots的对象:

  • Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,比如引用类型的静态变量。
  • 方法区中常量引用的对象。
  • 本地方法栈中所引用的对象。
  • Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
  • 被同步锁(synchronized)持有的对象。

垃圾回收算法主要有三种,依次是标记-清除算法、标记-复制算法、标记-整理算法

标记–清除算法

见名知义,标记–清除算法就是对无效的对象进行标记,然后清除。如下图:

image

对于标记–清除算法,你一定会清楚看到,在进行垃圾回收之后,堆空间有大量的碎片,出现了不规整的情况。在给大对象分配内存的时候,由于无法找到足够的连续的内存空间,就不得不再一次触发垃圾收集。另外,如果Java堆中存在大量的垃圾对象,那么垃圾回收的就必然进行大量的标记和清除动作,这个势必造成回收效率的降低。

标记–复制算法

标记–复制算法就是把Java堆分成两块,每次垃圾回收时只使用其中一块,然后把存活的对象全部移动到另一块区域。如下图:

image

标记–复制算法有一个很明显的缺点,那就是每次只使用堆空间的一半,造成了Java堆空间使用率的的下降。

现在大部分Java虚拟机的垃圾回收器使用的就是标记–复制算法,但是,对于Java堆空间的划分,并不是简单地一分为二。

image

首先得从两个分代收集理论说起:

  • 弱分代假说:大多数对象的生命存活时间很短。
  • 强分代假说:经过越多次垃圾收集的对象,存活的时间就越久。

正是这两个分代假说,使得设计者对Java堆的划分更加合理。下面,来说一下GC的分类:

  • Minor GC/Young GC:针对新生代的垃圾收集。
  • Major GC/Old GC:针对老年代的垃圾收集。
  • Full GC:针对整个Java堆以及方法区的垃圾收集。

好了,知道了GC的分类,是时候知道GC的流程了。

通常情况下,初次被创建的对象存放在新生代的Eden区,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域。以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区。可以看到,这两块Survivor区我们每一次只使用其中的一块,这样也仅仅是浪费了一块Survivor区。

每经历过一次垃圾回收的对象,它的分代年龄就加1,当分代年龄达到15以后,就直接被存放到老年代中。

还有一种情况,给大对象分配内存的时候,Eden区已经没有足够的内存空间了,这时候该怎么办?对于这种情况,大对象就会直接进入老年代。

标记–整理算法

标记–整理算法算是一种折中的垃圾收集算法,在对象标记的过程,和前面两个执行的是一样步骤。但是,进行标记之后,存活的对象会移动到堆的一端,然后直接清理存活对象以外的区域就可以了。这样,既避免了内存碎片,也不存在堆空间浪费的说法了。但是,每次进行垃圾回收的时候,都要暂停所有的用户线程,特别是对老年代的对象回收,则需要更长的回收时间,这对用户体验是非常不好的。如下图:

image

HotSpot的算法细节

根节点枚举

根节点枚举,其实就是找出可以作为GC Roots的对象,在这个过程中,所有的用户线程都必须停下。
到目前为止,几乎还没有虚拟机可以做到GC Roots遍历与用户线程并发执行。
当然,可达性分析算法中最耗时的寻找引用链的过程已经可以做到和用户线程并发执行了。那么,为什么需要在根节点枚举的时候停止用户线程?

其实也不难考虑,如果进行GC Roots遍历的时候,用户线程没有暂停,根节点集合的对象引用关系还在不断发生变化,这样遍历到的结果是不准确的。那么,Java虚拟机在查找GC Roots的时候,是真的需要进行全局遍历?

其实不是这样的,HotSpot虚拟机通过一个叫做OopMap的数据结构,可以知道哪些地方存储了对象引用。这样,大大减小了GC Roots的遍历时间。

安全点

安全点,是线程能够中断的点。我们在GC Roots遍历的时候,是一定要让用户线程停下来的。
为了使得线程到达最近的安全点停下来,有两种思路:

  • 抢先式中断: 暂停所有的用户线程,如果哪条线程没有在安全点,就恢复这条线程执行,直到它跑到安全点上在中断。不过没有Java虚拟机采用这种思路。
  • 主动式中断: 不对线程进行操作,仅仅设置一个简单的标志位,线程执行的时候不断区轮询这个标志位,当这个标志位为真的时候,线程就在离自己最近的安全点挂起。
安全区域

安全区域是安全点的拉伸和扩展,安全点解决了如何让线程停下,却没有解决如何让虚拟机进入垃圾回收状态。

安全区域是指能能够确保在某一代码片段中,引用关系不会发生变化的区域。因此,一旦线程进入了安全区域,就可以不去理会这些处于安全区域的线程。当线程离开安全区域的时候,虚拟机就会检查是否完成了根节点枚举。

记忆集与卡表

首先,跨代引用是存在的。因此,垃圾收集器在新生代建立了一个叫做记忆集的数据结构,用来避免把整个老年代假如GC Roots的扫描范围。

记忆集是抽象的数据结构,而卡表是记忆集的具体实现,这种关系就类似与方法区与元空间。

写屏障

写屏障的作用很简单,就是对卡表进行维护和更新

并发的可达性分析

前面我们说到过为什么要暂停所有的用户线程(这个动作也被称之为Stop The World)?
这其实是为了不让用户线程改变GC Roots对象的引用。试想,如果用户线程能够随便把死亡的对象重新标记为存活,或者把存活的对象标记为死亡,这岂不是会使的程序发生意想不到的错误。

经典的垃圾收集器

Serial 收集器

Serial 收集器是最基础、历史最悠久的收集器,它在进行垃圾收集的时候会暂停所有的工作线程,直到完成垃圾收集过程。
下面是Serial垃圾收集器的运行示意图:
image

ParNew 收集器

ParNew 垃圾收集器实则是Serial 垃圾收集器的多线程版本,这个多线程在于ParNew垃圾收集器可以使用多条线程进行垃圾回收。
image

Parallel Scavenge 收集器

也是一款新生代垃圾收集器,同样的基于标记–复制算法实现的。
它最大的特点是可以控制吞吐量
image

Serial Old 收集器

Serial Old 收集器是Serial 收集器的老年代版本。
其垃圾收集器的运行原理和Serial 收集器是一样的。
image

Parallel Old 收集器

Parallel Old 收集器同样是Parallel Scavenge 收集器的老年代版本,支持多线程并发收集。
下面就是它的运行示意图:
image

CMS 收集器

前面说到过Parallel Scavenge 收集器,它是一个可以控制吞吐量的垃圾收集器。现在要说的CMS 收集器,它是一个追求最短停顿时间的垃圾收集器,基于标记–清除算法实现的。
CMS 垃圾收集器的运作过程相对前面几个垃圾收集器来说比较复杂,整个过程可以分为四个部分:

  • 初始标记: 需要Stop The World,这里仅仅标记GC Roots能够直接关联的对象,所以速度很快。
  • 并发标记: 从关联对象遍历整个GC Roots的引用链,这个过程耗时最长,但是却可以和用户线程并发运行。
  • 重新标记: 修正并发时间,因为用户线程可能会导致标记产生变动,同样需要Stop The World。
  • 并发清除: 清除已经死亡的对象。
Garbage First 收集器

Garbage First(简称G 1)收集器是垃圾收集器发展史上里程碑式的成果,主要面向服务端应用程序。
另外G 1收集器虽然还保留新生代和老年代的概念,但是新生代和老年代不在固定,它们都是一系列区域的动态集合。

  1. java虚拟机
    1. 运行时数据区域
      1. 堆(Heap)
      2. 虚拟机栈(VM Stack)
      3. 本地方法栈(Native Method Stack)
      4. 方法区(Method Area)
      5. 程序计数器(Program Counter Register)
    2. 对象的创建与访问
      1. 对象的创建
      2. 对象的内存布局
      3. 对象的访问定位
    3. 垃圾收集算法
      1. 论对象已死?
        1. 标记–清除算法
        2. 标记–复制算法
      2. 标记–整理算法
    4. HotSpot的算法细节
      1. 根节点枚举
      2. 安全点
      3. 安全区域
      4. 记忆集与卡表
      5. 写屏障
      6. 并发的可达性分析
    5. 经典的垃圾收集器
      1. Serial 收集器
      2. ParNew 收集器
      3. Parallel Scavenge 收集器
      4. Serial Old 收集器
      5. Parallel Old 收集器
      6. CMS 收集器
      7. Garbage First 收集器
Redis集群
EventBus、AsyncEventBus