無限風域

Java Virtual Machine(JVM)

[RUBYTOC]

<center></center>

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



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

[程序计数器]^(Program Counter Register)

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

[虚拟机栈]^(Stack)

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

[堆]^(Heap)

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

[方法区]^(Method Area)

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

Hotspot


[对象创建]^(Object Creation)

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

[对象内存]^(Object Memory)

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

基本数据类型占用空间(bytes)
boolean1
byte1
short2
char2
int4
float4
long8
double8
reference4(32位)/8(64位)

[对象访问]^(Object Access)

[内存溢出]^(Out Of Memory)


[堆溢出]^(Heap Overflow)

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

[栈溢出]^(Stack Overflow)

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

[方法区溢出]^(Method Area Overflow)

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

[垃圾收集]^(Garbage Collecting)



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

[存活]^(Dead or Live)

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

[引用计数]^(Reference Counting)

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

[跟踪式]^(Trace)

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

<center></center>

[引用]^(Reference)

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

\text{描述一些有用但非必须的元素, 系统将要发生内存溢出前会回收软引用对象, 仍不足抛异常}\\
\text{可用于对内存敏感的高速缓存}\\
\text{与引用队列(ReferenceQueue)联合使用, 若引用对象被回收则JVM将其加入到关联引用队列中}
\end{cases}
$

\text{描述非必须对象, 弱引用对象只存活到下一次GC前(无论内存是否足够)}\\
\text{可以和引用队列联合使用, 若引用对象被回收, 则JVM将其加入到关联引用队列中}
\end{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线程优先级很低, 所以不一定能很快遍历到弱引用对象。

Finalize

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

[问题]^(Problem)

[过程]^(Process)

$$ \\text{对象GC Roots不可达} \\to \\text{GC判断} \\begin{cases} \\text{未写finalize方法&ensp;} \\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(不可达)}$.

<center></center>

[模板]^(Template)

  复写Finalize请遵循一下模板:

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

[方法区回收]^(Method Area Collector)

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

\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标准:

\text{先标记待回收对象, 标记完后统一回收所有对象}\\
\text{标记清除效率不高}\\
\text{标记清除产生大量不连续内存碎片, 可能导致分配较大对象无足够连续内存触发另一次GC}
\end{cases}
$

<center></center>

\text{也叫标记压缩(mark-compact), mark过程和mark-sweep一样}\\
\text{不直接清理可回收对象, 让所有活对象向一端移动并清理端边界外的内存}\\
\text{针对老年代的特点和复制算法在100%存活的极端情况}
\end{cases}
$

<center></center>

$$ \\text{原理} \\begin{cases} \\text{内存平分两块, 每次使用一块, 一块不足时复制到另一块并一次性清理已使用过的内存}\\\\ \\text{不用考虑内存碎片等复杂情况, 但内存缩小为原来的一半}\\\\ \\text{持续复制长生存期对象效率低} \\end{cases} $$

$$ \\text{实现} \\begin{cases} \\text{内存分一较大Eden空间和两较小Survivor空间, 每次使用Eden和其中一块Survivor}\\\\ \\text{不足存放到Survivor的新生代的活对象直接通过分配担保机制进入老年代} \\end{cases} $$

<center></center>

\text{把内存分为多个空间, 使用多线程每次只回收部分空间, 下次回收在原有基础上继续}\\
\text{使得跟踪式变为并发式而非独占式}\\
\text{由于切换线程等开销, 会加大跟踪式的整体开销}
\end{cases}
$

\text{堆分为新生代和老年代(根据对象存活周期划分, 不一定只有两代)}\\

\\begin{cases}
\\text{新生代存活率低, 选用复制算法}\\\\
\\text{老年代存活率高, 选用标记清理或标记整理算法}
\\end{cases}

\end{cases}
$

[复制算法]^(Copying Algorithm)

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

<center></center>

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

  企业开发-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>

[卡表]^(Card Table)

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

[垃圾收集器]^(Garbage Collector)


<center></center>

Stop The World

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

Serial Collector

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

ParNew Collector

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

  参数控制:

Parallel Scavenge

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

$

  参数控制:

Serial Old Collector

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

Parallel Old Collector

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

  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)

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

  参数控制:

G1 Collector

<center></center>

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

  与CMS相比有以下特点:

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

  适用:

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及其解决办法:

\text{少创建大对象大数组}\\
\text{让对象在新生代中多苟一会儿, 尽量使其在minor GC中被回收}
\end{cases}
$

\text{增大方法区空间}\\
\text{对方法区使用CMS}
\end{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.
  调节各代大小:

  垃圾收集器:

[参考]^(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]  <a href="
https://www.cnblogs.com/duanxz/p/4437844.html">对象的强、软、弱和虚引用, duanxz, 2015-04-18 18:38
  [07]  <a href="
http://www.codeceo.com/jvm-g1-gc.html">JVM 中 G1 垃圾回收器详细解析, stackvoid, 2015-03-16
  [08]  <a href="
https://mp.weixin.qq.com/s?__biz=MjM5NzMyMjAwMA==&mid=2651480275&idx=2&sn=4ec885c5fb6cfd2a3fda64bc89ac07d1">计算机体系 – 垃圾收集器, ImportNew, 2018-02-23
  [09]  <a href="
http://www.cnblogs.com/java-zhao/p/5183670.html">第五章 JVM 垃圾收集器(1), 赵计刚, 2016-02-05 22:15

To be Continued...

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »