JVM必知必会

329 阅读44分钟

扫盲1:JDK、JRE、虚拟机关联

JDK=JAVA语言+JAVA API类库+JAVA虚拟机

JRE=部分JAVA API类库+JAVA虚拟机

扫盲2:Write Once, Run Anywhere

Java应用摆脱硬件平台的束缚,一次编写,到处运行的特性得益于虚拟机

此刻进入正题...

内存区域

虚拟机将内存区域划分为若干个不同的数据区。不同的区有各自的用途,各自的创建和销毁时间,有的区随着虚拟机进程的启动而存在,有的区随着用户线程的启动和结束而建立和销毁。JDK7虚拟机规范的运行时数据区域如下

程序计数器

私有:线程私有区域,生命周期与线程一致

作用:可以当做是当前线程所执行的字节码指示器。如果是java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;Native方法,则计数器值为空

虚拟机栈

私有:线程私有区域,生命周期与线程一致

作用:虚拟机栈为虚拟机执行java方法服务。每个方法在执行的时候都会创建一个栈幀用于创建局部变量、操作数栈、动态链接、方法说出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈幀在虚拟机栈的入栈和出栈过程。

异常:如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError。如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够的内存,会抛出OutOfMemoryError

本地方法栈

本地方法栈为虚拟机执行native方法服务。其它与虚拟机栈相同,包括异常。

有的虚拟机直接把本地方法栈和虚拟机栈合并,比如SunHotSpot虚拟机。

共享:线程共享区域,生命周期与虚拟机进程一致

作用:此区域唯一目的是存放对象实例,包括数组

异常:如果在堆中没有内存完成实例分配,并且堆也无法扩展,会抛出OutOfMemoryError

方法区

共享:线程共享区域,生命周期与虚拟机进程一致

作用:存储被虚拟机加载的类信息、常量、静态变量等数据。该区域有一个运行常量池,用于存储编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池存放

异常:如果当方法区无法满足内存分配需求时,会抛出OutOfMemoryError

HotSpot

通过上述虚拟机规范的内存区域,已经熟悉了各个内存区域的特点(是否私有、作用、异常)除了这些,虚拟机是如何创建、布局、访问对象的呢?这些细节脱离具体虚拟机和内存区域是没有意义的,因此选择HotSpot虚拟机的堆来看下对象创建、布局和访问的过程。

对象创建

  • 当遇到一条new指令的时候,首先会去方法区检查这个类是否已经被加载。如果没有则执行类加载。
  • 类加载后,虚拟机为新生的对象分配内存。对象所需的内存大小在类加载完成后就确定了,为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来。如何划分呢?
  • 内存分配完后,虚拟机将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序访问这些字段的类型所对应的零值
  • 初始化零值后,虚拟机对对象进行必要的设置。设置对象头信息,比如对象是哪个类的实例、对象的哈希码、对象的GC分代年龄等
  • 执行程序的初始化方法,按照程序为字段赋值

对象布局

对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。

对象头:一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄,另一部分是类型指针,虚拟机通过这个指针来确定对象是哪个类的实例;

实例数据:略

对齐填充:略

对象访问

主流访问方式有使用句柄和直接指针两种,HotSpot使用直接指针的方式

使用句柄访问

最大的优势是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时已从对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference不需要修改

使用直接指针访问

最大优势是速度更快,因为节省了一次值指针定位的时间开销

异常

基于HotSpot虚拟机

堆溢出

堆常用虚拟机参数

-Xms设置堆最小值

-Xmx设置堆最大值

-XX:+HeapDumpOnOutOfMemoryError 开启自动dump内存溢出日志

测试代码

/**
* -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* -Xms与-Xmx指定一样大小防止堆自动扩展
*/
public class HeapOOM {

    static class OOMObject{

    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true){
            list.add(new OOMObject());
        }
    }
}

