JVM——GC

2018/03/04

分4个篇幅说明。

一 简介

  1. Java Virtual Machine,Java语言的运行环境。最具吸引力的特性。
  2. 自身拥有完善的硬件结构:处理器,堆栈,寄存器等,具有相应的指令系统。
  3. 使Java程序只需生成Java字节码,从而运行在各个OS平台。
  4. JVM实例随着Java程序的启动而产生,关闭而消亡。

二 结构

  1. 垃圾回收器(Garbage Collection):负责堆内存中未被引用的对象的回收。
  2. 类装载子系统(ClassLoader Sub System):除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。
  3. 执行引擎(Execution Engine):负责执行那些包含在装载类的方法中的指令。
  4. 运行时数据区(Java Memory Alloction Area):Java内存,VM运行时需要从整个个计算机内存中划分一块内存区域存储数据。比如字节码,从一状态的class文件得到的奇特信息,程序创建的对象,传递给方法参数,返回值,局部变量等。

三 内存管理

1 内存分区

分为5个区域(Java 1.7后删掉了 Method Area): 程序计数器(Program Counter Register), 本地方法栈(Native Stack类似虚拟机栈,HotSpot 把虚拟机栈和本地方法栈合二为一,属于线程私有),方法区(Method Area), 虚拟机栈(Stack), 堆(Heap)。

1.2 PC Register: 又叫程序寄存器,记录当前线程所执行的Java字节码的地址。

1.3. Stack:又称堆栈。

  1. 特征
    • 线程私有,生命周期与线程相同。每个线程都有自己的独立的栈空间。
    • java方法执行的内存模型:每个方法被调用时会创建一个栈帧(Stack Frame)——用于存储局部变量表,中间状态的操作栈,动态链接,方 法出口等。方法中局部变量在线程空间中。
    • 每一个方法被调用到执行完成的过程,对应着一个栈帧在虚拟栈中入栈到出 栈的过程。
    • 局部变量存放了编译期的8种基本数据,对象引用(根据不同虚拟机的实现, 他可能是一个指向对象起始地址的引用指针,也可能是一个对象的句柄或者其 他与此对象相关的位置)。 线程栈只存 基本类型和对象地址。

1.4 Method Area:

