【Java虚拟机】万字长文,搞定Java虚拟机方方面面!2

简介: 【Java虚拟机】万字长文,搞定Java虚拟机方方面面!

2.对象创建解析

2.1.对象创建的流程

1、对象创建的流程

  • 虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用。
  • 判断这个类是否已被加载、解析和初始化。
  • 为这个新生对象在Java堆中分配内存空间,其中Java堆分配内存空间的方式主要有以下两种
  • 指针碰撞
  • 分配内存空间包括开辟一块内存和移动指针两个步骤。
  • 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • 空闲列表
  • 分配内存空间包括开辟一块内存和修改空闲列表两个步骤。
  • 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
  • 将分配到的内存空间都初始化为零值
  • 设置对象头相关数据
  • GC分代年龄
  • 对象的哈希码hashCode
  • 元数据信息
  • 执行对象方法
  • 图解
  • 6b0ad43900be4cd39bd4485f9daee2f0.jpg

2、指针碰撞和空闲列表图解

299861f5be2443b0a7cf83187cc7ffe7.jpg


2.2.对象的结构

e083bd8fb23443b8baaab562cec7f8c3.jpg

2.3.对象的访问方式

当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作栈上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用是第二种):

1、使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。

2、直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。



f7bb1a14bb6c49eda8a32bb0aad50faa.jpg

对比

  • 垃圾回收分析:句柄方式访问对象当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址,直接指针访问对象垃圾回收时需要修改reference中存储的地址。
  • 访问效率分析,直接指针访问对象优于句柄方式访问对象,因为直接指针访问对象只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。

3.JVM垃圾回收

3.1.垃圾回收概述

1、什么是垃圾(Garbage)

  • 垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
  • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占用的内存空间会一直保留到应用程序结束,有可能影响其他对象的使用或者导致内存溢出

2、为什么需要GC

  • 对于高级语言来说,不进行垃圾回收,内存迟早会被消耗完。
  • 释放没用的独享,垃圾回收也可以清除内存里的记录碎片,碎片整理将所有占用的堆内存放到堆的一段,以便于JVM将整理出的内存分配给的新的对象
  • 随着应付业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序的正常进行,经常造成STW的GC又跟不上实际的需求,所以才会不短地尝试对GC进行优化。
  • 3、Java垃圾回收机制
  • 自动内存管理,降低内存泄露和内存溢出的风险
  • 自动内存管理,减轻了Java程序员的内存管理负担,可以更专注于业务的开发

3.2.引用计数法

1、引用计数法概念

  • 引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
  • 首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。

2、什么是引用计数算法

  • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;
  • 当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。
  • 3、那为什么主流的Java虚拟机里面都没有选用这种算法呢?

其中最主要的原因是它很难解决对象之间相互循环引用的问题。


648766d082d64811a9a4f66d4b92eb75.jpg

3.3.可达性分析算法

1、可达性分析算法简介

可达性分析算法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,

不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为回收对象,要至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。


a146a6f61705418db6015e92132bc081.jpg

2、可作为GC Roots的对象

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。(引用栈帧中的本地变量表的所有对象)

(2)方法区中静态属性引用的对象(引用方法区该静态属性的所有对象)

(3)方法区中常量引用的对象(引用方法区中常量的所有对象)

(4)本地方法栈中(Native方法)引用的对象(引用Native方法的所有对象)


cea9873d13354d34bd371b164315f127.jpg

3.4.Java五种引用类型

1、什么是引用

  • 每种编程语言都有自己操作内存中元素的方式,C、C++采用指针,而在Java中则是通过引用。
  • 在Java中一切都被视为对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
  • 通过这个引用指向某个对象,之后便可以通过这个引用来实现操作对象了。
String str = new String("abc");
System.out.println(str.toString());
  • jdk1.2之前,Java中的定义很传统,如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。


c8a78c01c3d04aadb0237383799653e9.jpg


067a63c33041484bb5c1a35cce4fe0f1.jpg

2、强引用

  • Java中默认声明的就是强引用,比如:
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动设置成null

强引用只能够通过GC Root的引用链找到就不会被回收,也就是说强引用只有当GC Roots全部断开时才会被回收。

案例:设置JVM参数,起始堆内存为2m:-Xms2m 最大堆内存为3m:-Xmx3m

public class Demo13 {
    public static void main(String[] args) {
        testStrongReference();
    }
    private static void testStrongReference(){
        //当new byte为1M 的时候,程序运行正常
        byte [] buff = new byte[1024 * 1024 *1];
    }
}

41411ba8431f45dbb48f75d6d460c569.jpg

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OOM(OutOfMemoryError),不回去回收。如果想中断强引用与对象之间的关系,可以显示的将强引用赋值成null,这样,JVM就可以在适当的时刻回收对象了。

3、软引用

  • 软引用是用来描述一些非必需但是仍有用的对象。**在内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出OOM的异常。**这种特性一般被用来实现缓存技术。
  • 在JDK1.2之后,用java.lang.ref.SoftReference类来表示软引用。

案例:创建多个对象,软引用指向,不会出现内存溢出,当内存不足时,会自动的回收掉

public class Demo13 {
    private static List<Object> list = new ArrayList<>();
    public static void main(String[] args) {
        testStrongReference();
    }
    private static void testStrongReference(){
        for (int i = 0; i < 10; i++) {
            byte [] buff = new byte[1024 * 1024]; 
            //指向软引用
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }
        System.gc(); //显示调用gc
        for (int i = 0; i < list.size(); i++) {
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }
    }
}

537c87531ac843108b6fd9ad077d4eb7.jpg

案例:软引用配合引用队列使用,当内存不足时,会自动的回收掉

public class Demo14 {
    private static final int _1GB = 1024*1024;
    public static void main(String[] args) {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            //关联了引用队列,当软引用所关联的byte数组被回收时,软引用自己会加入到引用队列中
            WeakReference<byte[]> reference = new WeakReference<>(new byte[_1GB],queue);
            System.out.println(reference.get());
            list.add(reference);
            System.out.println(list.size());
        }
        //从队列获取无用的软引用对象并移除
        Reference<? extends byte[]> poll = queue.poll();
        while (poll!=null){
            list.remove(poll);
            poll=queue.poll();
        }
        System.out.println("循环结束");
        for (WeakReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }
    }
}

c2629ba808214888b686192bf7a55e15.jpg

这里就说明了在内存不足的情况下,软引用将会被自动回收。

但是值的注意的一点,即使有byte[] buff引用指向对象,且buff是一个strong reference,但是SoftReference sr指向的对象仍然被回收了

这是因为Java的编译器发现了在之后的代码中,buff已经没有被引用了,所有自动进行了优化。

4、弱引用

弱引用的引用强度比软引用更弱一些,**无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。**在JDK1.2之后,用java.lang.ref.WeakReference来表示弱引用。

案例:测试弱引用,无论内存是否充足,都会被回收掉

public class Demo13 {
    private static List<Object> list = new ArrayList<>();
    public static void main(String[] args) {
        testStrongReference();
    }
    private static void testStrongReference(){
        for (int i = 0; i < 10; i++) {
            byte [] buff = new byte[1024 * 1024];
            //指向软引用
            WeakReference<byte[]> str = new WeakReference<>(buff);
            list.add(str);
        }
        System.gc(); //显示调用gc
        for (int i = 0; i < list.size(); i++) {
            Object obj = ((WeakReference) list.get(i)).get();
            System.out.println(obj);
        }
    }
}

13a9a90e857c4642928003222b22b9ea.jpg

5、虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能被回收,在JDK1.2之后,用PhantomReference类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()方法,而且它的get()方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和ReferenceQueue引用对列一起使用。

例如:创建ByteBuffer的时候会创建一个名为Cleaner的虚引用对象,当ByteBuffer没有被强引用所引用就会被jvm垃圾回收,虚引用Cleaner就会被放入引用队列,会有专门的线程扫描引用队列,被发现后会调用直接内存地址的方法将直接内存释放掉,保证直接内存不会导致内存泄露

