JVM理解2

1、垃圾回收(GC)

GC(Garbage Collection,即垃圾回收)的基本原理:将内存中不再被使用的对象进行回收。顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以对使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

GC 又分为年轻代 GC 和老年代 GC,年轻代 GC 只针对年轻代内存空间进行垃圾回收,耗时比较短;老年代 GC 针对整个堆与方法区的空间进行垃圾回收,耗时较年轻代 GC 长。

垃圾回收主要是发生在堆内存里面,在1.8以后FULLGC也会发生在meta space中。

 

1.1、Minor GC、Major GC、Full GC

  • Minor GC:对年轻代(包括 Eden 和 Survivor 区域)进行GC被称为 Minor GC。因为新生代的特点,MinorGC非常频繁,且回收速度比较快,每次回收的量也很大。
  • Major GC:对老年代的对象的收集称为 Major GC。Major GC的速度一般会比Minor GC慢10倍以上。
  • Full GC:Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC,Full GC是对整个堆来说的。Full GC不等于Major GC,也不等于Minor GC+Major GC。出现Full GC的时候经常伴随至少一次的 Minor GC。

GC 中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java 在对对象的生命周期特征进行分析后,按照新生代、老年代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。

 

1.2、STOP THE WORLD(STW)

Stop一the一World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。举例:可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。

minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。Full GC的 STW 时间更长。

停顿的原因:

  1. 分析工作必须在一个能确保一致性的快照中进行

  2. 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上

  3. 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证

 

特点描述:

  • 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。
  • STW事件和采用哪款GC无关,所有的GC都有这个事件。
  • 哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
  • STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
  • 开发中不要采用System.gc();会导致Stop一the一world的发生。

 

1.3、Full GC原因和解决方法

下面介绍几种可能会导致 JVM 进行 Full GC的情况及解决办法:

  • System.gc()方法的调用

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc()。

 

  • 老年代空间不足

老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误【java.lang.OutOfMemoryError: Java heap space】

为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

 

  • 方法区空间不足

JVM规范中运行时数据区域中的方法区,又被称为永生代或者永生区(并不是所有的jvm都有永生代的概念),方法区中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,方法区可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息【java.lang.OutOfMemoryError: PermGen space】
为避免方法区占满造成Full GC现象,可采用的方法为增大方法区空间或转为使用CMS GC。

 

  • CMS GC时出现promotion failed和concurrent mode failure

对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC);
对应解决办法为:增大survivor space、老年代空间或调低触发并发GC的比率。

但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

 

  • 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间

这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。

  • 堆中分配很大的对象

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但停顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

 

2、垃圾定位

在进行垃圾回收之前,需要首先进行垃圾定位,即判断哪些对象可以进行回收。当对象没有被任何引用指向时就可被垃圾回收。

 

2.1、引用计数法(目前JVM未使用)

引用计数法也就是记录当前对象的引用次数,当引用次数为0时则进行回收。给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加一;相反的,当引用失效的时候,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。也就是说,当计时器的数值为0的时候,这个对象就可以被回收了。

引用计数是垃圾收集器中的早期策略,但是引用计数法存在一个巨大的问题,就是循环依赖,例如:

针对上图这种情况,对象ABC之间相互引用,他们的counter永远不可能为0,造成他们永远无法被回收,因此目前主流的 JVM 里都没有选用引用计数算法来管理内存。

示例:

<- 背景 ->
对象objA 和 objB 都有字段 name,两个对象相互进行引用
objA.name = objB;
objB.name = objA;

<- 问题 ->
当这两个对象objA、objB再也没有其他任何引用时,实际上他们应该要被垃圾收集器进行回收才对
但因为他们相互引用,所以导致计数器不为0,这导致引用计数算法无法通知垃圾收集器回收该两个对象

 

2.2、可达性分析算法(JVM采用的算法)

这个算法的实质在于将一系列GC Roots作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该合集引用到的对象,并将其加入到该和集中,这个过程称之为标记(mark)。 最终,未被探索到的对象便是死亡的,是可以回收的。

对象可达指的就是:双方存在直接或间接的引用关系。 根可达或 GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。

public class MyObject {
    private String objectName;//对象名
    private MyObject refrence;//依赖对象

    public MyObject(String objectName) {
        this.objectName = objectName;
    }

    public MyObject(String objectName, MyObject refrence) {
        this.objectName = objectName;
        this.refrence = refrence;
    }

    public static void main(String[] args) {
        MyObject a = new MyObject("a");
        MyObject b = new MyObject("b");
        MyObject c = new MyObject("c");
        a.refrence = b;
        b.refrence = c;

        new MyObject("d", new MyObject("e"));
    }
}

创建了5个对象,他们之间的引用关系如下图,假设a是GC Roots的话,那么b、c就是可达的,d、e是不可达的。

目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。

 

2.2.1、GC Roots

垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。

GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象)。只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且是根对象。

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

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

 

3、JVM的五种引用

在 JDK1.2 版之后,Java 对引用的概念进行了扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)、终接器引用,这 5 种引用强度依次逐渐减弱。实际上,这些都是引用,即可以理解为变量。
除强引用外,其他 3 种引用均可以在 java.lang.ref 包中找到它们的身影,而终接器引用无需手动编码,其内部配合引用队列使用。如下图,显示了这 3 种引用类型对应的类,开发人员可以在应用程序中直接使用它们。

Reference 子类中只有终结器引用是包内可见的,其他 3 种引用类型均为 public,可以在应用程序中直接使用

  • 强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。
  • 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
  • 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

 

public static void main(String[] args) {
    //强引用
    String zzl="zzz";
    int t=1;

    //软引用
    SoftReference<Object> softReference=new SoftReference<>("aaa");
    System.out.println(softReference.get());  //输出aaa

    //弱引用
    WeakReference<Object> weakReference=new WeakReference<>(new String("zzz"));
    System.out.println(weakReference.get());  //输出zzz
    System.gc();
    System.out.println(weakReference.get());  //输出null

    //虚引用
    ReferenceQueue<Object> queue=new ReferenceQueue<>();
    PhantomReference<Object> phantomReference=new PhantomReference<>(new String("zzl"),queue);
    System.out.println(phantomReference.get());  //输出null
    System.gc();
    System.out.println(phantomReference.get());  //输出null
}

 

 

3.1、强引用(Strong Reference,不回收)

在 Java 程序中,最常见的引用类型是强引用(普通系统 99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。当在 Java 语言中使用 new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。

如果强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成 Java 内存泄漏的主要原因之一。

强引用具备以下特点:

  1. 强引用可以直接访问目标对象。
  2. 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象。
  3. 强引用可能导致内存泄漏。

 

3.2、软引用(Soft Reference,内存不足即回收)

软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把软引用所指向的这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。即仅有软引用引用该对象时,在垃圾回收后,如果此时内存仍不足则会再次出发垃圾回收,回收软引用对象。

软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

可以配合引用队列来释放软引用自身。

 

实例代码:

package cn.itcast.jvm.t2;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc  通过-XX:+PrintGCDetails -verbose:gc参数配置可在控制台输出查看垃圾回收的过程
 */
public class Demo2_3 {

    private static final int _4MB = 4 * 1024 * 1024;



    public static void main(String[] args) throws IOException {
        //强引用
        /*List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }

        System.in.read();*/

        //使用软引用
        soft();
    }

    public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println("list的大小:" + list.size());
        }
        System.out.println("循环结束,list的大小:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

当使用强引用时,会发生内存溢出:

当使用弱引用时,输出如下:

 

3.2.1、软引用队列

在软引用所指向的对象被回收后,默认情况下,该软引用是仍然存在的,此时我们可以配合使用引用队列来将软引用自身释放掉。

package cn.itcast.jvm.t2;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示软引用, 配合引用队列
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        
        System.out.println("remove结束,list的大小:" + list.size());
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}

输出如下:

可以看到,remove 后 list 集合的大小只有 1,即那些无用的软引用都被释放掉了。

 

3.3、弱引用(Weak Reference,发现即回收)

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。

但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

 

代码演示:

package cn.itcast.jvm.t2;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            System.out.print("第" + (i+1) + "次循环:");
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();
        }
        System.out.println("循环结束:" + list.size());
    }
}

输出如下:

 

3.4、虚引用(Phantom Reference,对象回收跟踪)

也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的 get()方法取得对象时,总是 null。

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

 

3.5、终结器引用

它用于实现对象的 finalize() 方法,也可以称为终结器引用。无需手动编码,其内部配合引用队列使用。

在 GC 时,终结器引用入队。由 Finalizer 线程通过终结器引用找到被引用对象调用它的 finalize()方法,第二次 GC 时才回收被引用的对象。

 

4、垃圾回收算法

JVM 垃圾回收算法:

  1. “标记–清除”算法:首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。
  2. “标记–整理”算法
  3. 复制算法:将内存划分为等大的两块,每次只使用其中的一块。
  4. 分代收集算法。

 

4.1、标记 - 清除算法

执行步骤:

  • 先标记:遍历内存区域,对需要回收的对象打上标记。
  • 再清除:再次遍历内存,对已经标记过的内存进行回收。

图解:

优点:速度较快,只标记然后清除,不做内存整理。

缺点:

  1. 会造成内容碎片。清除后由于回收的内存空间地址不连续,所以容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC,可能导致发生内存溢出。
  2. 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

适用场景:

  1. 存活对象较多的情况下比较高效
  2. 适用于年老代(即旧生代)

 

4.2、标记 - 整理算法