```
以前 方法区 Method Area, Non-Heap,永久代PermGen。Java8移除了永	久代,并使用本地内存。
1 线程共享,存储虚拟机已经加载的类信息,常量,静态变量,及时编译后的		代码。
2 回收目标,对常量池和对象类型。
3 HotSpot 在java1.7后 将运行时字符串常量池移除了。
4 编译期生成的运行时常量池,包括符号引用(Symbols)、字面量(interned strings)都移到java Heap;
5 类的静态变量(class statics)移到Java Heap。

``` #### 1.5 Java Heap: 堆 线程共享的内存区域,存放实例对象,以及对象的属性。 1. 包含有
* 年轻代Young(又分为伊甸区(eden)和幸存区(Survivor from和to)——大部分对象在Eden中生成,当Eden区满后,还存活的对象被copy到Survivor区(S0或S1)
* 老年代Tenured——Survivor中还存活的对象copy到Tenured中,也就是生命周期较长的对象
* Java1.8后 存储 运行时常量池,static变量(原存储在方法区)

1.6 Metaspace: 元空间 并不在虚拟机中,而是使用本地内存。默认情况下,元空间大小受本地内存限制,但可以通过一些参数指定:

  1. 特征
    • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类 型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该 值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高 该值。为了减少GC频率,需要设置为一个比较高的值。
    • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
    • 除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
  2. 元空间内存管理——由元空间虚拟机完成。
    • 类和其元数据的生命周期和其对应的类加载器是相同的。
    • 每一个类加载器的存储区域可以看做是一个元空间组块,所有的元空间组块合成了一个元空间。
    • 一个类加载器被GC,对应的元空间也会被GC。元空间GC过程没有重定位、压缩等操作。但元空间内的元数据会进行扫描来确定Java引用。
    • 元空间虚拟机采用组块分配的形式。组块大小因加载器类型而异。 类加载器持有的组块又被分为多个单元(线性分配)。
  3. 元空间调优
    • 缺点:类信息并不是固定大小,分配的空闲区块和类需要的区块大小不同,导致内存碎片存在。目前并不支持压缩回收。
    • 通过配置一个元空间的参数进行优化。size,min,max设置合理的值。
  4. 为什么要用元空间替代永久代:
    • 字符串存在永久代中,容易出现性能问题和内存溢出。
    • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
    • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
    • Oracle 可能会将HotSpot 与 JRockit 合二为一。

JVM structure

2 内存溢出

  1. 虚拟机栈,本地方法栈溢出: StackOverflowError, OutOfMemoryError。
  2. Java堆溢出:创建大量对象并且对象生命周期很长,OutOfMemoryError。
  3. 运行时常量区溢出:OutOfMemoryError。这里一个典型的例子就是String的intern方法, 当大量字符串使用intern时,会触发此内存溢出。
  4. 方法区溢出: 方法区存放class等元数据信息,OutOfMemoryError。

3 内存模型JMM

  1. 定义了多线程之间共享变量的可见性
  2. 如何在需求的时候对共享变量进行同步
  3. 是并发编程的基础。线程间的通信和同步。
  4. 单独模块去介绍。

四 垃圾回收

1 GC工作流程

  1. 标记(Mark):找出引用不为0的对象。
    • 方法:找到所有GC roots,将他们放到队列中,依次递归遍历根节点,引用的所有子节点和子节点,将所有被遍历的节点标记成live。弱引用不会被标记。
  2. 计划(Plan):判断是否需要压缩。遍历当前的generation(代代)所有标记live,根据特定算法判断。
  3. 清理(Sweep):回收所有Free空间。遍历live/Dead,把所有live实例中间的内存块加入可用内存链表中去。
  4. 引用更新(Relocate):将所有引用的地址进行更新。
    • 方法:计算出压缩后的每个对象的新地址,找到GC Root,遍历所有子节点,将被遍历到的节点中的引用地址进行更新,也包括弱引用
  5. 压缩(Compact):减少内存碎片,根据计算出来的新地址把对象移动到响应位置上。

2 GC算法

  1. 引用计数: 属于最原始的回收算法,但Java中没有使用这种算法。
    • 频繁计数影响性能
    • 无法处理循环引用等问题。
  2. 标记清除: 是很多垃圾回收算法的基础。标记、清除两个步骤。
    • 标记:遍历所有的GC Roots,并从GC Roots可达的对象设置为存活对象。
    • 清除:遍历堆中所有对象,将没有被标记可达的对象清除。
    • 缺点:
      • 标记过程完成后,又产生新的对象,却未被标记,将视为不可达对象而被清除。程序出错。
      • 大量的内存遍历工作,执行性能较低,程序吞吐量降低。
      • 对象被清除后,留下的内存空间不再连续,浪费内存。
  3. 标记压缩:在标记清除算法的基础上,增加了压缩过程。
    • 解决了内存不连续的问题。
    • 在压缩过程中,对象内存地址会发生改变,java程序只能等待压缩完成后继续。
  4. 复制算法:
    • 内存一分为二,只是用其中一份。
    • 将正在使用的那份内存中存活的对象复制到另一份空白内存中。
    • 最后将正在使用的内存空间的对象清除,完成回收。
    • 缺点:
      • 不适合存活对象太多的情况。复制性能较差。为降低复制成本,一般复制算法用于内存空间中新生代的垃圾回收,新生代内存中存活对象较少。
      • 内存空间占用成本高,因为它基于2份内存空间做对象复制,在非垃圾回收的周期内只用到一份内存空间,内存利用率较低
  5. 可达性分析法:根搜索算法
    • 使用一系列GC Roots对象作为起始点,遍历搜索走过的因引用链,当一个对象到GC Roots没有任何引用链相连时,对象不可用。
     每个应用程序都包含一组根(root)。每个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null。
     在应用程序中,只要某对象变得不可达,也就是没有根(root)引用该对象,这个对象就会成为垃圾回收器的目标。
     用一句简洁的英文描述就是:GC roots are not objects in themselves but are instead references to objects.而且,Any object referenced by a GC root will automatically survive the next garbage collection.
    
    

    在Java中,可以当做GC Root的对象有以下几种:

    1. 虚拟机(JVM)栈中的引用的对象
    2. 方法区中的类静态属性引用的对象
    3. 方法区中的常量引用的对象(主要指声明为final的常量值)
    4. 本地方法栈中JNI的引用的对象

3 GC器

回顾堆内存结构:新生代(伊甸区 + 2*幸存区(from + to)) 和 老年代。
GC 主要针对以上堆空间,当然也对针对元数据区(永久区)。

1. 串行回收器:单线程。

* 新生代——复制算法,老年代——标记压缩算法。
* 最古老最稳定。但回收时间长。

2. 并行回收器:多线程。针对新生代和老年代是否都使用并行?

  1. ParNew回收器: 新生代——复制算法,并行,老年代——标记压缩算法,串行。
    • 多核条件下,优于串行回收器。
    • 配置启动: -XX:+UseParNewGC
    • 指定并发数:-XX:ParallelGCThreads
  2. Parallel回收器: 两种配置。
    • 同ParNew相同:新生代——复制算法,老年代——标记压缩算法,但吞吐量优。通过配置:-XX:+UseParallelGC
    • 新生代,老年代均并行,XX:+UseParallelOldGC
  3. CMS(Concurrent Mark Sweep并发标记清除):并发表示它可以与应用程序并发执行、交替执行;标记清除表示这种回收器不是使用的是标记压缩算法
    • 流程:
      • 初始标记——标记从GC Root可以直接可达的对象;
      • 并发标记——主要标记过程,标记全部对象;
      • 重新标记——由于并发标记时,用户线程依然运行,因此在正式清理前,再做依次重新标记,进行修正;
      • 并发标记——和用户线程一起,基于标记结果,直接清理对象。
    • 配置:-XX:+UseConcMarkSweepGC
    • 特点:
      • 新生代和老年代都必须是连续的。
      • 优点:回收器与应用程序并发执行,减少应用程序的停顿时间。
      • 缺点:不会集中一段时间去回收,并且回收时应用程序还在运行,导致回收不彻底。同时回收的频率相较其他回收器要高,频繁的回收将影响应用程序的吞吐量。
  4. Java1.7后推出G1(Garbage First),试图取代CMS。
    • G1相对CMS回收器来说优点在于:
      • 因为划分了很多区块,回收时减小了内存碎片的产生;
      • G1适用于新生代和老年代,而CMS只适用于老年代。
  5. 配置回收的参数:

     -XX:+UseSerialGC:在新生代和老年代使用串行收集器
     -XX:+UseParNewGC:在新生代使用并行收集器
     -XX:+UseParallelGC :新生代使用并行回收收集器,更加关注吞吐量
     -XX:+UseParallelOldGC:老年代使用并行回收收集器
     -XX:ParallelGCThreads:设置用于垃圾回收的线程数
     -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
     -XX:ParallelCMSThreads:设定CMS的线程数量
     -XX:+UseG1GC:启用G1垃圾回收器
    

4 GC触发条件

  1. Minor GC: 就是Young GC。Java对象优先在Eden分配。当Eden没有足够空间分配时,将发起一次Minor GC。频率快,回收速度快。
  2. Full GC:
    • 老年代分配策略:大对象(需要大量连续空间的java对象,如长的字符串和数组)直接进入老年代;年轻代长期存活的对象。
    • 包括Old GC,老年代满、创建新对象却内存不够、从年轻代To区复制对象却内存不够时 进行Full GC, 不频繁,并且回收速度比Minor GC慢10倍以上。1次Full GC,至少伴随着1次Minor GC。若2次Full GC仍无法满足内存分配时,JVM将报OOM异常。
    • 执行Young GC时候预测其promote的object的总size超过老年代剩余size;
    • App调用System.gc时,但不一定执行。
    • 系统调用finalize。
    • Heap dump GC,默认也是出发Full GC。
    • 如果有Perm永久代的话,空间不足也会触发Full GC。

参考文档

Java1.6 1.7 1.8 内存对比

Java1.8

Java1.8MetaSpace

文内导航