运行,控制台输出如下,同时自动生成堆快照文件,如java_pid8012.hprof,通过其它工具分析(重要,待)

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid8012.hprof ...
Heap dump file created [61914536 bytes in 0.134 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at com.cn.HeapOOM.main(HeapOOM.java:15)

栈溢出

HotSpot只有一个栈,不区分虚拟机栈和本地方法栈

栈常用虚拟机参数

-Xss设置栈大小,每个线程会分配的栈空间大小,线程调用方法则对应栈幀在该栈的入栈和出栈

测试代码

/**
* -Xss128k
* 不断的向同一个线程分配到的栈中入栈,最终线程栈无法满足栈幀需求,而出现栈溢出
*/
public class JVMStackSOF {

    private int stackLength = 1;

    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JVMStackSOF jvmStackSOF = new JVMStackSOF();
        try {
            jvmStackSOF.stackLeak();
        }catch (Throwable e){
            System.out.println("stack length: "+ jvmStackSOF.stackLength);
            e.printStackTrace();
        }
    }
}

运行,控制台输出如下

stack length: 986
java.lang.StackOverflowError
	at com.cn.JVMStackSOF.stackLeak(JVMStackSOF.java:8)
	at com.cn.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
	at com.cn.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
	at com.cn.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
    ...

这个概念很重要:操作系统分配给每个进程的内存是有限制的,比如32位的Windows限制是2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机本身消耗的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈瓜分了。

1.每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就容易把剩下的内存耗尽。

2.在不能减少线程数量的情况下,只能通过减少最大堆、方法区和减少栈容量来换取更多的线程。

测试代码

/**
 * -Xss2m
 * 不断创建线程去消耗内存,最终可以分配给栈的内存被消耗完无法再分配给新线程,而出现内存溢出
 */
public class JVMStackOOM {
    
    public void stackLeakByThread(){
       while (true){
           new Thread(new Runnable() {
               public void run() {
                   while (true){
                       
                   }
               }
           }).start();
       }
    }
    
    public static void main(String[] args) {
      JVMStackOOM jvmStackOOM = new JVMStackOOM();
      jvmStackOOM.stackLeakByThread();
    }
}

方法区和运行时常量池溢出

55

垃圾收集器

从上面的内存划分区域可以看到,程序计数器、虚拟机栈、本地方法栈都属于线程私有区域,随线程而生,随线程而灭,因此,区域存储的内容也就跟着回收了。但是堆和方法区是线程共享的区域,垃圾收集器关注这部分区域进行动态回收

对象存活判定算法

在垃圾收集器回收之前,第一件事情就是判断哪些对象还“存活”,哪些对象已经“死去”。判断方式有以下两种:

引用计数算法

基本思想:给对象添加引用计数器,每当有一个地方引用它的时候,计数器加1;当引用失效的时候,计数器减1;当引用计数器为0的时候表示对象不会再被使用。

该方法简单,但是会出现循环引用的问题。比如A.instance=B,B.instance.A 那么实例A和实例B为循环引用的关系,按照上述算法,计数器都不为0,都判定为对象存活而不会被回收。因为循环引用问题,主流的java虚拟机没有使用该算法来管理内存。

可达性分析算法

基本思想:从“GC Roots”的对象作为起始点,从这些点开始向下搜索,搜索所走过的路径成为“引用链”,当一个对象到“GC Roots”没有任何引用链相连时,则表示该对象是“死亡”的。如下所示:

GC Roots有哪些呢?

  • 栈中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

finalize()方法

1.首先该方法在Object类中声明

 protected void finalize() throws Throwable { }

2.finalize()方法最多只会被调用一次

3.真正宣告一个对象死亡,至少要经历两次标记过程:

  • 对象在可达性分析时候发现不存在引用链,则会进行第一次标记
  • 如果对象覆盖finalize()并且finalize()未被虚拟机调用过,则需要执行finalize()方法;否则不需要执行finalize()方法
  • 如果需要执行finalize()方法,对象会放置到F-Queue队列中
  • 虚拟机建立的Finalizer线程(是否为守护线程)会去触发对象的finalize()执行,但是不承诺等待它结束(这样设计是为了防止在某个对象中的finalize方法执行缓慢或者死循环,很可能会导致队列中的其它对象永久处于等待)
  • 之后将会对F-Queue队列中的对象进行第二次标记,如果对象在finalize()拯救了自己,则会被移出“即将回收”的集合;否则,对象基本上真的被回收了

4.测试代码

/**
 * 1.对象可以在被GC时候自我拯救
 * 2.自我拯救机会只有一次,因为一个对象的finalize方法最多只会被系统调用一次
 */
public class FinalizeEscapeGC {

    private static FinalizeEscapeGC finalizeEscapeGC=null;

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize method executed!");
        finalizeEscapeGC=this;//对象自救
    }

    public static void main(String[] args) throws Exception {
        //新建对象
        finalizeEscapeGC = new FinalizeEscapeGC();
        
        //删除引用链
        finalizeEscapeGC=null;
        //发起GC
        System.gc();
        //Finalize线程优先级低,暂停0.5s等待该线程执行(执行则为对象第一次自我拯救)
        Thread.sleep(500);
        if (finalizeEscapeGC == null){
            System.out.println("I am dead1");
        }else {
            System.out.println("I am alive1");
        }

        //删除引用链
        finalizeEscapeGC=null;
        //发起GC
        System.gc();
        //Finalize线程优先级低,暂停0.5s等待该线程执行(执行则为对象第二次自我拯救)
        Thread.sleep(500);
        if (finalizeEscapeGC == null){
            System.out.println("I am dead2");
        }else {
            System.out.println("I am alive2");
        }
    }
}

输出结果

finalize method executed!
I am alive1 //第一次自我拯救成功了
I am dead2 //第二次自我拯救失败了

垃圾收集算法

先奉上堆空间细分图

标记清除算法

先标记出所有需要回收的对象(如何标记见上一部分),在标记完成后统一回收所有被标记的对象

优点:不需要其它内存分配担保(见复制算法)

缺点:

  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
  • 效率问题,标记和清除两个过程的效率都不高

复制算法

将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。

