Java虚拟机学习

原创
2018/06/12 18:32
阅读数 53

为什么要学习虚拟机

  • 直白一点:当然是为了工资更高一些了
  • 坦白一点:工欲善其事必先利其器。最近公司经常遇到OOM(OutOfMemory)问题,因为不知道什么原因,特此买了一本《深入理解Java虚拟机》来读,以解心中疑惑。并记录学习心得作分享。

Java内存结构

Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干区域,这些区域有各自的用途,以及创建和销毁时间。我们最关心的Java虚拟机的内存结构如下图所示: Java虚拟机内存结构

  1. 程序计数器, 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。它读取下一步执行的指令。由于CPU的时间分片机制,所以每个线程都有自己的独立的程序计数器,这样才能保证线程之间互不影响。 PS:如果线程正在执行时一个Java方法,则该计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则计数器值为空(Undefined)。此内存区域是Java虚拟机唯一没有规定任何OutOfMemoryError的区域。
  2. 虚拟机栈,这个也是线程私有的。它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行在执行的同时都会创建一个栈桢,(Stack Frame)用于存储局部变量表、操作数栈、动态链接等信息,每个方法的调用到执行就是一个栈桢入栈到出栈的过程 局部变量表存放了编译期间可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)很返回地址等。在局部变量表中,64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型占1个。 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法的生命周期内不会改变。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度(-Xss可调整栈大小)大于虚拟机所允许的深度,这个时候将会抛出StackOverFlowError异常,如果当Java虚拟机允许动态扩展虚拟机栈的时候,当扩展的时候没办法分配到内存的时候就会报OutOfMemoryError异常。
  3. 本地方法栈,与虚拟机栈基本相同,唯一的区别是虚拟机栈是执行Java方法的,本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机一样会抛出Stack OverflowError和OutOfMemoryError异常。
  4. 堆(Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域。Java堆还被细分为:新生代和老年代;再细一点还将新生代分为Eden空间、from survivor和To survivor。从内存分配角度来看,线程共享的Java堆可能分出多个线程私有的分配缓存区(Thread Local Allocation Buffer,TLAB)【后面章节详细讲解】。 堆内存

对象分配的方式:new 一个对象,如果该对象很大,就直接分配到老年区,如果不是很大就分配带新生代的eden区域,第一次GC的时候,会把eden区域没有被回收的对象(有引用)拷贝到s0区域,第二次内存回收的时候会把eden区域没有被回收的和s0区域中的对象拷贝到s1区域,并且情况s0区域。再次内存回收的时候,会把eden区域没有被内存回收的对象和s1区域的对象拷贝到s0区域,然后情况s1区域,一直这样s0区域和s1区域交替使用如果一个对象在GC的过程中,经过很多次都没有被GC,最终会被移动到老年区,这个次数可以通过参数来进行配置eden里面的对象大部分都会被GC回收,例如100个对象,GC回收异常98个对象都会被回收。老年代tenured中的对象都是经过很多次GC没有被回收的对象,通常配置eden:s0:s1区域的内存比例是8:1:1,new新生代区域:old区域的内存比例是1:3 当堆中没有内存完成实例分配,并且堆无法扩展时,抛出OutOfMemoryError异常。当前主流的虚拟机都是可以扩展的,通过-Xmx和-Xms控制。

  1. 方法区,和Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 方法区也叫“永久区”,方法区在物理上也是不需要连续的,可以选择固定大小或者扩展的大小,还可以选择不实现垃圾收集,方法区的垃圾回收是比较少的,这就是方法区为什么被称为永久区的原因,但是方法区也是可以执行回收的,该区域主要是针对常量池和类型的卸载;在方法区也规定当方法区无法满足内存分布的时候,将会抛出OutOfMemoryError异常; 运行时常量池(Runtime Constant Pool)是方法区的一部分,常量池存放编译器生成的各种字面量和符号引用。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
  2. 直接内存,并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,并可能导致OOM异常出现。JDK1.4后引入NIO,它可以使用Native函数库直接分配堆外内存,然后通过各存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

HotSpot虚拟机探秘

主要说明HotSpot虚拟机在Java堆中对象的分配、布局和访问过程。

对象的创建

Java对象创建流程图

命令:-XX:+/-DoEscapeAnalysis -XX:+/-EliminateAllocations -XX:+/-UseTLAB -XX:+PrintGC 打开/关闭逃逸分析、打开/关闭标量替换、打开/关闭线程本地内存、打印GC信息

  • 栈上分配 线程私有对象 无逃逸 支持标量替换 无需调整
  • 线程本地分配TLAB(Thread Local Allocation Buffer) 占用Eden,默认1% 多线程时,不用竞争就可以申请空间,提高效率 小对象(一般几十个byte) 无需调整
  • 老年代 大对象
  • Eden 新生对象

对象的访问

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中规定了一个执行对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。

  • 如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。 -- 好处:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普通的行为)时只会改变句柄中的实例数据指针而reference本身不需要修改。

  • 如果使用直接指针访问,那么Java堆对象的布局中必须考虑如何放置类型数据的相关信息,而reference中存储的直接地址就是对象地址。 -- 好处:速度更快,节省了一次指针定位的时间开销。sun hotspot 虚拟机就是通过这种方式进行对象访问的。

小结

以上介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行出栈和入栈操作。每个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。

垃圾收集器和内存分配策略

在进行垃圾回收时,通常要确定的有3件事、 - 哪些内存需要回收(也即什么是垃圾) - 什么时候回收 - 如何回收

对象已死吗?

什么样的对象是垃圾?一般来说,所有指向对象的引用都已失效,不可能再有程序能调用到这个对象,那么这个对象就成了垃圾,应该被回收。所以垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象中哪些对象还活着,哪些对象已死去。

引用计数算法

根据上述的描述,很容易想到引用计数算法来确定对象是否已死去。即:给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用。也就是该对象可以被回收了。 但是引用计数算法有一个致命问题且不好解决,就是循环引用的问题。引用计数在任何时候都不为0,但又没有其他的对象引用他们,于是引用计数算法无法通知GC收集器来回收他们。

public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

执行结果:

[GC (System.gc()) [PSYoungGen: 10859K->1784K(38400K)] 10859K->1792K(125952K), 0.0028838 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1784K->0K(38400K)] [ParOldGen: 8K->1693K(87552K)] 1792K->1693K(125952K), [Metaspace: 3498K->3498K(1056768K)], 0.0195677 secs] [Times: user=0.09 sys=0.01, real=0.02 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x00000000d5d80000, 0x00000000d8800000, 0x0000000100000000)
  eden space 33280K, 1% used [0x00000000d5d80000,0x00000000d5dd34a8,0x00000000d7e00000)
  from space 5120K, 0% used [0x00000000d7e00000,0x00000000d7e00000,0x00000000d8300000)
  to   space 5120K, 0% used [0x00000000d8300000,0x00000000d8300000,0x00000000d8800000)
 ParOldGen       total 87552K, used 1693K [0x0000000081800000, 0x0000000086d80000, 0x00000000d5d80000)
  object space 87552K, 1% used [0x0000000081800000,0x00000000819a7698,0x0000000086d80000)
 Metaspace       used 3504K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 378K, capacity 388K, committed 512K, reserved 1048576K

从运行结果中可以看到,GC日志中包含“1792K->1693K(125952K)”意味着虚拟机并没有因为这两个对象相互引用就不回收他们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

在主流的程序语言中,都是通过可达性分析来判断对象是否存活的。这个算法的基本思路就是通过一系列的成为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(不可达)时,说明该对象是不可用的,所以它将被判定为可回收对象。

可达性分析算法判定对象是否可回收

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈桢中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

无论是通过引用计数算法还是可达性分析,判断对象是否存活都与“引用”有关。

  • 强引用,在程序代码中普遍存在,例如Object o = new Object(),只要强引用还在,垃圾收集器则不会回收掉被引用的对象
  • 软引用,用来描述一些还有用但非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象进行回收,如果这次回收后后还没有足够的内存,则会抛出内存溢出异常。SoftReference来实现软引用
  • 弱引用也是用来描述非必需的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。WeakReference来实现弱引用。
  • 虚引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。为一个对象设置虚引用的唯一目的是能再这个对象被收集器回收的时收到一个系统通知。PhantomReference来实现虚引用。

垃圾收集算法

这里只介绍几种常见的垃圾收集算法

标记-清除法(Mark-Sweep)

最基础的收集算法是“标记-清除”算法,首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。 它的不足有两个:效率问题,标记和清除两个过程的效率都不高;空间问题,标记和清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

标记-清除 算法示意图

复制算法

为了解决效率问题,复制算法应运而生。它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样就是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况。它的不足:内存使用率问题,这种算法的代价是将内存缩小为原来的一半。

复制算法示意图

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM研究得出新生代将内存划分为为一块较大的Eden空间和两个较小的Survivor空间。当回收时,将Eden和survivor中还存活的对象一次性地复制到另外一个survivor空间上,最后清理掉Eden和刚才的额survivor空间。Hotspot虚拟机默认Eden和survivor的大小比例是8:1:1。当survivor空间不够用时,需要依赖年老代进行分配担保。也就是这些对象将通过分配担保机制进入年老代。

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会遍地,最关键的是,如果不想浪费50%的内存空间,就需要有额外的空间进行分配担保,以对应被使用的内存中所有对象都100%存活的极端情况,所以年老代不能用复制收集算法。 标记-整理算法,标记过程仍与“标记-清除”算法的过程一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都像一端移动,然后直接清理掉边界以外的内存。

标记-整理 算法示意图

分代收集算法

当前商业虚拟机的垃圾收集都采用“分代手机”算法,根据对象存活周期的不同将内存划分为几块。一般是把堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。在新生代中,每次垃圾收集时都发现有大量对象死去,只有少量存活,那就选用复制算法,只需付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外的空间分配担保,则使用“标记-整理”算法或者“标记-清除”算法进行回收。

新生代(Young Generation)

  • 所有新生成的对象首先都是放在新生代上。新生代的目标就是尽可能的收集掉哪些生命周期短的对象
  • 新生代内存按照8:1:1的比例分为Eden去和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时,先将Eden区存活的对象复制到survivor0区,然后清空Eden区,当这个survivor0区也存放满了,则将Eden区和survivor0区存活对象复制到survivor1区,然后清空Eden去和survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区互换,既保持survivor1区为空,如此往复。
  • 当survivor1区不足以存放Eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代,如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
  • 新生代发生的GC也叫Minor GC,发生频率比较高

老年代(Old Generation)

  • 在新生代中经历了N次垃圾回收扔存活的对象,就会被放到老年代中,因此,可以认为老年代存放的都是一些生命周期较长的对象。
  • 内存比新生代大很多(大概是1:2),当老年代的内存满时触发Major GC即Full GC,Full GC发生频率较低

持久代(Permanent Generation)

  • 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。在Java 1.7以前也叫方法区。

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机中有四种垃圾回收器:

  • 串行垃圾回收器(Serial Garbage Collector)
  • 并行垃圾回收器(Parallel Garbage Collector)
  • 并发标记扫描垃圾回收器(CMS Garbage Collector)
  • G1垃圾回收器(G1 Garbage Collector)

每种类型都有自己的优势和劣势,理解每一种类型的垃圾回收器并且根据应用程序选择正确的垃圾回收器。

Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器,它是一个单线程的收集器,不仅只会用一个CPU或一个线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须在暂停其他所有的工作线程,直到它收集结束 Serial收集器是虚拟机在Client模式下默认新生代收集。 好处:简单而高效,对于限定的单个CPU的环境来说,Serial收集器由于没有线程交互的开销,效率更高。

ParNew 收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为斗鱼serial收集器完全一样。它时许多运行在Server模式下的虚拟机中首选的新生代收集器。因为除了serial收集器之外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge 收集器

Parallel Scavenge收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。它的特别之处在于它的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即

** 吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)**,虚拟机总共运行了100分钟,其中垃圾收集时间花掉1分钟,那么吞吐量就是99%。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理算法那”。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它的主要用途有两个:

  • 在JDK1.5之前的版本中与Parallel Scavenge收集器搭配使用
  • 作为CMS收集器的后预备方案。在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器使之唉JDK1.6之后才开始提供的。可与Parallel Scavenge搭配使用,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑该组合。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是基于“标记-清除”算法实现的。它的运作过程比前面几种收集器来说更复杂,整个过程可分为4步:

  • 初始标记(CMS initial mark):仅仅是标记一下GC Roots能直接关联到的对象,速度快
  • 并发标记(CMS concurrent mark):GC Roots Tracing过程
  • 重新标记(CMS remark):修正并发标记期间因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录。停顿时间比初始标记时间长,但是比并发标记时间短
  • 并发清除(CMS concurrent sweep)

整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户程序一起工作,所以从总体来说,CMS收集器的内存回收过程是并发的。

CMS的优点:并发收集、低停顿。但CMS也有明显的3个缺点:

  • CMS收集器对CPU资源非常敏感。面向并发的程序都对CPU资源敏感。在并发阶段,会占用一部分的CPU资源导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数 = (CPU数量 + 3 )/ 4
  • CMS收集器无法处理浮动垃圾,可能会出现Concurrent Mode Failure 失败而导致另一次Full GC的产生。
  • CMS收集器是基于“标记-清除”算法的,这意味着回收结束后会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC。虽然CMS提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在CMS收集器顶不住要FullGC的时候开启内存碎片整合过程,但是内存整理是无法并发的,空间碎片问题没有了,但停顿时间不得不边长。

G1 收集器

G1(Garbage First)收集器是当前收集器技术发展的最前沿成果之一。G1是一款面向服务端应用的垃圾收集器。与其他GC收集器相比,G1具备一下特点:

  • 并发和并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间。
  • 分代收集
  • 空间整合:G1从整体上看是基于“标记-整理”算法实现的,从局部(两个Region之间)是基于“复制”算法实现的
  • 可预测停顿

理解GC日志

理解GC日志是处理Java虚拟机内存问题的基础技能。每一种收集器的日志形式都是由它们自身的实现所决定的。但也有一定的共性。


import java.util.ArrayList;
import java.util.List;

/**
 * @author Chensheng.Ku
 * @version 创建时间:2018/6/12 9:27
 */
public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;
        System.gc();
    }
    public static void testHeapGC(){
        List<ReferenceCountingGC> list = new ArrayList<>();
        while(true){
            list.add(new ReferenceCountingGC());
        }
    }

    public static void main(String[] args) {
        testHeapGC();
    }
}

运行结果:

[GC (Allocation Failure) [PSYoungGen: 31343K->5094K(38400K)] 31343K->26345K(125952K), 0.0612702 secs] [Times: user=0.06 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 36458K->5110K(71680K)] 57709K->57089K(159232K), 0.0136663 secs] [Times: user=0.02 sys=0.03, real=0.02 secs] 
[GC (Allocation Failure) [PSYoungGen: 69867K->5110K(71680K)] 121847K->120586K(187392K), 0.0293396 secs] [Times: user=0.02 sys=0.03, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5110K->4564K(71680K)] [ParOldGen: 115476K->115475K(220160K)] 120586K->120040K(291840K), [Metaspace: 3505K->3505K(1056768K)], 0.0441425 secs] [Times: user=0.11 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 69283K->4646K(132096K)] 184759K->181562K(352256K), 0.0258431 secs] [Times: user=0.03 sys=0.02, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4646K->0K(132096K)] [ParOldGen: 176916K->181476K(311808K)] 181562K->181476K(443904K), [Metaspace: 3509K->3509K(1056768K)], 0.0404066 secs] [Times: user=0.08 sys=0.00, real=0.05 secs] 
[GC (Allocation Failure) [PSYoungGen: 125286K->4224K(138240K)] 306762K->306533K(450048K), 0.0466533 secs] [Times: user=0.02 sys=0.08, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4224K->0K(138240K)] [ParOldGen: 302309K->306407K(495104K)] 306533K->306407K(633344K), [Metaspace: 3509K->3509K(1056768K)], 0.0612860 secs] [Times: user=0.09 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 131562K->75872K(262144K)] 437970K->437576K(757248K), 0.0935759 secs] [Times: user=0.03 sys=0.11, real=0.09 secs] 
[GC (Allocation Failure) [PSYoungGen: 259636K->96384K(281600K)] 621340K->619881K(807424K), 0.0690443 secs] [Times: user=0.11 sys=0.17, real=0.08 secs] 
[Full GC (Ergonomics) [PSYoungGen: 96384K->94211K(281600K)] [ParOldGen: 523497K->525546K(763392K)] 619881K->619758K(1044992K), [Metaspace: 3509K->3509K(1056768K)], 0.0680246 secs] [Times: user=0.14 sys=0.00, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 278020K->116864K(392192K)] 803567K->802156K(1155584K), 0.0772084 secs] [Times: user=0.11 sys=0.19, real=0.08 secs] 
[Full GC (Ergonomics) [PSYoungGen: 116864K->38912K(392192K)] [ParOldGen: 685291K->763121K(1041408K)] 802156K->802034K(1433600K), [Metaspace: 3509K->3509K(1056768K)], 0.0618044 secs] [Times: user=0.13 sys=0.06, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 310476K->139393K(414720K)] 1073598K->1072500K(1456128K), 0.0966146 secs] [Times: user=0.11 sys=0.11, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 139393K->32768K(414720K)] [ParOldGen: 933107K->1039607K(1356288K)] 1072500K->1072375K(1771008K), [Metaspace: 3509K->3509K(1056768K)], 0.0625357 secs] [Times: user=0.05 sys=0.03, real=0.06 secs] 
[GC (Allocation Failure) [PSYoungGen: 304346K->166017K(468480K)] 1343953K->1342841K(1824768K), 0.1447390 secs] [Times: user=0.16 sys=0.28, real=0.14 secs] 
[Full GC (Ergonomics) [PSYoungGen: 166017K->0K(468480K)] [ParOldGen: 1176824K->1342717K(1381888K)] 1342841K->1342717K(1850368K), [Metaspace: 3509K->3509K(1056768K)], 0.2634404 secs] [Times: user=0.26 sys=0.08, real=0.27 secs] 
[Full GC (Ergonomics) [PSYoungGen: 300809K->260097K(468480K)] [ParOldGen: 1342717K->1381633K(1381888K)] 1643527K->1641731K(1850368K), [Metaspace: 3509K->3509K(1056768K)], 0.1784625 secs] [Times: user=0.20 sys=0.08, real=0.17 secs] 
[Full GC (Ergonomics) [PSYoungGen: 300815K->299010K(468480K)] [ParOldGen: 1381633K->1381633K(1381888K)] 1682449K->1680644K(1850368K), [Metaspace: 3509K->3509K(1056768K)], 0.0413022 secs] [Times: user=0.01 sys=0.00, real=0.04 secs] 
[Full GC (Ergonomics) [PSYoungGen: 301058K->301058K(468480K)] [ParOldGen: 1381633K->1381633K(1381888K)] 1682692K->1682692K(1850368K), [Metaspace: 3509K->3509K(1056768K)], 0.0353384 secs] [Times: user=0.02 sys=0.00, real=0.03 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 301058K->301058K(468480K)] [ParOldGen: 1381633K->1381615K(1381888K)] 1682692K->1682673K(1850368K), [Metaspace: 3509K->3509K(1056768K)], 0.4115150 secs] [Times: user=0.64 sys=0.01, real=0.42 secs]
  • GC日志的开头"[GC"和"[Full GC"说明了这次垃圾回收的停顿类型。如果有Full,说明这次GC是发生了Stop-The-World的,如果是调用System.gc()方法所触发的收集,那么这里将显示"[Full GC(System)"。
  • 接下来的"[PSYoungGen"、"[ParOldGen"、"[Metaspace"表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,这里显示的PSYoungGen,所以JDK采用的是Parallel Scavenge收集器。老年代和永久代同理,名称也是又收集器决定的。
  • 后面方括号内部的,例如"PSYoungGen: 310476K->139393K(414720K)"含义是“GC前该内存区域已使用容量 -> GC后该内存区域已使用容量(该内存区域总容量)”。
  • 后面方括号外部的,例如"1343953K->1342841K(1824768K)"表示“GC前Java堆已使用容量 -> GC后Java堆已使用容量(Java堆总容量)”
  • 再往后“0.1447390 secs”表示该内存区域GC所占用的时间,单位是秒(sec),[Times: user=0.16 sys=0.28, real=0.14 secs] 表示用户态消耗的时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间。PS:CPU时间和墙钟时间的区别是:墙钟时间包括各种非运算的等待消耗时间,而CPU不包括这些时间。