public class PhantomReference<T> extends Reference<T> {
    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

6、终结器引用

创建的时候会关联一个引用队列,当对象没有被强引用所引用时,对象被垃圾回收时,会将终结器引用放入到一个引用队列中(被引用的对象暂时还没有被垃圾回收),

有专门的线程(优先级较低,可能会造成对象迟迟不被回收)扫描引用队列并调用finallize()方法,第二次GC的时候才能回收掉被引用的对象。

7、引用队列(ReferenceQueue)

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现他还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

8、Reference对象状态变更




7af973c6afbc4c05a46425a2b5d71a6a.jpg

3.5.垃圾回收算法

1、清除标记算法

定义:Mark Sweep


2a07c10b2d5645a0aaa6317013843379.jpg

标记清除算法会把不在GC Root链上的引用进行标记,然后清除,它的速度非常的快,但是有一个问题,它清除后的空间没有进行整理,会造成内存碎片,比如说我清理了一个2mb、一个3mb、一个5mb的内存对象,但是现在有一个对象需要8mb,由于没有进行内存整理,这个8mb的对象不能引用刚刚三个任何一个,所以只能等待,而且效率上不是很高。

2、清除整理算法

定义:Mark Compact


a6313d7b5a534cdaa9ab63ce537bed92.jpg

标记整理算法和标记清除算法差不多,只不过在清除标记出的内存空间后,会进行整理,所以相对的效率会低一点,但是不会产生内存碎片,上面说到的8mb的内存就可以申请到,将2mb、3mb、5mb整理成10mb的内存空间。

3、复制算法

定义:Copy


a7c5e79b7e6c4e08a6a48f0f5e611238.jpg

复制算法分为两块相等的内存空间,from存放对象,to为空内存空间,当标记出需要回收的地址空间时,会把当前在GC Root链上的对象放到to内存空间上,将from上的无用的地址空间全部删除,然后再to与from空间进行对调,这样做的好处是不会产生内存碎片,但是需要占用双倍的内存空间。


3.6.分代垃圾回收


25a0efcf075e4c43b9f79436fc4d2b50.jpg



878fb0d002d348688820bd5fbc7dbbae.jpg



4402344843af4e0db6fdfebd672bebfb.jpg


16d4bc605ba946379dfce3f4dc6a8982.jpg

对象首先分配再伊甸园区域。

新生代空间不足时,触发Minor GC,伊甸园和from中存活的对象copy到to中,存活的对象年龄加1并且交换from to幸存区。

Minor GC会引发stop the world,暂停其他用户线程,等垃圾回收结束之后,用户线程才能恢复运行。

当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。

当老年代空间不足时,会先尝试触发minor gc,如果之后空间仍然不足,那么就会触发Full GC,STW 的时间会更长。

3.7.JVM相关参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx或-XX:MaxHeapSize=size
新生代大小 -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC -XX:+ScavengeBeforeFullGC

1、JVM相关参数

(1)-Xms:初始堆大小

  • 默认值为物理内存的1/64(<1GB),默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限度。

(2)-Xmx:最大堆大小

  • 默认值为物理内存的1/4(<1GB),默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限度。

(3)-Xmn:年轻代大小

  • 整个堆大小=年轻代大小+老年代大小。

(4)-XX:NewSize:设置年轻代大小

(5)-XX:MaxNewSize:年轻代最大值

(6)-XX:PermSize:设置持久代初始值

  • 默认值为物理内存的1/64。

(7)-XX:MaxPermSize:设置持久代最大值

  • 默认值为物理内存的1/4。

(8)-Xss:每个线程的堆栈大小

  • JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256k。

(9)-XX:ThreadStackSize:Thread Stack Size

10)-XX:NewRatio:年轻代(包括Eden和两个Survivor区)与老年代的比值

  • -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。
  • Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

(11)-XX:SurvivorRatio:Eden区与Survivor区的大小比值

  • 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。

(12)-XX:LargePageSizeInBytes:内存页的大小不可设置过大, 会影响Perm的大小

(13)-XX:+DisableExplicitGC:关闭System.gc()

  • 这个参数需要严格的测试。

(14)-XX:+AggressiveOpts:加快编译

(15)-XX:+UseBiasedLocking:锁机制的性能改善

(16)-Xnoclassgc:禁用垃圾回收

(17)-XX:SoftRefLRUPolicyMSPerMB:每兆堆空闲空间中SoftReference的存活时间

  • 默认值为1s。

(18)-XX:PretenureSizeThreshold:对象超过多大是直接在老年代分配

  • 默认值为0,不在老年代分配,单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象。

(19)-XX:+CollectGen0First:FullGC时是否先YGC

  • 默认值为false。

2、并行收集器相关参数

(1)-XX:+UseParallelGC:Full GC采用parallel MSC (此项待验证)

  • 选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)。

(2)-XX:+UseParNewGC:设置年轻代为并行收集

  • 与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

(3)-XX:ParallelGCThreads:并行收集器的线程数

  • 此值最好配置与处理器数目相等 同样适用于CMS。
  • 4)-XX:+UseParallelOldGC:年老代垃圾收集方式为并行收集(Parallel Compacting)
  • 这个是JAVA 6出现的参数选项。

(5)-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间)

  • 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