执行步骤:

  • 标记:对需要回收的进行标记
  • 整理:让存活的对象,向内存的一端移动,然后清理掉没有用的内存

 

优点:没有内存碎片。因为经过整理后,内存地址不会不连续。

缺点:速度慢。存活的内存需向同一端移动,内存地址发生改变,对象的指向地址也需要随之发生改变,工作量比较大。

标记-整理算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不像标记-复制算法一样需要两块相同的内存空间,因此,其性价比比较高。

 

4.3、标记 - 复制算法

将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。

优点:不会有内存碎片

缺点:需要占用双倍内存空间

 

4.4、分代收集算法(JVM采用的垃圾回收算法)

当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。

一般情况下将堆区划分为新生代(Young Generation)和老年代(Tenured Generation)

  • 新生代:存放生命周期较短的对象的区域
  • 老年代:存放生命周期较长的对象的区域

在堆区之外还有一个就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法。新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

 

  • 新生代复制算法

  1. 所有新生成的对象首先都是放在年轻代的,对象首先分配在伊甸园(eden)区域。
  2. 新生代内存按照 8:1:1 的比例分为一个eden(伊甸园)区和两个 survivor(survivor0,survivor1) 区。一个Eden区,两个 Survivor 区(一般而言)。
  3. 新生代伊甸园空间不足时,会触发 minor gc。
  4. 在第一次发生 minor gc时,伊甸园区中的对象将会被复制到幸存区 to,然后to区和from区指向交换,即保证to区都是空的。
  5. 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor 区“To”是空的。在进行GC时,Eden区中所有存活的对象都会被复制到“To”,在“From”区中仍存活的对象会根据他们的年龄值来决定去向,年龄达到一定值(年龄阈值可以通过-XX:MaxTenuringThreshold来设置)的对象会被直接移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
  6. GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到老年代中。
  7. 当老年代空间也不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长。

也就是伊甸园区空间不足时,会发生垃圾回收(minor gc);当老年代空间也不足时,会先触发 minor gc,如果还不足,则会触发 full gc。

(当对象达到晋升阈值时,对象会被移动至老年代;或者当新生代的 to 区被填满时,即新生代内存不足时,未达到晋升阈值的对象也会将被移动至老年代中;)

 

5、JVM参数

5.1、JVM参数

jvm的参数有很多,根据 jvm 参数开头可以区分参数类型,共三类:“-”、“-X”、“-XX”,

1)标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容;例子:-verbose:class,-verbose:gc,-verbose:jni……

2)非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k……

3)非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps……

 

-XX 参数被称为不稳定参数,之所以这么叫是因为此类参数的设置很容易引起JVM 性能上的差异,使JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM 的性能及稳定性。

    1.布尔类型参数值
        -XX:+<option> '+'表示启用该选项
        -XX:-<option> '-'表示关闭该选项
    2.数字类型参数值:
        -XX:<option>=<number> 给选项设置一个数字类型值,可跟随单位,例如:'m'或'M'表示兆字节;'k'或'K'千字节;'g'或'G'千兆字节。32K与32768是相同大小的。
    3.字符串类型参数值:
        -XX:<option>=<string> 给选项设置一个字符串类型值,通常用于指定一个文件、路径或一系列命令列表。 例如:-XX:HeapDumpPath=./dump.core

 

5.2、常用参数配置

堆设置:

  • -Xms:初始堆空间大小。示例 -Xmx64m
  • -Xmx:最大堆空间大小。示例 -Xmx64m
  • -Xmn:新生代大小。示例 -Xmx32m
  • -XX:NewRatio:设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3
  • -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如值为3,表示Eden : Survivor=3 : 2,一个Survivor区占整个新生代的1/5
  • -XX:MaxTenuringThreshold:晋升阈值,设置转入老年代的存活次数。如果是0,则直接跳过新生代进入老年代
  • -XX:PermSize-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前)
  • -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后)

 

垃圾回收统计信息:

  • -XX:+PrintGC:
  • -verbose:gc:输出虚拟机GC详情
  • -XX:+PrintGCDetails:打印GC详情
  • -XX:+PrintGCDetails -verbose:gc:GC详情
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename
  • -XX:+ScavengeBeforeFullGC:FullGC 前进行 MinorGC

 

  • -Xss:设置每个线程可使用的内存大小,即栈的大小。示例 -Xss512k
  • -XX:+HeapDumpOnOutOfMemoryError:当抛出oom时进行heapdump
  • -XX:+HeapDumpPath:指定 heapdump 文件的路径和目录

 

最常见的几个参数如下:

  •  -Xms20m :设置jvm初始化堆大小为20m,一般与-Xmx相同避免垃圾回收完成后jvm重新分。
  •  -Xmx20m:设置jvm最大可用内存大小为20m。
  •  -Xmn10m:设置新生代大小为20m。
  •  -Xss128k:设置每个线程的栈大小为128k。