内存分配策略与回收策略

对象的内存分配,往大方向来讲,就是在堆上分配(但可能经过JIT编译后备拆散为标量类型并间接的再栈上分配),对象主要分布在新生代的Eden区上,如果启动了本地线程分配缓冲(LTAB),将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代

所谓大对象是指,需要大量连续内存空间的Java对象,最典型的的对象就是那种很长的字符串以及数组。虚拟机提供了一个-XX:PretenurseSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden去以及两个survivor区之间发生大量的内存复制。**注意:**PretenurSizeThreShold参数只对Serial和ParNew两款收集器有效。

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

为了坐到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生病经过第一次Minor GC后仍然存活的,并且能够被Survivor容纳的话,将被移动到survivor空间,并且对象年龄+1.可通过-XX:MinTenuringThreshold和 -XX:MaxTenuringThreshold来设置。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用 的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试着进行一次 Minor GC。如果小于,这时就要 改为进行一次Full GC。

JVM调优常用参数

  • 堆大小调整
参数名称 含义 默认值 说明
-Xms 初始堆大小 物理内存的1/64 默认(MinHeapFreeRatio)空余堆内存小于40% 时,JVM就会增加堆直到-Xmx的最大值
-Xmx 最大堆大小 物理内存的1/4 默认(MaxHeapFreeRatio)空余堆内存大于70%时,JVM就会减少堆直到-Xms的设置值
-Xmn 年轻代大小 此处的大小 = Eden+survivor * 2
-XX:PermSize 设置持久代大小 物理内存的1/64
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4
-Xss 每个线程的堆栈大小 JDK1.5后每个线程堆栈大小为1M。减少这个值能生成更过的线程。但是操作系统对一个进程内的线程数还是有限制的,如果栈不够深,应该是128K够用,大的应用建议设置为256K。这个选项对性能影响较大
-XX:ThreadStackSize 线程栈大小
-XX:NewRatio 年轻代与老年代的比值 -XX:NewRatio=4表示年轻代和老年代的比值为1:4。在Xms=Xmx并且设置了Xmn的情况下不需要设置该参数
-XX:SurvivorRatio Eden区和Survivor区大小比值 8 设置为8,表示Eden:survivor0:survivor1 = 8:1:1.
-XX:+DisableExplicitGC 关闭System.gc() 禁止使用程序的System.gc()方法
-XX:MaxTenuringThreshold 进入老年代最大年龄 如果设置为0的话,则年轻代对象将不经过survivor区,直接进入老年代。
-XX:PretenureSizeThreshold 对象大小超过该值将在老年代分配 0
-XX:TLABWasteTargetPercent TLAB占Eden区的百分比 1% 不建议调整
  • 收集器相关参数
参数名称 含义
-XX:+UseParallelGC 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenger + Serial Old的收集器组合进行内存回收
-XX:+UseParNewGC 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
-XX:+UseCon从MarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。Serial Old 收集器将作为CMS出现Concurrent Mode Failure失败后的后备收集器使用
-XX:+HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应对新生代的整个Eden和Survivor区所有对象都存活的极端情况
-XX:+UseParallelOldGC 打开此开关后使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收
-XX:+ParallelGCThreads 并行收集器的线程数。一般设置为与处理器数目相等
  • 辅助信息
参数名称 说明
-XX:+PrintGC 输出GC日志信息
-XX:+PrintGCDetails 输出日志详细信息

总结

此次学习,主要学习了垃圾收集算法、几款JDK1.7中提供的垃圾收集器特点,通过代码实例验证了Java虚拟机中自动内存分配和回收的主要规则。内存回收和垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及大量可调参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部
返回顶部
顶部