深入理解Java虚拟机之基础篇

 2019-12-22 10:42  阅读(763)
文章分类:JVM

前言:本文为作者学习深入理解JAVA虚拟机一书的一个总结,以供日后复习查阅。本文基于jdk1.8通过结合书中内容以及个人的理解来完成。本文为一个JVM的入门文章不会涵盖到书中的所有内容,甚至是高级部分的内容。

JDK默认的虚拟机

在开始介绍Java虚拟机之前介绍下一些基础的知识。首先当前JDK的版本中自带的(或者说默认)使用的是HotSpot虚拟机。此外JVM有两种运行模式分别是Client以及Server。在Dos下java -version可以查看,至于两者的区别不再赘述。

2019120001226\_1.png

Java虚拟机的内存区域及内存溢出异常

Java虚拟机在执行Java程序的过程中会把所管理的内存分为若干个数据区域,根据《Java虚拟机规范》规定可以分为下图的几个运行时的数据区域。下面将介绍下各个数据区域以及其各自内存溢出的案例。

2019120001226\_2.png

首先5个数据区域分为了线程共享以及线程隔离,顾名思义就是说程序运行的时候每个线程都拥有各自独立的虚拟机栈,本地方法栈,程序计数器三块数据区域,而方法区以及堆是所有线程共享的。在简单的介绍之后来看看各个区域的作用:

**方法区(永久代):**它用来存储类信息,常量,静态变量,即时编译器编译后的代码等数据。注意在JDK1.8中没有了方法区的概念,改成了元数据空间Metaspace,原有方法区存放的数据位置也有所改变。有关更多的区别或者JDK1.8的内容不再赘述。

**虚拟机栈:**它的生命周期跟线程相同,也就是说每开启一个线程就会有对应的一个虚拟机栈。它用来保存栈帧(Stack Frame),栈帧中存储局部变量表(各种基本数据类型以及对象的引用),方法出入口等信息。每一个方法执行的时候就会创建一个栈帧,而从一个方法的调用直至执行完成的过程就对应一个栈帧在虚拟机栈中的入栈以及出栈的过程。-Xss

2019120001226\_3.png

**Java堆:**它是Java虚拟机所管理的内存中最大的一块,它用来储存对象的实例,也就是说所有的对象实例以及数组都要在堆上分配内存,是垃圾收集器(GC)管理的主要区域。它可以分为新生代以及老年代,再细分还可以分为Eden,From Survivor,To- Survivor这个在后续会进行介绍。

**本地方法栈:**它是用来存放Java中一些Native方法,例如在解析ArrayList一文中经常看到的System.arraycopy。

**程序计数器:**它可以看做是当前线程所执行的字节码的行号指示器。JVM通过它来完成字节码指令,循环,跳转,分支等基础功能。也就是说虚拟机在执行程序的时候会根据程序计数器中保存的行号来完成下一步的执行。此外它不会发生内存溢出OOM。

**直接内存:**它并不是虚拟机运行时数据区的一部分也不是Java虚拟机规范中定义的内存区域。例如在NIO中可以通过ByteBuffer的allocateDirect来申请直接内存。这样因为避免了在Java堆以及Native堆中来回复制数据从而提高了性能。

**运行时常量池:**它用来存放编译期生成的各种字面量和符号的引用,它是方法区的一部分。

对象的创建

Java中通过关键字new来创建一个对象的实例。在虚拟机中创建对象主要有两个步骤:

  1. 首先虚拟机遇到了一条new指令,它会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,接着会检查这个类是否被加载,解析和初始化过。如果没有的话就必须先执行相应的类加载。
  2. 类加载的检查通过后虚拟机为新生的对象分配内存。分配内存时候针对Java堆是否规整分为两种情况:第一种情况Java堆中内存是规整的,所以用过的内存放一边,空闲的内存放一边,中间有一个指针作为分解点的指示器,那所分配的内存就是把指针指向空闲空间那边挪动一段与对象大小的相等的距离,这种分配方式叫做指针碰撞(Bump the Pointer)。举个例子(参考后文中的垃圾收集算法示意图)当前图上的所有网格块为1k的空间,当一个对象需要分配3k内存的时候,如果有连续的三块内存为1k的网格块那么就可以分配到内存。第二种情况Java堆中的内存是不规整的,也就是所已使用的内存空间跟空闲内存空间相互交错,这个时候就无法进行指针碰撞。根据上文的例子来说没有办法为一个3k的对象分配到一个连续的3k内存。这个时候虚拟机就必须维护一个列表,记录哪些内存块是可用的,分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的记录,这种分配方式叫做空闲列表(Free List)。

至于选择哪种内存的分配方式由所采用的垃圾收集器是否带有压缩整理功能(垃圾回收算法)来决定。垃圾收集器在后文中会提到,可以先知道的是Serial,Parnew等带Compact过程的收集器采用的是指针碰撞,而CMS采用的是空闲列表。

此外在虚拟机中创建对象是非常频繁的行为针对并发的情况有两个解决方案:

  1. 采用CAS配上失败充实的方法保证更新操作的原子性。
  2. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Therea Local Allocation Buffer,TLAB)。哪个线程需要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB可以通过-XX:+/-UserTlLAB参数来设定。

以上是虚拟机中创建对象的主要的两个步骤,经过上述的步骤之后从虚拟机的角度来看一个对象已经产生了,但是从Java程序的角度来说对象还没有完成创建(还没有初始化)。这边就不再赘述了。

对象的访问定位

下图为HotSpot虚拟机下一个对象的访问定位(通过直接指针访问对象)例子:

public class Test1 {

        public static void main(String[] args) {    
            test();
        }
        public static void test(){
            int number = 1;
            String name = "test";
            Test2 t2 = new Test2();
            float f = 0.0f;
            double d = 0.0d;
        }
    }

    public class Test2 {

        private int age;

        private String name;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }   
    }

2019120001226\_4.png

内存溢出OOM(OutOfMemoryError)

Java堆溢出