优点:内存规整(内存分配不用考虑内存碎片的情况,只要移动堆顶指针,按顺序分配内存即可

缺点:

  • 内存虽小为了原来的一半
  • 对象存活率较高时候就要进行较多的复制操作,效率会变低

上述缺点很明显,每次可用的只有堆总内存的一半?但是这样是合理的嘛,内存是否被浪费了呢?

答案是内存被浪费了。IBM公司研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两小块较小的Survivor空间,每次使用Eden和其中一块Servivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚使用过的Suervivor空间。

商业虚拟机都采用这种收集算法来回收新生代。比如HotSpot虚拟机,默认Eden和Servivor的大小比例是8:1,也就是每次新生代中可用内存空间为真个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”(HotSpot的这种划分和IBM的研究没有关系,刚开始就是这种布局)

一般场景下98%的对象可回收,但是没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。(如果另一块Survivor空间没有足够空间存放上一次新生代收集之后存活对象时,这些对象将直接通过分配担保机制进入老年代)

标记-整理算法

先标记出所有需要回收的对象(如何标记见上一部分),然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

优点:

  • 内存规整
  • 不需要其它内存分配担保

分代收集算法

堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量对象存活,那就选用复制算法,只需付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。商业虚拟机的垃圾收集都采用“分代收集”算法。

垃圾收集器

垃圾收集器是垃圾回收算法的实践。下图为JDK1.7 Update14之后的HotSpot虚拟机的垃圾收集器,连线表示可以搭配使用,以及指明了不同垃圾收集器可使用的区域,实现的算法

三个重要的概念

GC停顿 Stop The World

在枚举根节点的时候,为了避免出现枚举的过程中对象的引用关系还在不断变化(如果不满足那么枚举的准确性无法得到保障),在垃圾回收的时候必须停顿所有java执行线程。

安全点 Safepoint

Q:怎样进入GC停顿

A:在遇到安全点

Q:如何让所有java执行线程都“跑到”最近的安全点上停顿下来的呢?

A:1.抢先式中断:在GC时候,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑到”安全几点上。 2.主动式中断:在GC时候,设置一个标志,各个线程执行时候主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的。HotSpot虚拟机使用该方式。

安全区域 Safe Region

Q: Safepoint机制(安全点主动式中断机制)保证了程序执行时候,在不太长的时间内就会遇到可进入GC的安全点。但是,线程没有分配到CPU时间,比如线程处于阻塞状态,这个时候线程无法响应JVM中断请求,“跑到”安全点去中断挂起。

A:安全区域可以解决这个问题。安全区域是指在一段代码片段中,引用关系不会发生变化。在这个地方的任意地方开始GC都是安全的。

Q:安全区域如何工作

A:当线程执行到Safe Region中的代码时,首先标志自己已经进入了Safe Region,当在这段时间里JVM发起GC时,就不用了管标识自己为Safe Region状态的线程了。在现诚邀离开Safe Region时,它要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Regin的信号为止

Serial 收集器

单线程采用复制算法的新生代垃圾收集器,并在垃圾收集的时候必须暂定其他所有的工作线程

常用控制参数:

-XX:SurvivorRatio 设置Eden与Survivor比例
-XX:PretenureSizeThreshold 设置直接进入老年代的对象最小大小

Serial Old 收集器

单线程采用标记-整理的老年代垃圾收集器,并在垃圾收集的时候必须暂定其他所有的工作线程

ParNew 收集器

多线程采用复制算法的新生代垃圾收集器,其它与Serial收集器一样

Parallel Scavenge 收集器

多线程采用复制算法的新生代垃圾收集器

该收集器关注吞吐量,吞吐量=CPU用于运行用户代码的时间/(CPU用于运行用户代码的时间+垃圾收集时间)。高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

常用控制参数:

-XX:MaxGCPauseMillis 设置垃圾收集停顿时间
-XX:GCTimeRatio 设置吞吐量大小

Parallel Old 收集器

多线程采用标记-整理算法的老年代垃圾收集器

CMS 收集器

采用标记-清理算法的老年代垃圾收集器

该收集器关注最短回收停顿时间,互联网站或者B/S系统尤其重视服务的响应速度、希望系统停顿时间最短,以给用户带来较好体验。因此特别合适使用CMS。

常用控制参数:

-XX:CMSInitiatingOccupancyFraction 设置Full GC的内存占比
-XX:+UseCMSCompactAtFullCollection 设置是否开启空间碎片整理
-XX:CMSFullGCsBeforeCompaction 设置两次空间碎片整理的间隔次数

缺点:

1.对CPU敏感:

在并发阶段,最燃不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总体吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4 ???

2.无法处理浮动垃圾:

在并发清理阶段用户线程还在运行,自然还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们。这部分垃圾成为“浮动垃圾”。浮动垃圾可能会造成增加Full GC次数和OOM风险,CMS收集器采用以下两种策略来解决:

  • 预留内存。CMS并不是等老年代满了才进行Full GC,而是在使用的内存达到一定百分比的时候就发触发Full GC。百分比默认是68%,当然如果应用中老年代增长不是太快,以通过指定-XX:CMSInitiatingOccupancyFraction调高百分比
  • 备用Serial Old垃圾收集器。当预留内存无法满足程序需要的时候,会出现“Concurrent Mode Failure”失败,启动Serial Old单线程进行老年代垃圾收集,这样停顿时间就变长了。所以-XX:CMSInitiatingOccupancyFraction设置太高很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低了。因此,不宜设置过大。

3.产生空间碎片:

CMS是基于标记-清除算法实现的,这就意味着会出现大量空间碎片。当给大对象分配空间的时候,因为找不到连续空间来分配而不得不触发一次Full GC。为了解决这个问题CMS采用以下策略:

  • 使用-XX:+UseCMSCompactAtFullCollection设置(默认开启)是否在CMS进行Full GC之后进行内存碎片整理,整理过程是单线程的,空间碎片问题解决了,但是停顿时间变长了。
  • 使用-XX:CMSFullGCsBeforeCompaction设置(默认为0)执行多少次不压缩Full GC后,来一次带压缩的Full GC。如果在-XX:+UseCMSCompactAtFullCollection开启的前提下,-XX:CMSFullGCsBeforeCompaction为默认值表示Full GC后每次都进行内存碎片整理;如果设置为n表示两次之间间隔n次Full GC才进行一次内存碎片整理

内存分配策略

以下为几条普遍的内存分配规则,并通过Serial/Serial Old(ParNew/Serial Old组合也一样)垃圾收集器组合来验证内存分配和回收策略

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,该GC期间发现Survivor无法容纳下存活的对象,则进行分配担保将存活对象转移到老年代,然后在Eden中分配内存给新对象;如果GC期间发现Survivor能容纳下存活的对象,则在Eden中分配内存给新对象。

测试代码

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=8 新生代中Eden与其中一块Survivor空间大小比值
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[2*_1M];
        byte[] a2 = new byte[2*_1M];
        byte[] a3 = new byte[2*_1M];
        byte[] a4 = new byte[4*_1M];
    }
}

输出

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew: 7639K->520K(9216K), 0.0041433 secs] 7639K->6664K(19456K), 0.0041816 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4866K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  53% used [0x00000000f9a00000, 0x00000000f9e3e600, 0x00000000fa200000)
  from space 1024K,  50% used [0x00000000fa300000, 0x00000000fa382380, 0x00000000fa400000)
  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2936K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0de230, 0x00000000fb0de400, 0x00000000fc2c0000)
No shared spaces configured.

大对象直接进入老年代

大对象需要连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

Q:按照“对象优先在Eden分配”的规则,当Eden无足够存空间安置大对象的时候,会触发Minor GC。但是对象通常是朝生夕灭,这对虚拟机来说不是好消息。

A:虚拟机提供了-XX:PretenureSizeThreshold参数,让大于这个设置值得对象直接在老年代分配。避免在Eden和Survivor之间大量的内存复制(内存复制的缺点)。该对象对Parallel Scavenge无效。

测试代码(与前一个例子对比下)

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=8 Eden与Survivor比例
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 * -XX:PretenureSizeThreshold=3m 直接晋升老年代的对象最小值
 * -XX:+PrintTenuringDistribution Survivor区中的对象的年龄分布
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[2*_1M];
        byte[] a2 = new byte[2*_1M];
        byte[] a3 = new byte[2*_1M];
        byte[] a4 = new byte[4*_1M];
    }
}

输出

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:PretenureSizeThreshold=3145728 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
Heap
 def new generation   total 9216K, used 7803K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  95% used [0x00000000f9a00000, 0x00000000fa19efb0, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2936K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0de230, 0x00000000fb0de400, 0x00000000fc2c0000)
No shared spaces configured.

从输出可以看到,4M的字节数组直接分配到了老年代(tenured generation)中

长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1。对象在Survivor区每“熬过”一次Minor GC,年龄就增加1,当对象的年龄增加到一定程度(默认15),就将会被(在下一次触发Minor GC时候)晋升到老年代中。虚拟机提供了-XX:MaxTenuringThreshold参数来设置晋升到老年代的最小年龄。

测试代码:设置-XX:MaxTenuringThreshold=15

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=6 Eden与Survivor比例
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 * -XX:MaxTenuringThreshold=15 设置晋升到老年代的最小年龄
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[_1M/15];//为了不让surivivor被使用超过50%去模拟当达到年龄阈值的晋升老年代,设置为_1M/15,原因见“动态对象年龄判断”。
        byte[] a2 = new byte[4*_1M];
        byte[] a3 = new byte[4*_1M];//触发一次GC
        a3=null;
        a3 = new byte[4*_1M];//触发第二次GC
    }
}

输出为:

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew: 5847K->776K(9216K), 0.0037015 secs] 5847K->4872K(19456K), 0.0037337 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC[DefNew: 5041K->0K(9216K), 0.0013605 secs] 9137K->4868K(19456K), 0.0013790 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e227b0, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa2000e0, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4868K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  47% used [0x00000000fa400000, 0x00000000fa8c1190, 0x00000000fa8c1200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2961K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0e46e8, 0x00000000fb0e4800, 0x00000000fc2c0000)
No shared spaces configured.

输出

[GC[DefNew
Desired survivor size 655360 bytes, new threshold 15 (max 15)  
- age   1:     603304 bytes,     603304 total
: 5654K->589K(8960K), 0.0035976 secs] 5654K->4685K(19200K), 0.0036317 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 15 (max 15)
- age   1:        224 bytes,        224 total
- age   2:     598680 bytes,     598904 total
: 4843K->584K(8960K), 0.0011677 secs] 8939K->4680K(19200K), 0.0011913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 8960K, used 4810K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 7680K,  55% used [0x00000000f9a00000, 0x00000000f9e204d0, 0x00000000fa180000)
  from space 1280K,  45% used [0x00000000fa180000, 0x00000000fa212378, 0x00000000fa2c0000)
  to   space 1280K,   0% used [0x00000000fa2c0000, 0x00000000fa2c0000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2947K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0e0cc0, 0x00000000fb0e0e00, 0x00000000fc2c0000)
No shared spaces configured.

从输出可以看到进行了两次GC(GC DefNew 没打印一个这个表示进行了一个GC),然后在进程退出的时候打印了内存的分布情况。一块survivor区被使用了45%。

Desired survivor size 655360 bytes--655360bytes为survivor的1/2大小值

new threshold 15 --晋升的最小年龄为15

设置-XX:MaxTenuringThreshold=1

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=6 Eden与Survivor比例
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 * -XX:MaxTenuringThreshold=15 设置晋升到老年代的最小年龄
 * -XX:+PrintTenuringDistribution Survivor区中的对象的年龄分布
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[_1M/15];
        byte[] a2 = new byte[4*_1M];
        byte[] a3 = new byte[4*_1M];//触发一次GC
        a3=null;
        a3 = new byte[4*_1M];//触发第二次GC
    }
}

输出

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=1 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=6 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 1 (max 1)
- age   1:     603304 bytes,     603304 total
: 5654K->589K(8960K), 0.0037347 secs] 5654K->4685K(19200K), 0.0037730 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 1 (max 1)
- age   1:        224 bytes,        224 total
: 4843K->0K(8960K), 0.0011726 secs] 8939K->4680K(19200K), 0.0011882 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 8960K, used 4225K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 7680K,  55% used [0x00000000f9a00000, 0x00000000f9e204d0, 0x00000000fa180000)
  from space 1280K,   0% used [0x00000000fa180000, 0x00000000fa1800e0, 0x00000000fa2c0000)
  to   space 1280K,   0% used [0x00000000fa2c0000, 0x00000000fa2c0000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4680K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  45% used [0x00000000fa400000, 0x00000000fa8922a8, 0x00000000fa892400, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2953K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0e2710, 0x00000000fb0e2800, 0x00000000fc2c0000)
No shared spaces configured.

从输出可以看到,survivor区未被使用,allocation1对象晋升到了老年代中。 请注意,如果不触发第二次GC,则不会晋升到老年代中。比如把“a3 = new byte[4*_1M];//触发第二次GC”删除,重新运行输出为:

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=1 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=6 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 1 (max 1)
- age   1:     603304 bytes,     603304 total
: 5654K->589K(8960K), 0.0036958 secs] 5654K->4685K(19200K), 0.0037328 secs] [Times: user=0.02 sys=0.03, real=0.01 secs] 
Heap
 def new generation   total 8960K, used 4919K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 7680K,  56% used [0x00000000f9a00000, 0x00000000f9e3ab38, 0x00000000fa180000)
  from space 1280K,  46% used [0x00000000fa2c0000, 0x00000000fa3534a8, 0x00000000fa400000)
  to   space 1280K,   0% used [0x00000000fa180000, 0x00000000fa180000, 0x00000000fa2c0000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2936K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0de218, 0x00000000fb0de400, 0x00000000fc2c0000)
No shared spaces configured.