(6)-XX:+UseAdaptiveSizePolicy:自动选择年轻代区大小和相应的Survivor区比例

  • 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

(7)-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比

  • 公式为1/(1+n)

(8)-XX:+ScavengeBeforeFullGC:Full GC前调用YGC

  • 默认值为rue

3、CMS相关参数

(1)-XX:+UseConcMarkSweepGC:使用CMS内存收集

  • 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置。

(2)-XX:+AggressiveHeap

  • 试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)

(3)-XX:CMSFullGCsBeforeCompaction:多少次后进行内存压缩

  • 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理。

(4)-XX:+CMSParallelRemarkEnabled:降低标记停顿

(5)-XX+UseCMSCompactAtFullCollection:在FULL GC的时候, 对年老代的压缩

  • CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片。
  • (6)-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义初始化定义开始CMS收集
  • 禁止hostspot自行触发CMS GC。

(7)-XX:CMSInitiatingOccupancyFraction=70:使用cms作为垃圾回收 使用70%后开始CMS收集

(8)-XX:CMSInitiatingPermOccupancyFraction:设置Perm Gen使用到达多少比率时触发

(9)-XX:+CMSIncrementalMode:设置为增量模式

  • 用于单CPU情况。

4、辅助信息

(1)-XX:+PrintGC

  • 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

(2)-XX:+PrintGCDetails

输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

(3)-XX:+PrintGC:PrintGCTimeStamps

  • 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

(4)-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间.可与上面混合使用

  • 输出形式:Total time for which application threads were stopped: 0.0468229 seconds。

(5)-XX:+PrintHeapAtGC:打印GC前后的详细堆栈信息

(6)XX:+PrintTenuringDistribution:查看每次minor GC后新的存活周期的阈值

3.8.GC分析

测试代码:添加JVM参数 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc

public class Demo16 {
    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;
    public static void main(String[] args) {
    }
}

faf05a6f98fc4a8589daa8395307a4c5.jpg

public class Demo16 {
  //测试Minor GC回收,当添加的数据在伊甸园的空间范围内
    private static final int _512KB = 512 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}

414dc36062ec406092a8e02226bbd997.jpg


public class Demo16 {
  //当添加的数据超过新生代的总容量时或对象达到年龄阈值,老年代空间足够的情况下,会向老年代中存放
    private static final int _512KB = 512 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}

b24adf8830514d7a82d36f46e7156e82.jpg

public class Demo16 {
    //当添加的数据大于新生代的整体容量时,老年代空间足够的情况下,会直接存放在老年代,不会触发Minor GC
    private static final int _8MB = 8 * 1024 * 1024;
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }
}

51ce4e4a877f4fb18ad497ad5c25b12e.jpg

3.9.垃圾回收器

1、串行(Serial/Serial Old垃圾回收器)

  • 单线程
  • 堆内存较小,适合个人电脑
  • 指定JVM参数:-XX:+UseSerialGC = Serial + SerialOld
  • 这里要注意:Serial指定新生代用的垃圾回收算法是复制,SerialOld指定的是老年代用的垃圾回收算法是标记整理
  • 图解

a76cf69eebc34ce18df82e582d69be68.jpg

Serial垃圾收集器的特点

“Stop The World”,它进行垃圾收集时,必须暂停其他所有线程,直到他收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉。

使用场景:多用于桌面应用,Client端的垃圾回收器。

桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接收。

2、ParNew 收集器


ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX: SurvivorRatio、-XX: PretenureSize’ Threshold、-XX: HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码

parnew垃圾收集器的特点?


ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。

使用-XX: ParallelGCThreads 参数来限制垃圾收集的线程数

多线程操作存在上下文切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗

并发与并行的概念讲解 CMS垃圾回收器

并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能

会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上

3、吞吐量优先(Parallel Scavenge垃圾收集器)

多线程

堆内存较大,多核cpu

单位时间内,STW的时间最短

指定JVM参数:

-XX:+UseParallelGC (新生代吞吐量回收器,复制算法) -XX:UseParallelOldGC(老年代吞吐量回收器,标记整理算法)

-XX:ParallelGCThreads=n (控制线程数)

-XX:MaxGCPauseMillis=ms (最大暂停的毫秒数,默认值是200ms,即STW的时间)

-XX:+UseAdaptiveSizePolicy (自适应的新生代大小调整策略)

-XX:GCTimeRatio=ratio (设置垃圾回收时间的占比,默认ratio是99 ,也就是说 1/1+99 = 0.01,100分钟里只能有1分钟进行垃圾回收)

