windawings

Java Virtual Machine(JVM)
#tree{min-height:20px;padding:19px;margin-bottom:20px;bo...
扫描右侧二维码阅读全文
12
2018/03

Java Virtual Machine(JVM)

目录(Index)

jvm architecture

运行时数据区域(Runtime Memory Aera)


  包括共享内存区(方法区, 堆, GC主要关注区域)和线程隔离内存区(虚拟机栈, 本地方法栈和程序计数器)。

程序计数器(Program Counter Register)

  字节码解释器通过程序计时器选取下一条执行的字节码指令(分支, 循环, 跳转, 异常处理以及线程恢复等)。

  • 执行Java方法时, 计数器记录线程正在执行的虚拟机字节码指令地址; Native方法时, 计数器为空
  • 计数器内存区是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

虚拟机栈(Stack)

  虚拟机栈是Java方法执行的内存模型, Java方法执行需要创建一个栈帧存储局部变量表, 操作数栈, 动态连接以及方法出口等信息。

  • 栈内存指虚拟机栈中局部变量表(基本数据类型, 对象引用, return地址类型(指向一条字节码指令的地址))。其中64位长度longdouble占用两个局部变量空间, 其余数据类型占用一个。
  • 线程请求大于虚拟机所允许深度抛出StackOverflowError; 虚拟机拓展时无法申请到足够内存抛出OutOfMemoryError.本地方法栈同理。

(Heap)

  Java堆唯一目的为分配几乎所有对象实例, 是垃圾回收(garbage collector)的主要管理区域, 细分为新生代和老年代以便更好更快得回收和分配内存。

  • 堆在逻辑内存中连续(即物理内存上可不连续)
  • 堆分配实例失败或堆无法拓展时抛出OutOfMemoryError

方法区(Method Area)

  方法区存储虚拟机加载的类信息, 常量, 静态变量以及即时编译器编译后的代码等数据。

  • 不需要物理连续的内存, 可以选择固定内存大小, 可拓展
  • 可以选择不实现GC, 该区域主要回收常量池和类型卸载
  • 方法区分配内存失败抛出OutOfMemoryError
  • class文件包含版本, 字段, 方法, 接口以及常量池(存放编译期生成的字面量和符号引用, 类加载后存入方法区的运行时常量池中)等描述信息
  • 运行时常量池为方法区的一部分, Java常量不一定只在编译期产生, 也可以在运行中产生, 比如String类的intern方法

Hotspot


  • new → 检查参数(是否需要类加载) → 分配内存(指针碰撞法和空闲列表法) → Init方法执行对象初始化
  • 内存中的对象包含对象头, 实例数据对齐填充三块区域
  • 对象访问有直接指针访问法和句柄访问法(先访问句柄池获取到对象内存地址, 再访问到对象)

对象创建(Object Creation)

  对象创建即new指令时, 先检查指令参数是否定位到一个类的符号引用, 符号引用的类是否已被加载解析和初始化过(若没有需要执行类加载); 后为新对象分配内存, 即从堆中划分确定大小的内存(有指针碰撞空闲列表分配方式), GC带有压缩整理功能则Java堆规整, 即可采用指针碰撞分配方式(即已用与空闲泾渭分明, 只需要向空闲内存区移动指针), 反之空闲列表(记录已用与空闲内存区的列表); new指令结束后执行Init方法进行对象初始化。

对象内存(Object Memory)

  • 对象头$
    \begin{cases}
    \text{对象运行时数据(如哈希码, GC分代年龄, 锁状态标志, 线程持有锁, 偏向线程ID, 偏向时间戳), 64位64bits, 32位32bits}\\
    \text{类型指针(对象指向其类元数据的指针, 用于虚拟机判断对象实例的类, 确定其Class指针), 64位64bits(压缩开启32bits), 32位32bits}
    \end{cases}
    $

  • 对齐填充: 要求8bytes对齐, 不足需要补位
  • 实例数据