从输出结果看到,虽然-XX:MaxTenuringThreshold=1,但是因为仅触发了一次GC,allocation1对象还在survivor区中,surivivor被使用了46%。

动态对象年龄判断

虚拟机并不是永远的要求独享的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半(虚拟机提供了-XX:TargetSurvivorRatio设置比例,默认为50%),年龄大于或等于该年龄的对象就可以直接晋升老年代,不需要等到MaxTenuringThreshold要求的年龄。

先运行代码,触发一次GC找到合适参数,使得survivor被使用大于Survivor空间的一半(增大第二次GC时候晋升老年代的概率,虽然相同年龄这个条件还未知是否满足)

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=6 Eden与Survivor比例
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 * -XX:MaxTenuringThreshold=15 设置晋升到老年代的最小年龄
 * -XX:+PrintTenuringDistribution Survivor区中的对象的年龄分布
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[_1M/6];
        byte[] a2 = new byte[4*_1M];
        byte[] a3 = new byte[4*_1M];//触发一次GC
    }
}

输出

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=6 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 1 (max 15)
- age   1:     708248 bytes,     708248 total
: 5824K->691K(8960K), 0.0037617 secs] 5824K->4787K(19200K), 0.0037960 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 8960K, used 5022K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 7680K,  56% used [0x00000000f9a00000, 0x00000000f9e3aab8, 0x00000000fa180000)
  from space 1280K,  54% used [0x00000000fa2c0000, 0x00000000fa36ce98, 0x00000000fa400000)
  to   space 1280K,   0% used [0x00000000fa180000, 0x00000000fa180000, 0x00000000fa2c0000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2961K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0e48e0, 0x00000000fb0e4800, 0x00000000fc2c0000)
No shared spaces configured.

Process finished with exit code 0

从输出可知,Survivor被使用54%,超出一半。

添加两行代码,并执行:

/**
 * -Xms20m  最小堆10m
 * -Xmx20m  最大堆10m
 * -Xmn10m  新生代10m
 * -XX:+UseSerialGC    使用Serial+Serial Old组合的垃圾收集器
 * -XX:SurvivorRatio=6 Eden与Survivor比例
 * -XX:+PrintGCDetails 发生垃圾收集行为打印内存回收日志,进程退出的时候打印当前的内存各区域分配情况
 * -XX:+PrintCommandLineFlags  查看被设置过的XX参数和值
 * -XX:MaxTenuringThreshold=15 设置晋升到老年代的最小年龄
 * -XX:+PrintTenuringDistribution Survivor区中的对象的年龄分布
 */
public class TestAllocation {

    private static final int _1M=1024*1024;

    public static void main(String[] args) {
        byte[] a1 = new byte[_1M/6];
        byte[] a2 = new byte[4*_1M];
        byte[] a3 = new byte[4*_1M];//触发一次GC
        a3=null;
        a3 = new byte[4*_1M];//触发第二次GC
    }
}

输出