根据字母拆分理解意思:

 

5.3、查看JVM参数

可使用下面命令查看当前所有 Java 进程的 jvm 参数。

jps -lv
  • -l :输出完全的包名,应用主类名,jar的完全路径名
  • -v:输出jvm参数

 

6、GC分析

示例:

package cn.itcast.jvm.t2;

import java.util.ArrayList;

/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
    }
}

以  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC 配置运行上面程序,打印出 GC和内存等信息,输出如下:

上图中可以看到,默认情况下,就算什么代码都没有,Java 程序启动后就会使用一定的内存,此时全部在伊甸园区。可以看到伊甸园区使用了 22%,其他区域暂未被使用到。

 

演示伊甸园区垃圾回收,修改代码如下:

package cn.itcast.jvm.t2;

import java.util.ArrayList;

/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}

输出如下:

 

如果想要存放一个大对象,而新生代区放不下,则会直接将该大对象放在老年代。

当主线程发生OOM时,程序会异常终止。但是当主线程内的其他线程OOM时,此时发生OOM的线程会被杀死,内存释放,但其他线程不受影响,即主线程仍会继续正常运行。

代码示例:

package cn.itcast.jvm.t2;

import java.util.ArrayList;

/**
 *  演示内存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("thread start");
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(10000L);
        System.out.println("sleep end....");
    }
}

输出如下:

上面代码,当线程中发生 OOM 时,主线程并没有停止。当 sleep 休眠结束后,主线程才会终止。

 

7、垃圾回收器

如果说垃圾回收算法是内存回收的方法论,那么垃圾回收器就是内存回收的实践者。

垃圾回收器按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

  • 串行垃圾回收:指的是在同一个时间段内只允许有一个回收器线程用于执行垃圾回收操作。
  • 并行垃圾回收:和串行垃圾回收相反,并行运算可以拥有多个垃圾回收线程同时执行垃圾回收。

不管是串行还是并行垃圾回收器,在进行垃圾回收时,用户线程都是停止的,即使用“Stop The World”的机制,在回收的时候,需要暂停所有的线程。此时只是垃圾回收线程在工作。

 

7.1、串行垃圾回收器

串行垃圾回收:指的是在同一个时间段内只允许有一个回收器线程用于执行垃圾回收操作。适合堆内存较小,单核CPU的情况。

 

8、GC调优

JVM调优是一个不断调整的过程,不能指望着一蹴而就。要不断调整相关参数,观察结果进行对比分析。还有就是,不同的垃圾收集器的JVM参数是不一样的,所以具体的GC调优要根据不同的收集器做调整。

最快的 GC 就是不发生 GC。

当发生 full gc时,查看Full GC前后的内存占用可考虑以下问题:

  • 数据量是不是太多?
  • 数据表示是否太臃肿?
  • 是否存在内存泄漏?

(实际上可能多数的 Java 应用不需要在服务器上进行 GC 优化;多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题;在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合);减少创建对象的数量;减少使用全局变量和大对象;GC 优化是到最后不得已才采用的手段;在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。)

 

8.1、新生代调优

新生代特点:

① new操作内存分配廉价(TLAB thread-local allocation buffer)
② 新生代垃圾回收采用复制算法,回收代价低
③ 大部分对象用过即可回收
④ Minor GC的时间远低于Full GC

 

新生代内存并非是越大越好,总内存是一定的,新生代大了,老年代就会小,触发Full GC 的概率越大,full gc 的速度比 minor gc慢多了。

新生代调优方法

  • ① 内存大小调整。命令:-Xmn size,Oracle建议新生代大小占整个堆空间25%-50%。
  • ② 新生代调优参考
  1. 新生代内存建议能容纳【并发量 * (请求 + 响应) 】的数据。
  2. 幸存区的大小应能保留【当前活跃对象 + 需要晋升对象】的数据。
  3. 晋升阈值配置得当,让长时间存货对象尽快晋升。

-XX:+PrintTenuringDistribution:通过配置该 jvm 参数来打印幸存区年龄空间数据,以此预估一个合理阈值。
-XX:MaxTenuringThreshold=threshold:设置晋升阈值

 

8.2、老年代调优

以  CMS垃圾回收器 为例:

  1. CMS 的老年代内存越大越好。避免浮动垃圾过多,进而引起并发清除失败,退化为SerialOld。
  2. 先尝试不做调优,如果没有 Full GC 那么已经内存可以满足。如果Full GC频繁,则先尝试调优新生代。
  3. 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

-XX:CMSInitiatingOccupancyFraction=percent (设置当老年代的空间占用占老年代总内存的多少百分比时,就发生Full GC)

 

posted @ 2022-08-28 01:12  wenxuehai  阅读(144)  评论(0编辑  收藏  举报
//右下角添加目录