/**
     * VM Args : -verbose:gc -XX:+PrintGCDetails 打印GC信息  -Xms20M 虚拟机的最小内存 -Xmx20M 虚拟机的最大内存 -Xmn10M新生代的内存  
     * @author Administrator
     *
     */
    public class OOMTest {

        public static void main(String[] args) {
            
            String[] s = new String[100000000];
            
        }

    }

运行结果:

[GC [PSYoungGen: 835K->568K(9216K)] 835K->568K(19456K), 0.0012390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC [PSYoungGen: 568K->504K(9216K)] 568K->504K(19456K), 0.0012505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC [PSYoungGen: 504K->0K(9216K)] [ParOldGen: 0K->463K(10240K)] 504K->463K(19456K) [PSPermGen: 2550K->2550K(21504K)], 0.0151389 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
    [GC [PSYoungGen: 0K->0K(9216K)] 463K->463K(19456K), 0.0003908 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 463K->452K(10240K)] 463K->452K(19456K) [PSPermGen: 2550K->2550K(21504K)], 0.0079912 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at OOMTest.main(OOMTest.java:10)
    Heap
     PSYoungGen      total 9216K, used 409K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 5% used [0x00000000ff600000,0x00000000ff666740,0x00000000ffe00000)
      from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
      to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
     ParOldGen       total 10240K, used 452K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 4% used [0x00000000fec00000,0x00000000fec71038,0x00000000ff600000)
     PSPermGen       total 21504K, used 2581K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
      object space 21504K, 12% used [0x00000000f9a00000,0x00000000f9c85578,0x00000000faf00000)

可以看到如果是Java堆发生内存溢出,那么错误信息中会指出Java heap space。

虚拟机栈和本地方法栈溢出

/**
     * VM Args : -Xss减少栈内存容量
     * @author Administrator
     *
     */
    public class OOMTest {

        private  int length;

        public static void main(String[] args) {
            OOMTest o = new OOMTest();
            try{
                o.test();
            }catch(Throwable e){
                System.out.println("length="+o.length);
                e.printStackTrace();
            }
        }

        public void test(){
            length++;
            test();
        }

    }

运行结果:

length=987
    java.lang.StackOverflowError
        at OOMTest.test(OOMTest.java:21)
        at OOMTest.test(OOMTest.java:22)
        at OOMTest.test(OOMTest.java:22)
        at OOMTest.test(OOMTest.java:22)
        at OOMTest.test(OOMTest.java:22)
        at OOMTest.test(OOMTest.java:22)
        at OOMTest.test(OOMTest.java:22)

虚拟机栈跟本地方法栈会出现两种异常:如果线程请求的栈深度大于虚拟机所允许的最大深度就抛出StackOverFlowError异常;如果虚拟机在扩展栈时无法申请到足够的内存空间则抛出OutOFMemoryError异常。上述的例子为第一种异常通过递归来实现,第二种异常在单线程下难以实现。

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

/**
     * VM Args : -XX:PermSize=10M -XX:MaxPermSize=10M (1.8之前用于方法区)
     *           1.8用 -XX:MetaspaceSize -XX:MaxMetaspaceSize
     * @author Administrator
     *
     */
    public class OOMTest {
        public static void main(String[] args) {
            List list = new ArrayList();
        }
    }

运行结果:

Error occurred during initialization of VM
    OutOfMemoryError: Metaspace

因为1.8中已经没有了方法区这个概念,并且要让元数据空间抛出异常比较麻烦,所以上述的测试在配置参数设置元数据空间为4就可以得到OOM异常。可以看到异常信息中指出的是Metaspace。

本机直接内存溢出

import java.lang.reflect.Field;
    import java.nio.ByteBuffer;
    import java.util.ArrayList;
    import java.util.List;

    import sun.misc.Unsafe;
    /**
     * VM Args : -Xmx20M -XX:MaxDirectMemorySize=10M 设置直接内存的容量
     * @author Administrator
     *
     */
    public class OOMTest {

        public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];  
            unsafeField.setAccessible(true);  
            Unsafe u = (Unsafe) unsafeField.get(null); 
            while(true){
                u.allocateMemory(1024*1024);
            }
    /*      List<ByteBuffer> list = new ArrayList<ByteBuffer>();
            while(true){
                list.add(ByteBuffer.allocateDirect(1024*1024));
            }*/
        }

    }

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError
        at sun.misc.Unsafe.allocateMemory(Native Method)
        at OOMTest.main(OOMTest.java:16)

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
        at java.nio.Bits.reserveMemory(Bits.java:658)
        at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
        at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)
        at OOMTest.main(OOMTest.java:20)

上面贴出了两个运行结果,分别对应代码中的两种实现案例。第二种通过NIO ByteBuffer申请直接内存的方法实际上在底层的实现也是调用了Undafe的allocateMemory方法。

内存溢出与内存泄露

上述的案例都是内存溢出的情况,现在针对内存泄露做个简单的介绍。所谓的内存溢出简单来说就是内存满了JVM无法分配更多的内存给你。内存泄露是因为程序中有一些对象不会被垃圾回收,一直占据着内存从而对系统性能造成潜在的影响。例如过期引用,无用又不处理的缓存。过期引用就是指永远也不会被解除的引用,垃圾回收机制也不会处理。针对过期引用的处理在jdk源码中可以很容易看到。例如在jdk1.8ArrayList主要方法和扩容机制(源码解析)一文中,每次ArrayList删除对一个数组对象的时候,都会把这个对象的引用设置为NULL:elementData[–Size]=null;来让GC回收,防止过期引用存留发生内存泄露。

理解GC(垃圾收集,Garbage Collection)日志

在Java堆内存的时候打印了一些GC的日志,这边针对这些日志来做一些介绍。首先在下面两条日志可以看到有GC还有FullGC,它们表示的是这次垃圾收集的停顿类型,如果是Full GC(如果是调用System.gc( )就变成Full GC(System))就说明这次GC发生了STW(Stop-The-World),STW在后面介绍收集器的时候会说到。PSYoungGen表示采用的是Parallel Scavenge收集器,同样的如果是其他的收集就是DefNew,ParNew等等。方括号内的568K->504K表示GC前该区域内存的使用容量->GC后该区域内存的使用容量。方括号外的568K->5.4K表示GC前Java堆已使用的容量->GC后Java堆已使用的容量,0.0012505表示GC的时间,单位是秒secs。此外还有ParOldGen,PSPermGen这些表示的是GC发生的区域,Times中user,sys和real表示用户态消耗的CPU时间,内核态消耗的CPU和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