基本数据类型 占用空间(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8
reference 4(32位)/8(64位)

对象访问(Object Access)

  • 句柄方式: Java堆中会划分出句柄池(包含对象实例数据和类型数据的具体地址)提供对象的句柄访问方式, 引用中存储的即是对象的句柄地址(对象被移动时只需要改变句柄中的实例数据指针而不需要修改引用的句柄地址值)
  • 指针方式: 直接指针访问方式则使得引用存储的是对象的地址, 节省了一次指针定位的时间开销, 对于访问频繁的对象可减少可观的执行时间成本

内存溢出(Out Of Memory)


堆溢出(Heap Overflow)

  在不断创建对象过程中保证GC Roots与对象间有可达路径避免GC清除, 此时对象实例数量撑到最大堆容量限制时抛出内存溢出异常。

  • 可通过工具查看溢出对象到GC Roots的引用链, 掌握溢出对象类型信息和GC Roots引用链信息可定位溢出代码
  • 若最大堆限制设置过低, 可检查虚拟机的堆参数(-Xmx-Xms)是否可调大, 可在代码上检查是否存在对象生命周期过长, 持有状态时间过长等情况以减少内存消耗

栈溢出(Stack Overflow)

  在Hotspot中-Xoss参数(设置本地方法栈大小)实际上是无效的, 栈容量只由-Xss决定。

  • 单线程时无论栈帧过大还是虚拟机栈容量过小, 分配内存失败都抛出StackOverflowError
  • 多线程更容易引起内存溢出(多个栈容量叠加), 通过减少线程或更换64位虚拟机解决, 若无法解决则只能减少最大堆和减少栈容量从而获取更多线程

方法区溢出(Method Area Overflow)

  String.intern是一个native方法, 方法逻辑: 返回一个字符串常量池中与此String对象相等的字符串对象引用, 若常量池没有相等的则把次String对象加入常量池中并返回其对象引用。
  常量池分配在永久代中, 可通过-XX:PermSize-XX:MaxPermSize设置方法区大小, 可间接影响到常量池的容量。

  • JDK1.6intern中把首次遇到的字符串复制到永久代并返回永久代中实例的引用, StringBuilder则放在Java堆中
  • JDK1.7intern中不再复制实例, 只在常量池中记录首次出现的实例引用, 即internStringBuilder创建返回的是同一个字符串实例引用

垃圾收集(Garbage Collecting)


  线程隔离的三个区域(程序计数器, 虚拟机栈和本地方法栈)与线程共生共灭, 当方法或线程结束时其内存自然回收, 所以无需过多考虑回收问题。

存活(Dead or Live)

  主要有引用计数器法, 跟踪式分析法(或可达性分析法)。

引用计数(Reference Counting)

  每引用同一对象一次其计数器加一, 引用失效减一, 计数器为0则对象可回收。一般用哈希表管理计数, 以被管理的对象地址为key, 计数为value.可手动控制(MRC), 也可以自动管理(ARC)。
  缺点是需要频繁更新引用计数, 引用形成环路造成循环引用时难以处理(一般通过介入弱引用解决)。
  引用计数不仅可以用来管理内容, 操作系统的文件资源也可以适应(如文件描述符fd)。

跟踪式(Trace)

  把所有被管理对象的引用关系组织为一张有向图, 从必定无需回收的节点作为根节点触发跟踪其引用关系, 从而遍历所有可达节点, 可达节点就无需回收, 不可达就可以回收。跟踪式为自动管理。
  JVM中通过多个GC Roots的对象起始向下搜索所经过的路径为引用链, 当一个对象到任何GC Roots没有任何引用链时, 则此对象可回收。

  • 可达性判断: 单条引用链最弱的一条引用决定对象的可达性; 多条引用链最强的一条引用决定对象的可达性
  • GC Roots: 包括虚拟机栈栈帧的本地变量表中(即栈内存), 方法区中类静态属性和常量, 本地方法栈JNI这几处所引用的对象
GC Roots

引用(Reference)

  $强引用 \gt 软引用 \gt 弱引用 \gt 虚引用$。

  • 强引用(Strong Reference) 类似Object obj = new Object(); GC不回收存在强引用的对象(宁愿抛出OutOfMemoryError)
  • 软引用(Soft Reference)$
    \begin{cases}
    \text{描述一些有用但非必须的元素, 系统将要发生内存溢出前会回收软引用对象, 仍不足抛异常}\\
    \text{可用于对内存敏感的高速缓存}\\
    \text{与引用队列(ReferenceQueue)联合使用, 若引用对象被回收则JVM将其加入到关联引用队列中}
    \end{cases}
    $

  • 弱引用(Weak Reference)$
    \begin{cases}
    \text{描述非必须对象, 弱引用对象只存活到下一次GC前(无论内存是否足够)}\\
    \text{可以和引用队列联合使用, 若引用对象被回收, 则JVM将其加入到关联引用队列中}
    \end{cases}
    $

  • 虚引用(Phantom Reference)$
    \begin{cases}
    \text{虚引用是否存在不影响对象生命周期, 也无法通过虚引用获取对象实例}\\
    \text{仅用于GC回收虚引用对象时收到系统通知, 跟踪对象被GC回收的活动}\\
    \text{虚引用必须和引用队列联合使用}\\
    \text{可通过判断引用队列中是否加入虚引用来得知引用对象是否将被回收, 即可在其被回收前做操作}
    \end{cases}
    $
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference(object, queue);

软引用(Soft Reference)

  SoftReference保存对一个java对象的软引用, 对象被回收前get方法返回强引用, 对象被回收后get方法返回null.

/* 在触发OutOfMemoryError前, JVM尽可能优先回收长时间闲置不用的软引用对象, 刚创建的新软引用对象尽可能保留 */
MyObject aRef = new MyObject();
SoftReference aSoftRef = new SoftReference(aRef);
// 此时(MyObject)aSoftRef.get()返回强引用
aRef = null;
// 此时(MyObject)aSoftRef.get()可能返回null

ReferenceQueue queue = new ReferenceQueue();
SoftReference ref = new SoftReference(aRef, queue);

// 当软引用对象aRef被回收时, ref强引用的SoftReference对象被列入ReferenceQueue中
// poll方法: 队列为空返回null; 非空返回前一个SoftReference对象

// 常用以下方式清理SoftReference对象
SoftReference ref = null;
while ((ref = (SoftReference) q.poll()) != null) {
   // 清除ref
}

  通过软引用构造Java对象的高速缓存, 避免重复构建同一个对象(比如查询同一条数据库数据)。

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Hashtable;

public class EmployeeCache {
    /** 一个Cache实例 */
    static private EmployeeCache cache;

    /** 用于Chche内容的存储 */
    private Hashtable<String, EmployeeRef> employeeRefs;

    /** 垃圾Reference的队列 */
    private ReferenceQueue<Employee> q;

    // 构建一个缓存器实例
    private EmployeeCache() {
        employeeRefs = new Hashtable<String,EmployeeRef>();
        q = new ReferenceQueue<Employee>();
    }

    // 取得缓存器实例
    public static EmployeeCache getInstance() {
        if (cache == null) {
            cache = new EmployeeCache();
        }
        return cache;
    }

    /** 依据所指定的ID号,重新获取相应Employee对象的实例 */
    public Employee getEmployee(String ID) {
        Employee em = null;

        // 缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。
        if (employeeRefs.containsKey(ID)) {
            EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
            em = (Employee) ref.get();
        }

        // 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,
        // 并保存对这个新建实例的软引用
        if (em == null) {
            em = new Employee(ID);
            System.out.println("Retrieve From EmployeeInfoCenter. ID = " + ID);
            this.cacheEmployee(em);
        }

        return em;
    }

    /** 清除Cache内的全部内容 */
    public void clearCache() {
        cleanCache();
        //employeeRefs.clear(); 上一步选择性清除了一遍这里又整体清除一遍?
        System.gc();
        System.runFinalization();
    }

    /** 以软引用的方式对一个Employee对象的实例进行引用并保存该引用 */
    private void cacheEmployee(Employee em) {
        cleanCache();// 清除垃圾引用
        EmployeeRef ref = new EmployeeRef(em, q);
        employeeRefs.put(em.getID(), ref);
    }

    private void cleanCache() {
        EmployeeRef ref = null;

        // ReferenceQueue起到监听器的作用, 可监听到对象存活
        while ((ref = (EmployeeRef) q.poll()) != null) {
           employeeRefs.remove(ref._key);
        }
    }

    /** 继承SoftReference,使得每一个实例都具有可识别的标识 */
    private class EmployeeRef extends SoftReference<Employee> {
        private String _key = "";

        public EmployeeRef(Employee em, ReferenceQueue<Employee> q) {
            super(em, q);
            _key = em.getID();
        }
    }
}

弱引用(Weak Reference)

  弱引用较之软引用有更短暂的生命周期。GC线程一旦在其管辖内存区域扫描到弱引用对象, 不管当前内存空间是否足够都会将其内存回收。不过GC线程优先级很低, 所以不一定能很快遍历到弱引用对象。

  • 通常用于Debug, 内存监视工具等程序中
  • Proxy类中会把动态生成的Class实例暂存与一个Map<WeakReference>中作为Cache
  • 可用于规范化映射WeakHashMap, WeakHashMap使用WeakReference作为value, 当key的引用被置为null时, map内容会被很快GC.
  • 当想引用一个对象但又不想介入其生命周期时可使用弱引用, 以避免对其GC过程判断中造成干扰

Finalize

  FinalizeObjectprotected方法(没有任何操作), 可被子类覆盖, GC回收前会调用且只调用一次。建议用于清理本地对象(JNI创建的对象)和显示调用其他非内存资源(如Socket和文件)的释放方法。

问题(Problem)

  • Finalize方法不能保证执行(即使调用System.runFinalizersOnExit()Runtime.runFinalizersOnExit(true)(二者已被JDK弃用), 且可能导致finalizer方法被活对象调用时, 其他线程正在对该对象并行操作致使不正确行为或死锁, 即本质上不安全), 调用System.gc()System.runFinalization()只增加其执行机会
  • 超类的Finalize需要显示调用
  • Finalize中的异常会被GC忽略且不会向上传递, 也没有日志记录
  • Finalize给性能增加负担
  • Finalize中可将待回收对象赋值到GC Roots可达的对象引用, 导致该对象再生

过程(Process)

$$
\text{对象GC Roots不可达} \to \text{GC判断} \begin{cases}
\text{未写finalize方法 } \to \text{对象被回收}\\
\text{复写finalize方法} \to \text{进入F-Queue队列} \to \text{低优先级线程执行队列中的finalize方法} \to \text{GC判断} \begin{cases}
\text{GC Roots不可达} \to \text{回收对象}\\
\text{GC Roots可达}\;\;\,\, \to \text{对象再生}
\end{cases}
\end{cases}
$$

  对象有终结状态空间$F = {unfinalized(未准备), finalizable(准备), finalized(已执行)}$和可达状态空间$R = {reachable(可达), finalizer\text{-}reachable(由finalizable可达), unreachable(不可达)}$.

finalize lifycycle
  • 新建对象由路径A持有状态[reachable, unfinalized]
  • 引用关系变迁 → 路径BCD变为finalizer-reachable或路径EF变为unreachable
  • 当unfinalized对象的可达状态变为finalizer-reachable或unreachable时, 由路径GH: unfinalized → finalizable
  • JVM取出某finalizable对象标记为finalized并执行其finalize方法(由活线程调用, 则再次reachable), 该对象经由KJ变化状态为[reachable, finalized], 同时影响其他对象经由LMN可达状态由finalizer-reachable → reachable
  • 没有[unreachable, finalizable]的状态, 不然也无法调用这个对象的finalize方法了
  • 显示调用finalize方法不会影响上述状态变迁, 但JVM最多调用一次该方法(即使对象再生)
  • 处于[unreachable, finalized]的对象其内存将会被回收, JVM会优化未复写该方法的对象的GC, 直接经由路径O回收对象
  • System.runFinalizersOnExit等方法可以使对象即使处于reachable状态, JVM仍对其执行finalize方法

模板(Template)

  复写Finalize请遵循一下模板:

@Override
protected void finalize() throws Throwable {
    try {
        // release resources here
    } catch (Throwable t) {
        throw t;
    } finally {
        super.finalize();
    }
}

方法区回收(Method Area Collector)

  永久代的GC主要回收废弃常量和无用的类。

  • 废弃常量: 当常量池中某个常量没有任何对象引用, 也没有任何地方引用该字面量时, 该常量会被GC清除
  • 无用的类$
    \begin{cases}
    \text{堆中不存在类实例}\\
    \text{加载该类的类加载器(ClassLoader)被回收}\\
    \text{所用的java.lang.Class对象未被任何地方引用, 即无法通过反射访问该类方法}
    \end{cases}, \text{需要同时满足}
    $

  HotSpot提供-Xnoclassgc参数表示不对方法区进行GC. -verbose:class -XX:+TraceClassLoading XX:+TraceClassUnLoading可查看类的加载卸载信息。
  在大量使用反射, 动态代理以及CGLib等bytecode框架场景, 动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景下需要虚拟机具备类卸载功能以保证永久代不会溢出。

收集算法(Collect Algorithem)

  GC会监控每个对象的运行状况(申请, 引用被引用, 赋值等), 通过有向图实时监测对象是否可达来管理内存。
  Java语言中判断一个内存空间是否符合GC标准:

  • obj = null; // 以下再没有调用
  • obj = new Object(); // 重新分配内存空间

  • 标记-清除算法$
    \begin{cases}
    \text{先标记待回收对象, 标记完后统一回收所有对象}\\
    \text{标记清除效率不高}\\
    \text{标记清除产生大量不连续内存碎片, 可能导致分配较大对象无足够连续内存触发另一次GC}
    \end{cases}
    $
mark-sweep
  • 标记整理算法 $
    \begin{cases}
    \text{也叫标记压缩(mark-compact), mark过程和mark-sweep一样}\\
    \text{不直接清理可回收对象, 让所有活对象向一端移动并清理端边界外的内存}\\
    \text{针对老年代的特点和复制算法在100%存活的极端情况}
    \end{cases}
    $
mark-compact
  • 复制算法
    $$
    \text{原理}
    \begin{cases}
    \text{内存平分两块, 每次使用一块, 一块不足时复制到另一块并一次性清理已使用过的内存}\\
    \text{不用考虑内存碎片等复杂情况, 但内存缩小为原来的一半}\\
    \text{持续复制长生存期对象效率低}
    \end{cases}
    $$
    $$
    \text{实现}
    \begin{cases}
    \text{内存分一较大Eden空间和两较小Survivor空间, 每次使用Eden和其中一块Survivor}\\
    \text{不足存放到Survivor的新生代的活对象直接通过分配担保机制进入老年代}
    \end{cases}
    $$
copying
  • 增量算法 $
    \begin{cases}
    \text{把内存分为多个空间, 使用多线程每次只回收部分空间, 下次回收在原有基础上继续}\\
    \text{使得跟踪式变为并发式而非独占式}\\
    \text{由于切换线程等开销, 会加大跟踪式的整体开销}
    \end{cases}
    $

  • 分代收集算法$
    \begin{cases}
    \text{堆分为新生代和老年代(根据对象存活周期划分, 不一定只有两代)}\\
    \begin{cases}
    \text{新生代存活率低, 选用复制算法}\\
    \text{老年代存活率高, 选用标记清理或标记整理算法}
    \end{cases}
    \end{cases}
    $

复制算法(Copying Algorithm)

  声明: 本人并不清楚这里young GC和minor GC有什么区别
  一般针对新生代采用复制算法。上述复制算法的实现中提到复制算法并不是简单地把空间分为两个区域, 而是分为eden和survivor空间, survivor又分为s0和s1两个子空间。

eden survivor

  新分配对象加入eden(若eden放不下, eden复制存活对象到from后, 再存放新对象) → eden要溢出时触发young GC → eden和from存活对象copy进to中(to中也放不下, 全部进入老年代)

  • 一个对象多次触发young GC依然存活进入老年代(触发一次还活着, "续一秒"直到阈值)
  • 新生代触发的GC叫minor GC
  • 除了新生代的eden和survivor以外, 还有老年代的Tenured和永久代的Permanent区域
  • 老年代也会进行young GC, 在Tenured要溢出时触发Full GC, Tenured一般采用CMS策略
  • 永久代包含类和对象的元数据, 也会被回收, 触发的GC叫major GC
  • major GC至少伴随一次young GC, 一般比young GC慢十倍以上
  • Eden:from:to = 8:1:1

  企业开发-Xmx = -Xms, 通常设置-Xmx为2048m, Resin服务器中配置如下:

<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
  • 年轻代:年老代为1:3
  • -XX:MaxTenuringThreshold=15: 默认, 年轻代对象经过15次的复制后进入到年老代
  • -XX:PretenureSizeThreshold: 指定大对象以直接进入老年代

(Card)(Table)

  为了支持新生代的高频回收, 通过使用卡表数据结构加快新生代回收速度。

  • 比特位集合, 每个比特位表示老年代某区域是否持有新生代引用
  • 标记位1则持有引用, 0为一定不含有新生代引用
  • 新生代GC时只需扫描卡表位为1的老年代区域即可

垃圾收集器(Garbage Collector)


  • 新生代: Serial, ParNew, Parallel Scavenge, G1
  • 老年代: CMS(Concurrent Mark Sweep), Serial Old(MSC), Parallel Old, G1
  • 连线的两个收集器可配合工作

Stop The World

  编译代码时在每个方法循环结束或执行结束的点注入safepoint, 需要等待所有用户线程进入safepoint后暂停所有线程, 之后进行GC.

Serial Collector

  Serial是一个单线程收集器(串行收集器), 即只用一个CPU只用一个收集线程去完成GC.
  再者, 它进行GC时必须暂停其他所有工作线程, 直到它GC工作完成。

  • 最稳定, 效率高
  • 可能产生较长停顿
  • 复制算法
  • -XX:+UseSerialGC: 使用Serial GC

ParNew Collector

  Serial多线程版本, 其余特性与Serial一致。

  • 并行: 多条GC线程并行工作, 但用户线程仍被暂停等待
  • 复制算法

  参数控制:

  • -XX:+UseParNewGC: 使用ParNew GC
  • -XX:ParallelGCThreads: 限制线程数量

Parallel Scavenge

  Parallel Scavenge是一个新生代收集器, 使用多线程复制算法。CMS等收集器目标是尽可能缩短GC时用户线程停顿时间, 而Parallel Scavenge目标是达到一个可控的吞吐量(Throughout), 常称"吞吐量优先"收集器, 同时与ParNew重要区别是具备自适应调节策略

  • 吞吐量: 用户线程耗时与CPU总耗时比值。$
    \text{吞吐量} = {\text{运行用户代码时间} \over \text{运行用户代码时间} + \text{GC时间}}
    $
  • 响应时间: 短时间停顿适合用户交互场景, 提升用户体验
  • 高吞吐量: 高效利用CPU, 适合后台运算无过多交互场景

  参数控制:

  • -XX:+UseParallelGC: 使用Parallel Scavenge GC + Serial Old GC
  • -XX:MaxGCPauseMillis: 最大GC停顿时间(毫秒 ms), 牺牲吞吐量
  • -XX:GCTimeRatio: 吞吐量大小(0 ~ 100 整数), GC时间与总时间比率, 如设置19则${\text{GC时间} \over \text{总时间}} = {1 \over {1+19}} = 5\% $
  • -XX:+UseAdaptiveSizePolicy: 开启后不用手动设置新生代大小-Xmn, Eden与Survivor比例-XX:SurvivorRatio, 晋升老年带对象年龄-XX:PretenureSizeThreshold等细节参数, 自适应动态调整策略
  • -XX:GCTimeRatio-XX:MaxGCPauseMillis只设置其中一个就好

Serial Old Collector

  Serial老年代版本, 使用单线程标记整理算法
  主要用途:

  • Client: 虚机使用
  • Server: JDK1.5及其旧版本中与Parallel Scavenge搭配使用; CMS后备预案, 并发收集触发Concurrent Mode Failure时使用

Parallel Old Collector

  Parallel Scavenge老年代版本, 使用多线程标记整理算法。源于JDK1.5及其以前Parallel Scavenge只能和Serial Old搭配在多CPU下的糟糕表现, JDK1.6提供Parallel Old以获得"吞吐量优先"组合。

  • -XX:+UseParallelOldGC: 使用Parallel Scavenge GC + Parallel Old GC并行

  resin服务器配置如下:

<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-Xss1m</jvm-arg>
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>

<jvm-arg>-XX:+UseParallelOldGC</jvm-arg>
<jvm-arg>-XX:GCTimeRatio=19</jvm-arg>

<jvm-arg>-XX:+PrintGCDetails</jvm-arg>
<jvm-arg>-XX:+PrintGCTimeStamps</jvm-arg>

CMS Collector

  CMS(Concurrent Mark Sweep)收集器以最短回收停顿时间为目标, 与ParNew新生代收集器搭配, 适应互联网站或B/S系统的服务端, 强调服务响应速度。对待老年代的回收尽力做到能多concurrent就多concurrent.
  步骤: 初始标记(initial mark)并发标记(concurrent mark)重新标记(remark)并发清除(concurrent sweep)

  • 初始标记和重新标记仍需要噢~在这儿停顿!(STW, stop-the-world)
  • 并发清除和并发标记耗时最长, 期间用户线程并发执行
  • 初始标记: 只标记GC Roots直接关联的对象
  • 并发标记: GC Roots Tracing
  • 重新标记: 修正并发标记时用户线程继续运作使得一部分对象标记产生变动的标记记录
  • 消耗时间: $\text{初始标记} \lt \text{重新标记} \lt \text{并发标记}$

  • 优点: 并发收集低停顿
  • 缺点: $
    \begin{cases}
    \text{若老年代对象太多, STW会过于耗时}\\
    \text{CPU资源敏感, 默认回收线程} = {\text{CPU数量} + 3 \over 4}\\
    \text{无法处理浮动垃圾, Concurrent Mode Failure后可能导致一次Full GC}\\
    \text{产生大量内存空间碎片, 并发阶段会降低吞吐量}
    \end{cases}
    $

  参数控制:

  • -XX:+UseConcMarkSweepGC: 使用CMS GC
  • -XX:+UseCMSCompactAtFullCollection: 默认开启, Full GC后进行一次碎片整理, 独占线程变长停顿时间
  • -XX:+CMSFullGCsBeforeCompaction: 设置每多少次Full GC后进行一次碎片整理
  • -XX:+ParallelCMSThreads: 设置CMS线程数, 一般约等于可用CPU数, 默认$\text{CPU数量} + 3 \over 4$
  • -XX:CMSInitiatingOccupancyFraction: 指定当年老代空间满了多少后进行垃圾回收

G1 Collector

G1

  G1(Garbage-First)收集器面向服务端应用, 使用增量算法, 目的是替换JDK1.5的CMS收集器, 于JDK6u14有Experimental试用, JDK7u4移除实验性标记(又一文说要求7u14以上)

  与CMS相比有以下特点:

  • 标记整理算法: 不会产生空闲碎片。
  • 可预测停顿: 建立可预测的停顿时间模型, 在指定时间内回收部分价值最大的空间
  • 独立区域(Region): 堆分为多个大小相等区域, 新生代老年代不再物理隔离而是一部分Region集合
  • 筛选回收: 只回收部分region, 可控STW, 无需与用户线程抢CPU, CMS并发清理需要抢导致降低吞吐

  步骤: 初始标记 → 并发标记 → 最终标记 → 筛选回收

  • 并发标记: 此期间发生引用关系变化的对象会被记录在remember set logs中
  • 筛选回收: 通过跟踪各region垃圾堆积价值(回收量和回收时间), 在后台维护优先列表, 每次根据允许的收集时间优先回收价值最大的region, 即此阶段根据用户期望回收时间回收价值较大的对象

  适用:

  • 追求高响应: ParNew/CMS或G1
  • 追求高吞吐: Parallel Scavenge/Parallel Old, G1吞吐量无优势

Remember Set

  当引用对象被赋值, JVM发出write barrier暂时中断写操作, 检查是否老年代引用了新生代对象, 是则记录remember set, GC扫描时会从根集合 + remeber set向下扫描, 以避免老年代引用新生代时, GC需要扫描整个老年代来确认可达路径。
  G1中对每个region分配一个remember set, 引用对象被赋值时检查是否跨region引用, 跨则记录到当前region的remember set中用于之后扫描。
  并发标记期间发生引用变化的对象, 将被记录进remember set logs.在重新标记或最终标记阶段把该logs加入remember set中再trace节点。

Full GC

  2016年的文章认为ParNew/CMS和Parallel Scavenge/Parallel Old的组合用得最多。
  触发Full GC及其解决办法:

  • 老年代空间不足$
    \begin{cases}
    \text{少创建大对象大数组}\\
    \text{让对象在新生代中多苟一会儿, 尽量使其在minor GC中被回收}
    \end{cases}
    $
  • 方法区空间不足$
    \begin{cases}
    \text{增大方法区空间}\\
    \text{对方法区使用CMS}
    \end{cases}
    $
  • CMS GC时promotion failed(minor GC时新生代和老年代空间都不足)或concurrent mode failure$
    \begin{cases}
    \text{增大survivor空间和老年代空间}\\
    \text{调低-XX:CMSInitiatingOccupancyFraction}\\
    \text{-XX:CMSMaxAbortablePrecleanTime}=5(ms) \text{, 避免重新标记很久后才执行并发清理}
    \end{cases}
    $
  • 空间分配担保机制

空间分配担保机制

  Minor GC时, 若老年代最大可用连续空间大于新生代所有对象总空间, 则GC安全; 若不大于, 查看HandlePromotionFailure是否设置允许担保失败。若允许, 检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小, 若大于则尝试Minor GC; 若小于或不允许担保失败, 则需要进行Full GC.
  jdk6u24后, HandlePromotionFailure不再影响分配担保策略, 只要老年代连续空间大于新生代总空间或历次晋升平均大小则进行Minor GC, 否则Full GC.

JVM优化

  主要调节各代大小和选择collector.
  调节各代大小:

  • -Xmx: 一般-Xmx = -Xms防止堆内存频繁调整
  • -Xms
  • -Xmn: -Xmn = -Xmx/4-Xmx/3, 过小minor GC频繁, 过大老年代变小, 都会导致提前Full GC. 按照调大-Xmx → 调大-Xmn → 调大-XX:SurvivorRatio策略调节
  • -XX:SurvivorRatio: 默认8, 过大Eden变大Survivor变小, minor GC减少, minor GC存活可能直接进入老年代; 过小Eden变小Survivor变大, minor GC增多, 进入老年代对象可能减少
  • -XX:MaxTenuringThreshold: 默认15
  • -XX:PermSize: 一般-XX:PermSize = -XX:MaxPermSize, 实际开发中尽量不适用jsp而使用velocity等模版引擎技术, 不要引入无关jar(讲道理maven也可以裁剪)

  垃圾收集器:

  • Parallel Scavenge/Parallel Old: 关注吞吐, 用于密集CPU低交互场景, 也可用于JVM自动调优场景(-XX:+UseParallelOldGC-XX:GCTimeRatio-Xmx-XX:+UseAdaptiveSizePolicy). 使用-XX:+UseParallelOldGC指定
  • ParNew/CMS: 关注低STW, 用于高交互场景, -XX:+UseConcMarkSweepGC指定, -XX:CMSInitiatingOccupancyFraction调节老年代满多少百分比后GC
  • 企业级CPU下G1不能用时, ParNew/CMS是首选, Parallel用于JVM自动管理内存

参考(Reference)


  [01]  深入理解 Java 虚拟机 精华总结(面试), 刘金辉, 2016-05-21 17:58
  [02]  java finalize 方法总结、GC 执行 finalize 的过程, Smina 俊, 2017-07-16 01:29
  [03]  Why Not to Use finalize() Method in Java, Lokesh Gupta, 2012-10-31 11:32
  [04]  CMS 垃圾收集器介绍, Mark__Zeng, 2015-09-26 13:20
  [05]  GC 之一 --GC 的算法分析、垃圾收集器、内存分配策略介绍, duanxz, 2016-03-01 11:16
  [06]  对象的强、软、弱和虚引用, duanxz, 2015-04-18 18:38
  [07]  JVM 中 G1 垃圾回收器详细解析, stackvoid, 2015-03-16
  [08]  计算机体系 – 垃圾收集器, ImportNew, 2018-02-23
  [09]  第五章 JVM 垃圾收集器(1), 赵计刚, 2016-02-05 22:15

To be Continued...

Last modification:June 22nd, 2019 at 03:36 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment

captcha