-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:SurvivorRatio=6 -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 1 (max 15)
- age   1:     704928 bytes,     704928 total
: 5530K->688K(8960K), 0.0035402 secs] 5530K->4784K(19200K), 0.0097797 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC[DefNew
Desired survivor size 655360 bytes, new threshold 15 (max 15)
- age   1:        584 bytes,        584 total
: 5091K->0K(8960K), 0.0013044 secs] 9187K->4780K(19200K), 0.0013226 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 8960K, used 4329K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 7680K,  56% used [0x00000000f9a00000, 0x00000000f9e3a518, 0x00000000fa180000)
  from space 1280K,   0% used [0x00000000fa180000, 0x00000000fa180248, 0x00000000fa2c0000)
  to   space 1280K,   0% used [0x00000000fa2c0000, 0x00000000fa2c0000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4779K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  46% used [0x00000000fa400000, 0x00000000fa8aafa0, 0x00000000fa8ab000, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2899K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  13% used [0x00000000fae00000, 0x00000000fb0d51f8, 0x00000000fb0d5200, 0x00000000fc2c0000)
No shared spaces configured.

从输出可知,survivor空间未被使用,原先上一步骤看到的survivor内容由于相同年龄所有对象大小的总和大于Survivor空间的一半,所以直接晋升到了老年代。

空间分配担保

Minor GC: 收集新生代区域的垃圾。Eden无法分配足够内存给对象的时候触发
Full GC: 收集整个堆区域(新生代+老年代)的垃圾。什么时候触发呢?
	1.晋升到老年代的对象大于了老年代的剩余空间时;
	2.显示调用System.gc()方法时;
	3.下面这种情况触发

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立那么Minor GC是安全的。如果不成立,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代的平均大小,如果条件满足,则尝试进行一次Minor GC,这次Minior GC需要空间内存担保并且担保失败了,则会发起一次Full GC。

虚拟机性能监控和故障处理工具

通过上述的理论积累,下面开始实践

JDK自带的命令

jstat-虚拟机统计信息监视工具

jstat -gc vmid --监视堆状况,包含Survivor、Eden、老年代、永久代等的容量和已使用空间、GC次数和时间

jstat -gcutil vmid --监视内容与-gc基本相同,但输出关注使用空间占比

[appdev@hangzhou01 ~]$ jstat -gc 7855
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
4224.0 4224.0 2023.7  0.0   34304.0  16784.0   85428.0    80362.0   61888.0 60198.6 7104.0 6727.3   1384    5.074  12      1.024    6.098
[appdev@hangzhou01 ~]$ jstat -gcutil 7855
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
 47.91   0.00  49.03  94.07  97.27  94.70   1384    5.074    12    1.024    6.098
  • S0/S1 对应新生代Survivor,S0已使用47.91%空间
  • E 对应新生代Eden,已使用49.03%
  • O 对应老年代,已使用94.07
  • M 对应永久代,已使用97.27
  • YGC Minor GC总次数
  • YGCT Minor GC总时间
  • FGC Full GC总次数
  • FGCT Full GC总时间
  • GCT 总GC时间

jinfo-查看Java配置信息

jinfo -flag 参数 vmid --查看未被显示指定的参数的系统默认值

[appdev@hangzhou01 ~]$ jinfo -flag MaxTenuringThreshold 7855
-XX:MaxTenuringThreshold=15

ps -ef | grep vmid --查看java应用显示指定的参数值

[appdev@hangzhou01 ~]$ ps -ef | grep 7855
appdev    7730  5969  0 21:21 pts/0    00:00:00 grep --color=auto 7855
appdev    7855     1  0 Jan17 ?        00:18:54 /home/appdev/jdk8/bin/java -Djava.util.logging.config.file=/home/appdev/tomcat-jenkins/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /home/appdev/tomcat-jenkins/bin/bootstrap.jar:/home/appdev/tomcat-jenkins/bin/tomcat-juli.jar -Dcatalina.base=/home/appdev/tomcat-jenkins -Dcatalina.home=/home/appdev/tomcat-jenkins -Djava.io.tmpdir=/home/appdev/tomcat-jenkins/temp org.apache.catalina.startup.Bootstrap start

jmap-Java内存映像工具

jmap -dump:format=b,file=快照文件名 vmid --生成堆快照

[appdev@hangzhou01 ~]$ jmap -dump:format=b,file=heapDumpFile.txt 7855
Dumping heap to /home/appdev/heapDumpFile.txt ...
Heap dump file created
[appdev@hangzhou01 ~]$ ls
heapDumpFile.txt 

在当前目录下找到生成的堆快照文件,可以下载下来使用分析工具分析。

jstack-Java栈跟踪工具

jstack -l vmid >>快照文件名称 --生成线程快照,包含锁的信息

[appdev@hangzhou01 ~]$ jstack -l 7855 >> threadDumpFile.txt
[appdev@hangzhou01 ~]$ ls
threadDumpFile.txt 

线程快照是当前虚拟机内每一条线程正在执行的方法栈的集合,生成线程快照的主要目的是定位线程长时间挺短的原因,如线程死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。

可视化工具

VisualVM-多合一故障处理工具

虚拟机类加载机制

Java内存模型