图解

52d5e98318944cdabcb62da40feec9f7.jpg

4、响应时间优先(CMS垃圾回收器)

  • 多线程
  • 堆内存较大,多核cpu
  • 单次的STW的时间最短
  • 指定JVM参数

-XX:+UseConcMarkSweepGC (老年代的垃圾回收 并发,采用标记清除算法,当CMS垃圾回收处理剩余过多的垃圾碎片时,就会退化成单线程的SerialOld垃圾回收器) -XX:+UseParNewGC(新生代的)

-XX:ParallelGCThreads=n (并行的垃圾回收线程数)

-XX:CMSInitiatingOccupancyFraction=percent (执行CMS内存达到一定比值进行垃圾回收)

-XX:ConcGCThreads=threads (并发线程数,设置为并行的线程数的四分之一)

-XX:CMSScavengeBeforeRemark (在重新标记之前,先对新生代进行垃圾回收)

图解


8e6a000140b34b0cb3490d12ada57ce1.jpg

CMS垃圾回收执行流程

  • 初始标记(CMS initial mark) -----标记一下 GC Roots 能直接关联到的对象,速度很快

并发标记(CMS concurrent mark --------并发标记阶段就是进行 GC RootsTracing 的过程

重新标记(CMS remark) -----------为了修正并发标记期间因用户程序导致标记产生变动的标记记录

并发清除(CMS concurrent sweep)

3.10.G1垃圾回收

1、G1垃圾回收阶段

定义:Garbage First

  • 2004 论文发布
  • 2009 JDK6u14加入体验
  • 2012 JDK7u4官方支持
  • 2017 JDK9默认

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认是暂停200ms
  • 超大堆内存,会将堆划分成多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间用的是复制算法

相关JVM参数

  • -XX:+UseG1GC (使用G1回收器)
  • -XX:G1HeapRegionSize=size (设置Region空间大小)
  • -XX:MaxGCPauseMillis=time (设置STW暂停时间)

538578e14ec84f529c9f137cac8061da.jpg

2、Young Collection



28d9f17ee433437287f6dbfca313bff2.jpg

3、Young Collection + CM

  • 在Young GC时会进行GC Root的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定。
  • -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

1089440e4e414964b45aae015a6af38a.jpg


4、Mixed Collection

  • 会对伊甸园、幸存区、老年代进行全面的垃圾回收
  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW
  • -XX:MaxGCPauseMillis=ms

ea5793d510594890baf3b97f6e06a3a0.jpg


5、G1收集器的运作大致可分为以下几个步骤

  • 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象。
  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象尽心可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

最终标记(Final Marking):为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set 中。

  • 筛选回收(Live Data Counting and Evacuation)

3.11.Full GC触发

  • SerialGC
  • 新生代内存不足发生的垃圾收集 - Minor GC
  • 老年代内存不足时发生垃圾收集 - Full GC
  • ParallelGC
  • 新生代内存不足发生的垃圾收集 - Minior GC
  • 老年代内存不足时发生的垃圾收集 - Full GC
  • CMS
  • 新生代内存不足发生的垃圾收集 - Minor GC
  • 老年代内存不足时,只有在并发失败时产生Full GC,没有并发失败时,产生Minor GC
  • G1
  • 新生代内存不足时发生的垃圾收集 - Minor GC
  • 老年代内存不足时,当老年代占堆内存的百分之45的时候,会触发Full GC回收,或者当垃圾产生的速度大于垃圾回收的速度时,会产生Full GC。

3.12.新生代跨代引用

1、什么是跨代引用

跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用

d6d1ec9f3cea4c3481944d76674a710b.jpg

2、跨代引用的问题

YGC时,为了找到年轻代中存活的对象,不得不遍历整个老年代,反之亦然。这种方案存在在极大的性能浪费。因为跨代引用是极少的,为了找出那么一丁点跨代引用,却遍历了整个老年代。

解决方案:记忆集(Card Table)

记忆集就是用来记录跨代引用的表,通过引入记忆集避免遍历老年代。以YGC为例说明,要回收年轻代,只需要引用年轻代对象的GC Root+记忆集,就可以判断出Young区对象是否存活,不必在遍历老年代。

缺点

存在滞后性,浪费一定的空间,如上图所示,YGC时实际上无引用对象实际是可以被回收的,但是由于老年代中被引用,所以无法被回收。

3.13.GMS与G1重新标记


728335e035c94f8691f939d0a01acec3.jpg


f11ac9acf457408eb75454fe452619e1.jpg

工作流程上来看,CMS的重新标记,和G1的最终标记之前都是并发标记。

既然时同时运行,用户程序就可能修改对象的引用关系,修改对象引用关系就可能影响GC回收。

所以,CMS重新标记,G1最终标记都是为了解决一件事,那就是并发过程中用户程序修改了对象引用关系后,如何让GC收集器仍旧能正确回收垃圾对象的问题。



image.jpeg

3.14.JVM部分新特性

1、JDK 8u20字符串去重

  • 优点:节省了大量内存
  • 缺点:略微多占用了cpu时间,新生代回收时间略微增加
  • 开启字符串去重的JVM参数:-XX:+UseStringDeduplation 默认是开启的

4a0ec1394aa64d6c971888045fd77d1b.jpg

  • 将所有新分配的的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个char[]
  • 注意:与String.intern()不一样
  • String.intern()关注的是字符串对象
  • 而字符串去重关注的是char[]
  • 在JVM内部,使用了不同的字符串表

2、JDK 8u40并发标记类卸载

所有对象都经过并发标记后,就能直到哪些类不在被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类。

-XX:+ClassUnloadingWithConcurrentMark 默认启用

3、JDK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象进行拷贝
  • 巨型对象回收时被优先考虑
  • G1会跟踪老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时被处理掉。

4、JDK9并发标记起始时间的调整

  • 并发标记必须在堆空间沾满前完成,否则退化成Full GC
  • JDK9之前需要使用 -XX:InitiatingHeapOccupancyPercent

JDK9可以动态调整

-XX:InitiatingHeapOccupancyPercent用来设置初始值

进行数据采样并且动态调整

总会有一个安全的空挡空间


相关文章
|
18天前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
1天前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
6天前
|
存储 算法 前端开发
JVM架构与主要组件:了解Java程序的运行环境
JVM的架构设计非常精妙,它确保了Java程序的跨平台性和高效执行。通过了解JVM的各个组件,我们可以更好地理解Java程序的运行机制,这对于编写高效且稳定的Java应用程序至关重要。
20 3
|
14天前
|
Java 编译器 测试技术
Java零基础教学(03):如何正确区别JDK、JRE和JVM??
【8月更文挑战第3天】Java零基础教学篇,手把手实践教学!
38 2
|
15天前
|
人工智能 Java 编译器
Java零基础(3) - 区别JDK、JRE和JVM
【8月更文挑战第3天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
36 1
|
20天前
|
Arthas 监控 算法
JVM成神路终章:深入死磕Java虚拟机序列总纲
JVM成神路终章:深入死磕Java虚拟机序列总纲
|
20天前
|
监控 Oracle Java
(一)JVM成神路之初识虚拟机 - 探寻Java虚拟机的前世今生之秘
JVM(Java Virtual Machine)Java虚拟机的概念大家都不陌生,Java之所以可以做到“一次编译,到处运行”的跨平台性,其根本原因就在于JVM。JVM是建立在操作系统(OS)之上的,Java虚拟机屏蔽了开发人员与操作系统的直接接触,我们在通过Java编写程序时,只需要负责编写Java代码即可,关于具体的执行则会由JVM加载字节码后翻译成机械指令交给OS执行。
|
12天前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优与性能提升
本文旨在为Java开发者提供一条清晰的路径,以深入掌握Java虚拟机(JVM)的内部机制和性能调优技巧。通过具体案例分析,我们将探讨如何识别性能瓶颈、选择合适的工具进行监控与调试,以及实施有效的优化策略,最终达到提高应用程序性能的目的。文章不仅关注理论,更注重实践应用,帮助读者在面对复杂的Java应用时能够游刃有余。
34 0
|
20天前
|
存储 Java 对象存储
Java虚拟机(JVM)中的栈(Stack)和堆(Heap)
在Java虚拟机(JVM)中,栈(Stack)和堆(Heap)是存储数据的两个关键区域。它们在内存管理中扮演着非常重要的角色,但各自的用途和特点有所不同。
30 0
|
20天前
|
存储 算法 Java
(四)JVM成神路之深入理解虚拟机运行时数据区与内存溢出、内存泄露剖析
前面的文章中重点是对于JVM的子系统进行分析,在之前已经详细的阐述了虚拟机的类加载子系统以及执行引擎子系统,而本篇则准备对于JVM运行时的内存区域以及JVM运行时的内存溢出与内存泄露问题进行全面剖析。