[GC [PSYoungGen: 568K->504K(9216K)] 568K->504K(19456K), 0.0012505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [Full GC [PSYoungGen: 504K->0K(9216K)] [ParOldGen: 0K->463K(10240K)] 504K->463K(19456K) [PSPermGen: 2550K->2550K(21504K)], 0.0151389 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Java虚拟机的内存回收

什么时候进行回收

系统进行垃圾回收有三种情况:

  1. 程序中调用了System.gc( )。
  2. 当新生代中的内存不够分配给新对象的时候发生的Minor GC。
  3. 当老年代中的内存不够分配给对象的时候发生Full GC(或者说是Major GC),执行Full GC的时候会有STW(Stop The World)问题。此外System.gc( )执行的就是Full GC。

如果对上述的术语中有不理解的地方可以先暂时混个脸熟,知道有这么个东西后阅读完后面的文章就可以理解了。

哪些东西需要进行回收

前文介绍的虚拟机栈,本地方法栈,程序计数器三个区域都是随线程生,随线程灭,线程结束内存也就回收了,因此这三个区域不需要过多的考虑GC问题。而Java堆和方法区则不一样,每个类每个方法需要分配的内存都不同,甚至需要在程序运行期间才知道创建了多少对象,这些内存的分配都是动态的。因此这两个数据区域是需要考虑GC的。

Java堆上对象的回收

有引用计数算法以及可达性分析算法来确定一个对象是否需要被回收。下面对两种算法进行介绍:

**引用计数算法:**给对象添加一个引用计数器,如果有地方引用到它计数器加一当引用失效的时候减一,任何时候计数器为0就进行回收。但是引用计数算法无法解决相互引用的问题,比如下面实例中t1跟t2虽然指向了Null,但是因为t1,t2相互引用,那么计数器就不为0这个时候GC就无法回收。而通过下面的代码运行结果可以看到t1,t2还是被GC了所以HotSpot并不是通过引用计数算法来判断对象是否需要被回收。

public class OOMTest {

        public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
            OOMTest t1 = new OOMTest();
            OOMTest t2 = new OOMTest();
            t1 = t2;
            t2 = t1;
            t1 = null;
            t2 = null;
            System.gc();
        }
    }

[GC [PSYoungGen: 1351K->568K(38912K)] 1351K->568K(125952K), 0.0033675 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
    [Full GC [PSYoungGen: 568K->0K(38912K)] [ParOldGen: 0K->466K(87040K)] 568K->466K(125952K) [PSPermGen: 2552K->2551K(21504K)], 0.0102347 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
    Heap
     PSYoungGen      total 38912K, used 1013K [0x00000007d5680000, 0x00000007d8180000, 0x0000000800000000)
      eden space 33792K, 3% used [0x00000007d5680000,0x00000007d577d7d0,0x00000007d7780000)
      from space 5120K, 0% used [0x00000007d7780000,0x00000007d7780000,0x00000007d7c80000)
      to   space 5120K, 0% used [0x00000007d7c80000,0x00000007d7c80000,0x00000007d8180000)
     ParOldGen       total 87040K, used 466K [0x0000000780400000, 0x0000000785900000, 0x00000007d5680000)
      object space 87040K, 0% used [0x0000000780400000,0x0000000780474998,0x0000000785900000)
     PSPermGen       total 21504K, used 2558K [0x000000077b200000, 0x000000077c700000, 0x0000000780400000)
      object space 21504K, 11% used [0x000000077b200000,0x000000077b47fa30,0x000000077c700000)

**可达性分析算法:**通过一系列称为GC Roots的对象作为起点向下搜索,搜索的路径称作引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连那么这个对象就会被判定为可GC的对象。GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。(参考上图对象访问定位,如果栈帧中没有引用)
  • 方法区中类静态属性引用的对象。(参考上图对象访问定位,如果没有其他对象的静态属性引用)
  • 方法区中常量引用的对象。(参考上图对象访问定位,如果没有其他对象的常量引用,以String对象来思考)
  • 本地方法栈中JNI(一般说的Native方法)引用的对象。

2019120001226\_5.png

上述的两种算法都是通过对象的引用来判断对象是否存活,经常可以看到针对引用有一个简单的定义就是reference类型的数据中存储的数值代表的是另一块内存的起始地址就称作这块内存代表着一个引用。例如OOMTest t1 = new OOMTest(),t1这个reference类型的数据保存了一个地址,它指向Java堆上OOMTest对象,t1就叫做一个引用。实际上Java的引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,四种引用强度依次减弱:

  • **强引用:**类似OOMTest t1 = new OOMTest()就是一个强引用,只要强引用存在就永远不会回收对象。
  • **软引用:**用来描述一些有用但非必须的对象。对于软引用关联的对象他们在快要发生OOM之前才会对这些对象进行回收,如果回收后还没有足够的内存才会抛出OOM异常。Java中提供了类SoftReference来实现软引用。
  • **弱引用:**用来描述视非必须的对象。对于弱引用对象它只能存活到下一次GC之前。一旦发生GC无论是否内存够用都会被回收。Java中提供了类WeakReference来实现弱引用,通过WeakHashMap可以保存WeakReference(底层的对象数组中的对象继承自WeakReference)。
  • **虚引用:**为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。Java中提供了类PhantomReference来实现虚引用。

下面这个简单的实例,演示了在Java中后面三种的使用,通过运行结果可以看到SoftReference除非是发生OOM之前才会回收否则不会被回收。而WeakReference以及PhantomReference执行Sysem.gc()的时候就被回收了。此外要创建PhantomReference的实例必须带有ReferenceQueue。

public class References {

        private static ReferenceQueue<Test> rq = new ReferenceQueue<Test>();

        public static void checkQueue(){
            Reference<? extends Test> inq = rq.poll();
            if(inq != null)
                System.out.println("In queue: "+ inq.get());
        }

        public static void main(String[] args){
            int size = 10;
            LinkedList<SoftReference<Test>> sa = new LinkedList<SoftReference<Test>>();
            for(int i =0;i<size;i++){
                sa.add(new SoftReference<Test>(new Test("Soft "+i),rq));
                System.out.println("Just created: "+sa.getLast());
                checkQueue();
            }
            LinkedList<WeakReference<Test>> wa = new LinkedList<WeakReference<Test>>();
            for(int i =0;i<size;i++){
                wa.add(new WeakReference<Test>(new Test("Weak "+i),rq));
                System.out.println("Just created: "+wa.getLast());
                checkQueue();
            }
            SoftReference<Test> s = new SoftReference<Test>(new Test("Soft"));
            WeakReference<Test> w = new WeakReference<Test>(new Test("Weak"));
            System.gc();
            LinkedList<PhantomReference<Test>> pa = new LinkedList<PhantomReference<Test>>();
            for(int i =0;i<size;i++){
                pa.add(new PhantomReference<Test>(new Test("Phantom "+i),rq));
                System.out.println("Just created: "+pa.getLast());
                checkQueue();
            }
            //System.gc();
        }
    }

    class Test{
        private String name;
        public Test(String name){this.name = name;}
        public String toString(){return name;}
        protected void finalize(){
            System.out.println("Finalizing "+name);
        }
    }

运行结果:

Just created: java.lang.ref.SoftReference@7852e922
    Just created: java.lang.ref.SoftReference@4e25154f
    Just created: java.lang.ref.SoftReference@70dea4e
    Just created: java.lang.ref.SoftReference@5c647e05
    Just created: java.lang.ref.SoftReference@33909752
    Just created: java.lang.ref.SoftReference@55f96302
    Just created: java.lang.ref.SoftReference@3d4eac69
    Just created: java.lang.ref.SoftReference@42a57993
    Just created: java.lang.ref.SoftReference@75b84c92
    Just created: java.lang.ref.SoftReference@6bc7c054
    Just created: java.lang.ref.WeakReference@232204a1
    Just created: java.lang.ref.WeakReference@4aa298b7
    Just created: java.lang.ref.WeakReference@7d4991ad
    Just created: java.lang.ref.WeakReference@28d93b30
    Just created: java.lang.ref.WeakReference@1b6d3586
    Just created: java.lang.ref.WeakReference@4554617c
    Just created: java.lang.ref.WeakReference@74a14482
    Just created: java.lang.ref.WeakReference@1540e19d
    Just created: java.lang.ref.WeakReference@677327b6
    Just created: java.lang.ref.WeakReference@14ae5a5
    Finalizing Weak 1
    Finalizing Weak
    Finalizing Weak 9
    Finalizing Weak 8
    Finalizing Weak 7
    Finalizing Weak 6
    Finalizing Weak 5
    Finalizing Weak 4
    Finalizing Weak 3
    Finalizing Weak 2
    Finalizing Weak 0
    Just created: java.lang.ref.PhantomReference@7f31245a
    In queue: null
    Just created: java.lang.ref.PhantomReference@6d6f6e28
    In queue: null
    Just created: java.lang.ref.PhantomReference@135fbaa4
    In queue: null
    Just created: java.lang.ref.PhantomReference@45ee12a7
    In queue: null
    Just created: java.lang.ref.PhantomReference@330bedb4
    In queue: null
    Just created: java.lang.ref.PhantomReference@2503dbd3
    In queue: null
    Just created: java.lang.ref.PhantomReference@4b67cf4d
    In queue: null
    Just created: java.lang.ref.PhantomReference@7ea987ac
    In queue: null
    Just created: java.lang.ref.PhantomReference@12a3a380
    In queue: null
    Just created: java.lang.ref.PhantomReference@29453f44
    In queue: null

**对于对象的回收这边做一个总结:**通过前面的实例可以知道即便是在可达性分析算法中不可达的对象并不会马上被GC,在GC之前它还会调用finalize()方法,并且该方法只能执行一次。如果在finalize()方法中这个对象还没有跟引用链上的任何一个对象进行关联那么它就会被回收掉。总而言之一个对象被回收需要经过两个阶段的标记一是可达性算法分析对象不可达;二是finalize()方法没有重新跟GC Roots关联。

方法区的回收

方法区的回收主要包括两个部分:废弃常量和无用的类。以常量池中字面量的回收为例子,假设一个字符串”abc”在常量池中,如果当前没有一个String对象的引用指向这个字符串,也没有其他地方引用它,那么在GC过后这个”abc”常量就会被清理出常量池。判断一个无用的类需要满足三个条件:

  1. 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的classLoader以及被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

满足上述三个条件后并不代表它们就会被回收,是否对类进行回收HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

Java虚拟机如何回收内存

针对内存回收Java虚拟机使用了多种的垃圾算法并通过垃圾收集器来实现。值得注意的是不同的虚拟机在垃圾收集算法以及垃圾收集器的使用上会有不同。

垃圾收集算法

**标记-清除算法:**最基础的收集算法,它分为两个步骤:一是标记(前文总结提到)需要回收的对象;二是清除。它的不足有两点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,可以看到清除后会产生大量不连续的内存碎片,当需要分配比较大的对象时无法找到足够的连续内存就不得不提前触发另一次的垃圾收集。

2019120001226\_6.png

**复制算法:**有不足就会有更新出现,针对标记-清除算法的效率问题复制算法出现了。它把可用的内存分成大小相等的两块,每次只使用其中的一块,当这块内存用完就把存活的对象复制到另一块,接着把用过的这块内存进行清理。这样就不用考虑内存碎片等复杂情况,但是代价相对较大,首先存活的对象移动到另一块内存需要时间,其次因为假设你有1G的内存,每次使用只能使用500M,这是一种空间的浪费。

现在商业的虚拟机都采用这种收集算法来回收新生代据IBM的专门研究表明了98%的对象都是”朝生夕死”的,所谓的朝生夕死说的就是一个对象的生命周期很短。例如在方法中创建一个对象,随着这个方法结束,这个对象的生命也就结束了。而哪些不会随着方法结束而结束呢?例如常量池中的对象等等。所以就没有必要按照1:1的比例来划分内存,而是将内存划分为Eden跟两块较小的Survivor内存空间,每次使用Eden和其中一块Survivor,当回收的时候就把Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清除掉Eden和刚刚使用的Survivor空间。Hotspot虚拟机默认按照8:1来分配,即Eden 8:Survivor0 1:Survivor1 1。(SurvivorRatio=Eden/Survivor0=Eden/Survivor1)这样每次就只有10%的内存被浪费。当Servivor内存不够用的时候就会通过老年代来进行分配担保。这个在后文详细介绍。

2019120001226\_7.png

**标记-整理算法:**一样是对前面复制算法不足而提出的一种算法,它的标记过程跟标记-清除一样,不同的是它不是马上进行清除,而是根据某种算法让所有存活的对象都向一段移动,然后清理掉端边界外的内存。虽然标记整理解决了复制算法中的空间浪费,但是并没有解决了对象在内存中移动的问题。

2019120001226\_8.png

分代收集算法:它并不是一种新的算法思想,而是根据对象存活周期的不同将内存划分为新生代和老年代。根据各个年代的特点来选择最适当的收集算法。

垃圾收集器

从下图可以看到针对不同的年代使用所需的垃圾收集器,每个垃圾收集器都有其对应的垃圾收集算法也可以说是垃圾收集算法的实现。从下图可以看到针对新生代以及老年代有各种不同的垃圾收集器,而两个垃圾收集器之间的连线表示它们可以搭配使用。值得注意的是并没有最好的垃圾收集器,只有最合适的垃圾收集器。并且所以的GC

2019120001226\_9.png

Serial收集器

  1. Serial收集器只会使用一个CPU或者一条收集线程去完成垃圾收集。
  2. 在进行垃圾收集的时候会暂停掉其他所有的工作线程(STW),直到它收集结束。
  3. 虚拟机在Client模式下默认的新生代垃圾收集器。
  4. 采用复制算法。

ParNew收集器

  1. ParNew是Serial的多线程版本在算法,STW等方面都是一样的,不同的是使用多条线程进行垃圾收集。
  2. 参数-XXParallelGCThread控制垃圾收集的线程数。

Parallel Scavnge收集器

  1. Parallel Scavnge与ParNew在算法,多线程等方面是一样的,不同的是它关注的点在与吞吐量(Throughput),所谓的吞吐量就是CUP用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,垃圾收集花掉1分钟,那吞吐量就是99%。
  2. 参数-XX:MaxGCPauseMillis,控制最大垃圾收集停顿时间(STW)。注意,并不是把参数的值设置低就可以把系统的GC速度变得更快,GC的停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小,导致GC更加频繁,停顿的时间缩短,但是吞吐量就下降了。
  3. 参数-GCTimeRatio,设置吞吐量大小。
  4. 参数-XX:+UserAdaptiveSizePolicy,设置参数后虚拟机会根据当前系统运行情况自动设置诸如-Xmn,-XX:survivorRatio等等参数以提高最合适的停顿时间或者最大吞吐量,这种调节方式叫做GC自适应策略。

Serial Old收集器

  1. Serial Old是Serial的老年代版本,同样是个单线程收集器。
  2. 使用标记-整理算法。

Parallel Old收集器

  1. Parallel Scavenge的老年代版本,同样是个多线程收集器
  2. 使用标记-整理算法。
  3. 在注重吞吐量以及CPU资源敏感的场合可以优先考虑Parallel Scavenge + Parallel Old组合。

CMS(Concurrent Mark Sweep)收集器

  1. CMS的作用是减少STW的时间,它执行的过程分为:初始标记,并发标记,重新标记,并发清除。STW会发生在前面两个步骤,其内存回收的过程与用户线程一起并发执行的。
  2. 缺点之一是对CPU资源非常敏感,会占用CPU资源,导致程序变慢,总吞吐量降低。
  3. 缺点之二是无法处理浮动垃圾(Floating Gabage),可能出现”Councurrent Mode Failure”失败而导致另一次Full GC(发生STW)的产生。
  4. 缺点之三它是基于标记-清除算法所以在GC结束后会有大量空间碎片产生,对于大对象的分配带来很大的麻烦。

G1收集器

上述介绍的几个垃圾收集器收集的范围都在新生代和老年代,而G1收集器中则没有一个严格的划分(如把内存块分为新生代跟老年代),它只是保留了新生代以及老年代的概念,然后将Java堆分为多个大小相等的独立区域(Region),每一块Region可能是新生代也可能是老年代。它的执行过程分为:初始标记,并发标记,最终标记,筛选回收。

G1收集器的优点:

  1. 并行与并发:G1能够充分利用CPU、多核环境下的硬件优势,原本部分收集器需要停顿Java线程来执行GC动作,G1仍然可以通过并发的方法让Java继续执行。
  2. 分代收集。
  3. 空间整合:G1使用了标记-整理算法不会产生内存空间碎片。
  4. 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

内存分配与回收策略

前文介绍了对象的分配,垃圾收集算法以及各种垃圾收集器。下面将通过实际案例来进行演示,并介绍几条最普遍的内存分配规则。正如文章开头说的本文是基于JDK1.8下运行在Server上的HotSpot虚拟机。下面将先从其默认使用的垃圾收集器组合Parallel Scavenge + Parallel Old开始,然后分别介绍其余的垃圾收集器组合看看它们在同样代码情况下是如何进行内存分配与回收的。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的内存空间进行分配时虚拟机会进行一次Minor GC。本次测试通过参数-Xms20MB,-Xmx20MB来指定Java堆的内存大小,而-Xmn10MB指定了新生代的内存大小,所以老年代的内存大小就是10MB。-verbose:gc以-XX:PrintGCDetail用来打印GC日志。以及默认情况下开启的-XX:SurvivorRatio=8(Eden跟一个Survivor的比例大小),这样Eden区就有8MB,两个Survivo区r各占1MB的空间。

/**Parallel Scavenge + Parallel Old
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails 
     * @author Chan
     *
     */
    public class OOMTest {
        private static final int _1MB = 1024*1024;

        public static void main(String[] args) {

            byte[] allocation1,allocation2,allocation3,allocation4;
            allocation1 = new byte[2 * _1MB];
            allocation2 = new byte[2 * _1MB];
            allocation3 = new byte[2 * _1MB];
            allocation4 = new byte[4 * _1MB];

        }
    }

运行结果:

Heap
     PSYoungGen      total 9216K, used 7291K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 89% used [0x00000000ff600000,0x00000000ffd1ef58,0x00000000ffe00000)
      from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
      to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
     ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到在Parallel Scavenge + Parallel Old上并没有发生GC,但是可以知道的是allocation1,allocation2,allocation3一共6MB的内存直接占据了Eden区89%的内存区域,而到了要分配allocation4内存的时候很明显Eden区的内存已经不足以分配,而Survivor空间只有1MB是无法容下6MB的内存(Eden区把存活对象移动到一个Survivor)。这个时候Parallel Scavenge + Parallel Old垃圾收集器组合直接把allocation4放到了老年代。

/**Parallel Serial + Serial Old
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails  -XX:+UseSerialGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC (Allocation Failure) [DefNew: 7127K->538K(9216K), 0.0035108 secs] 7127K->6682K(19456K), 0.0035567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4716K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
      from space 1024K,  52% used [0x00000000ff500000, 0x00000000ff586950, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到使用了Serial + Serial Old的组合执行了Minor GC。首先可以看到这次Minor GC的结果是新生代7127k变成了538k,但是Java堆的已使用量几乎没有减少,因为allocation1,2,3三个对象都还存活。接着在给aloocation4分配内存的时候虚拟机发现allocation1,allocation2,allocation3无法放到Survivor(看不懂麻烦查阅前文复制算法)空间,所以只能把6MB放入到老年代中。GC结束后allocation4也就被顺利的放到了Eden中。可以看到新生代中使用了4MB而老年代用了6MB。

日志中的GC(Allocation Failure)是JDK1.8版本虚拟中才有的日志信息它Minor GC执行时出现,如果你使用低版本的JVM来测试就不会有这个信息。

/**Parallel ParNew + Serial Old
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails  -XX:+UseParNewGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC (Allocation Failure) [ParNew: 7291K->561K(9216K), 0.0025604 secs] 7291K->6705K(19456K), 0.0026121 secs] [Times: user=0.03 sys=0.02, real=0.00 secs] 
    Heap
     par new generation   total 9216K, used 4740K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
      from space 1024K,  54% used [0x00000000ff500000, 0x00000000ff58c7b0, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
     Metaspace       used 2819K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 304K, capacity 386K, committed 512K, reserved 1048576K
    Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

可以看到ParNew + Serial Old组合结果跟Serial + Serial Old组合基本一致。但是注意日志的最后一行,它的意思就是说这个组合已经被弃用了,将会在未来的版本中删除。事实也正如它所说的,在JDK1.9的版本中已经把这个垃圾收集组合给删掉了,所以后续的测试内容不再包括这个垃圾收集器组合。

/**ParNew + CMS + Serial Old ,如果CMS出现Concurrent Mode Failure,则Serial Old收集器将作为后备收集器
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails  -XX:+UseConcMarkSweepGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC (Allocation Failure) [ParNew: 7128K->561K(9216K), 0.0028190 secs] 7128K->6708K(19456K), 0.0028773 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     par new generation   total 9216K, used 4740K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
      from space 1024K,  54% used [0x00000000ff500000, 0x00000000ff58c7b0, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     concurrent mark-sweep generation total 10240K, used 6146K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到使用ParNew + CMS + Serial Old的组合一样进行了GC,内存的分配也基本一致。

/** G1 
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails  -XX:+UseG1GC
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0012024 secs]
       [Parallel Time: 1.0 ms, GC Workers: 4]
          [GC Worker Start (ms): Min: 139.6, Avg: 139.7, Max: 139.9, Diff: 0.3]
          [Ext Root Scanning (ms): Min: 0.0, Avg: 0.3, Max: 0.9, Diff: 0.9, Sum: 1.3]
          [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
             [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
          [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
          [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
          [Object Copy (ms): Min: 0.0, Avg: 0.4, Max: 0.6, Diff: 0.6, Sum: 1.7]
          [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.2]
             [Termination Attempts: Min: 1, Avg: 1.8, Max: 4, Diff: 3, Sum: 7]
          [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
          [GC Worker Total (ms): Min: 0.6, Avg: 0.8, Max: 1.0, Diff: 0.4, Sum: 3.2]
          [GC Worker End (ms): Min: 140.5, Avg: 140.5, Max: 140.5, Diff: 0.0]
       [Code Root Fixup: 0.0 ms]
       [Code Root Purge: 0.0 ms]
       [Clear CT: 0.0 ms]
       [Other: 0.2 ms]
          [Choose CSet: 0.0 ms]
          [Ref Proc: 0.1 ms]
          [Ref Enq: 0.0 ms]
          [Redirty Cards: 0.0 ms]
          [Humongous Register: 0.0 ms]
          [Humongous Reclaim: 0.0 ms]
          [Free CSet: 0.0 ms]
       [Eden: 1024.0K(10.0M)->0.0B(9216.0K) Survivors: 0.0B->1024.0K Heap: 7168.0K(20.0M)->6768.1K(20.0M)]
     [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC concurrent-root-region-scan-start]
    [GC concurrent-root-region-scan-end, 0.0004848 secs]
    [GC concurrent-mark-start]
    [GC concurrent-mark-end, 0.0000304 secs]
    [GC remark [Finalize Marking, 0.0032743 secs] [GC ref-proc, 0.0000472 secs] [Unloading, 0.0006634 secs], 0.0041524 secs]
     [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC cleanup 10M->10M(20M), 0.0002631 secs]
     [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     garbage-first heap   total 20480K, used 10864K [0x00000000fec00000, 0x00000000fed000a0, 0x0000000100000000)
      region size 1024K, 2 young (2048K), 1 survivors (1024K)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

前文提到G1去掉了物理上新生代以及老年代的区别,只保留了新生代以及老年代的概念,用Region来表示。因此下面的几个内存分配就不再贴G1收集器。

Minor GC和Full GC

这边介绍下这两个GC的区别:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集器动作,因为Java对象大多数具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也必将快。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,除了MajorGC,经常会伴随着至少一次得Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢上10倍以上。

大对象直接进入老年代

大对象指的是需要大量连续内存空间的Java对象,最典型的就是很长的字符串以及数组(例如Byte[1024*1024])。因为大对象的出现容易导致内存提前触发垃圾收集以获取足够的连续空间来分配给它们,所以在程序中应该避免出现一些朝生夕死的大对象出现。虚拟机提供参数-XX:PretenureSizeThreshold参数,令对象直接在老年代进行分配,避免在Eden以及两个Survivor之间发生大量内存复制。注意书中提到PretenureSizeThreshold不能直接指定3M,需要写成3*1024*1024=3145728这种形式,但是测试过程中发现指定为3M是没有问题的。

/**Parallel Scavenge + Parallel Old
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails   
     * -XX:PretenureSizeThreshold=3M 
     * @author Chan
     * 
     */
    public class OOMTest {
        private static final int _1MB = 1024*1024;

        public static void main(String[] args) {

            byte[] allocation;
            allocation = new byte[4 * _1MB];

        }
    }

运行结果:

Heap
     PSYoungGen      total 9216K, used 5243K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 64% used [0x00000000ff600000,0x00000000ffb1ef38,0x00000000ffe00000)
      from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
      to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
     ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到Parallel Scavenge + Parallel Old并没有因为这个参数的设置,因为它不支持这个参数所以并没有直接把4MB的对象(大于设置的3MB)直接丢到老年代。

/** Serial + Serial Old 
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails   
     * -XX:PretenureSizeThreshold=3M  -XX:+UseSerialGC
     * @author Chan
     * 执行代码相同
     */

运行结果:

Heap
     def new generation   total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed1ef28, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到Serial + Serial Old组合实现了直接把大对象放入老年代的功能。

/**ParNew + CMS + Serial Old 
     * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails   
     * -XX:PretenureSizeThreshold=3M  -XX:+UseConcMarkSweepGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

Heap
     par new generation   total 9216K, used 1148K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  14% used [0x00000000fec00000, 0x00000000fed1f128, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     concurrent mark-sweep generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

ParNew + CMS + Serial Old组合也可以实现。

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

通过给每个对象定义一个年龄(Age)计数器可以让虚拟机识别哪些对象放在新生代,哪些对象放到老年代中。如果对象在Eden出生并经历过一次Minor GC后仍然存活,并且能够被Survivor容纳的话就会被移动到Survivor空间中,此时年龄增加1。当它的年龄到达一定程度(默认15)的时候就会被晋升到老年代中.通过参数-XX:MaxTenuringThreshold参数可以来手动控制这个年龄值。下面将对每种垃圾收集器(除去G1跟被淘汰的ParNew+Serial Old组合)在相同代码情况下MaxTenuringThreshold=1以及MaxTenuringThreshold=5来进行对比。

/**Parallel Scavenge + Parallel Old
     * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms30M -Xmx30M -Xmn15M 
     * -XX:MaxTenuringThreshold=1  -XX:SurvivorRatio=3   
     * @author Chan
     * 
     */
    public class OOMTest {
        private static final int _1MB = 1024*1024;

        public static void main(String[] args) {

            byte[] allocation1,allocation2,allocation3;
            allocation1 = new byte[ _1MB/4];
            allocation2 = new byte[4 * _1MB];
            allocation3 = new byte[4 * _1MB];//发生一次Minor GC
            allocation3 = null;
            allocation3 = new byte[6 * _1MB];//发生一次Minor GC     
        }
    }

运行结果:

[GC (Allocation Failure) [PSYoungGen: 5458K->888K(12288K)] 5458K->4992K(27648K), 0.0022221 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     PSYoungGen      total 12288K, used 5076K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
      eden space 9216K, 45% used [0x00000000ff100000,0x00000000ff517228,0x00000000ffa00000)
      from space 3072K, 28% used [0x00000000ffa00000,0x00000000ffade040,0x00000000ffd00000)
      to   space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
     ParOldGen       total 15360K, used 10248K [0x00000000fe200000, 0x00000000ff100000, 0x00000000ff100000)
      object space 15360K, 66% used [0x00000000fe200000,0x00000000fec02020,0x00000000ff100000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到在Parallel Scavenge + Parallel Old并没有因为我们设置了最大年龄数为1就回收掉Servivor中的内存。所以这个组合对这个参数无效。

/**Serial + Serial Old
     * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms30M -Xmx30M -Xmn15M 
     * -XX:MaxTenuringThreshold=1  -XX:SurvivorRatio=3  -XX:+UseSerialGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC (Allocation Failure) [DefNew: 5458K->794K(12288K), 0.0028194 secs] 5458K->4890K(27648K), 0.0028634 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [DefNew: 4890K->0K(12288K), 0.0009721 secs] 8986K->4888K(27648K), 0.0009967 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 12288K, used 6236K [0x00000000fe200000, 0x00000000ff100000, 0x00000000ff100000)
      eden space 9216K,  67% used [0x00000000fe200000, 0x00000000fe817228, 0x00000000feb00000)
      from space 3072K,   0% used [0x00000000feb00000, 0x00000000feb00000, 0x00000000fee00000)
      to   space 3072K,   0% used [0x00000000fee00000, 0x00000000fee00000, 0x00000000ff100000)
     tenured generation   total 15360K, used 4888K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
       the space 15360K,  31% used [0x00000000ff100000, 0x00000000ff5c6158, 0x00000000ff5c6200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到Serial + Serial Old组合对这个参数的设置起了作用,在第一次Minor GC的是 allocation1被放到了Survivor中去此时年龄为1。而当到了第二次Minor GC的时候allocation1就被回收到了老年代中去了。接着来看看MaxTenuringThreshold参数为5的情况。

运行结果:

[GC (Allocation Failure) [DefNew: 5458K->794K(12288K), 0.0026437 secs] 5458K->4890K(27648K), 0.0026918 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [DefNew: 4890K->792K(12288K), 0.0008974 secs] 8986K->4888K(27648K), 0.0009175 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 12288K, used 7028K [0x00000000fe200000, 0x00000000ff100000, 0x00000000ff100000)
      eden space 9216K,  67% used [0x00000000fe200000, 0x00000000fe817228, 0x00000000feb00000)
      from space 3072K,  25% used [0x00000000feb00000, 0x00000000febc6148, 0x00000000fee00000)
      to   space 3072K,   0% used [0x00000000fee00000, 0x00000000fee00000, 0x00000000ff100000)
     tenured generation   total 15360K, used 4096K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
       the space 15360K,  26% used [0x00000000ff100000, 0x00000000ff500010, 0x00000000ff500200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以明显的看到Survivor中的allocation1仍然存活。说明它并没有被回收到老年代中。

/**ParNew + CMS + Serial Old
     * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms30M -Xmx30M -Xmn15M 
     * -XX:MaxTenuringThreshold=1  -XX:SurvivorRatio=3  -XX:+-XX:+UseConcMarkSweepGC 
     * @author Chan
     * 执行代码相同
     */

运行结果:

[GC (Allocation Failure) [ParNew: 5458K->822K(12288K), 0.0019947 secs] 5458K->4921K(27648K), 0.0020633 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [ParNew: 4918K->0K(12288K), 0.0022521 secs] 9017K->4900K(27648K), 0.0022866 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 
    Heap
     par new generation   total 12288K, used 6236K [0x00000000fe200000, 0x00000000ff100000, 0x00000000ff100000)
      eden space 9216K,  67% used [0x00000000fe200000, 0x00000000fe817228, 0x00000000feb00000)
      from space 3072K,   0% used [0x00000000feb00000, 0x00000000feb00000, 0x00000000fee00000)
      to   space 3072K,   0% used [0x00000000fee00000, 0x00000000fee00000, 0x00000000ff100000)
     concurrent mark-sweep generation total 15360K, used 4900K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

可以看到ParNew + CMS + Serial Old组合在MaxTenuringThreshold=1的情况下一样直接把allocation1的值放到了老年代,继续看看MaxTenuringThreshold=5的情况。

运行结果:

[GC (Allocation Failure) [ParNew: 5458K->817K(12288K), 0.0018453 secs] 5458K->4916K(27648K), 0.0019036 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    [GC (Allocation Failure) [ParNew: 4913K->953K(12288K), 0.0014081 secs] 9012K->5051K(27648K), 0.0014430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     par new generation   total 12288K, used 7190K [0x00000000fe200000, 0x00000000ff100000, 0x00000000ff100000)
      eden space 9216K,  67% used [0x00000000fe200000, 0x00000000fe817228, 0x00000000feb00000)
      from space 3072K,  31% used [0x00000000feb00000, 0x00000000febee600, 0x00000000fee00000)
      to   space 3072K,   0% used [0x00000000fee00000, 0x00000000fee00000, 0x00000000ff100000)
     concurrent mark-sweep generation total 15360K, used 4098K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

同样的,当MaxTenuringThreshold=5的时候,Survivor中的allocation1并没有被回收掉,说明参数生效了。

动态对象年龄判定

上文介绍了同构参数MaxTenuringThreshold来控制对象在新生代中的存活时间,但是并不是所有的情况都是有效的。当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。下面直接拿上文中的Serial + Serial Old作为测试对象。

/**Serial + Serial Old
     * VM Args : -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -
     * XX:MaxTenuringThreshold=5   -XX:+UseSerialGC 
     * @author Chan
     * 执行代码相同
     */
    public class OOMTest {
        private static final int _1MB = 1024*1024;

        public static void main(String[] args) {

            byte[] allocation1,allocation2,allocation3;
            allocation1 = new byte[ _1MB/4];
            allocation2 = new byte[4 * _1MB];
            allocation3 = new byte[4 * _1MB];//发生一次Minor GC
    /*      allocation3 = null;
            allocation3 = new byte[4 * _1MB];//发生一次Minor GC     
    */  }
    }

运行结果:

[GC (Allocation Failure) [DefNew: 5335K->794K(9216K), 0.0032016 secs] 5335K->4890K(19456K), 0.0032484 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4972K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
      from space 1024K,  77% used [0x00000000ff500000, 0x00000000ff5c6960, 0x00000000ff600000)
      to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
     tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

这本次测试中,先把最后一次分配allocation3注释掉,先看第一次Minor GC的结果。很明显可以allocation1被分配到了Survivor中去了。接着来看看当我们把注释去掉后的运行结果。

运行结果:

[GC (Allocation Failure) [DefNew: 5335K->794K(9216K), 0.0028416 secs] 5335K->4890K(19456K), 0.0028880 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
    [GC (Allocation Failure) [DefNew: 4890K->0K(9216K), 0.0010953 secs] 8986K->4888K(19456K), 0.0011211 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    Heap
     def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
      from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
     tenured generation   total 10240K, used 4888K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
       the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffac6158, 0x00000000ffac6200, 0x0000000100000000)
     Metaspace       used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
      class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

不难看出此时Survivor中的内存已经被清空了,allocation1的数据也被放到了老年代中去了。正如小节开头介绍的,此时Survivor中allocation1的内存大小已经大于Survivor空间的一半,那么Minor GC的时候它没有等到设置的年龄5就已经被放到了老年代中去了。所以通过MaxTenuringThreshold来控制新生代中对象存活要注意这个问题。

空间分配担保

书中还介绍到了这个方法,并且通过参数HandlePromotionFailure参数来进行控制,但是这个参数在JDK1.8中已经删去。此外书中也提到了JDK6以后主要老年代的连续空间大于新生代对象总大小或者历次晋升到老年代对象的平均大小就会进行Minor GC,否则进行Full GC。简单的说,所谓的空间分配担保就是在发生Minor GC的时候会判断下老年代是否有足够的内存,以防Eden和Survivor内存不够无法分配的时候可以把对象放到老年代中。如果足够就进行Minor GC,否则的话就进行Full GC来腾出更多的内存以供对象分配。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机之基础篇

相关推荐