• JVM —— 运行时数据区之堆的详细介绍(汇总篇)



    一个Java程序运行起来是一个进程,这个进程对应着一个JVM实例,一个JVM实例对应着一个运行时数据区。而一个运行时数据区对应着一个方法区和一个堆。而一个进程中会有多个线程,这些线程会共享方法区和堆区。

    核心概述

    • 一个JVM实例只对应一个堆内存,对也是Java内存管理的核心区域。
    • Java堆区在JVM启动的时候就被创建,齐空间大小也就确定了。是JVM管理的最大的一块内存空间。
    • 堆内存大小是可以调节的,使用 -Xms 和-Xmx命令进行调节,其语法和栈空间大小一致。
    • Java虚拟机规范规定,堆可以处于物理上不连续的内存空间中,但在逻辑上他应该是被视为连续的
    • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer , TLAB)。
    • 所有的对象实例几乎都在堆空间分配,也有一些特殊情况不在堆空间分配。(比如逃逸分析,栈上分配等,这部分会在后边详细说明)
    • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
    • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
    • 堆是垃圾回收(GC)的重点区域。

    在这里插入图片描述

    堆空间代码演示

    堆的空间大小使用-Xms 和-Xmx进行设置,其中-Xms表示的是Java启动的时候堆空间的大小,-Xmx表示的是堆空间最大的大小。

    准备好我们的代码:
    Test1 堆空间大小设置10M

    public class Test1 {
    //    -Xms10m 和-Xmx10m
        public static void main(String[] args) {
            System.out.println("开始");
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("结束");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Test2 堆空间大小设置20M

    public class Test2 {
    //    -Xms20m 和-Xmx20m
        public static void main(String[] args) {
            System.out.println("开始");
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("结束");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在启动之前我们要想设置堆空间的大小:
    在这里插入图片描述
    在这里插入图片描述

    两个运行程序分别设置好,我们就可以启动了。
    启动之后我们找到我们本机java的安装目录,进入bin目录,打开jvisualvm.exe就可以看到以下界面:
    在这里插入图片描述
    其中红框里边的就是我们刚刚启动的Java进程,双击就会打开:
    在这里插入图片描述
    红框中就可以看到我们刚刚设置的堆大小:10M。
    我们再看看Visual GC界面,这里边就是我们对应的对里边各个细化的不同区域的堆空间占用大小。包括新生代,老年代等空间的大小和占用情况。这部分我们在后边会详细介绍,这里先让大家感受一下。当然,有效的同学打开之后没有Visual GC界面,是因为没有安装Visual GC插件,插件安装请看:安装Visual GC插件
    在这里插入图片描述

    堆空间划分

    在这里插入图片描述

    现代垃圾收集器大部分都基于分带手机理论设计。
    Java7及之前的堆内存逻辑上分为三部分:新生区 + 养老区+永久代

    • Young Generation Space 新生区 Young/new
      • 又被划分为Eden区和Survivor区
    • Tenure Generation Space 养老区 Old/Tenure
    • Permanent Space 永久区 Perm

    **
    Java8及之后的堆内存逻辑上分为三部分:新生区 + 养老区+元空间

    • Young Generation Space 新生区 Young/new
      • 又被划分为Eden区和Survivor区
    • Tenure Generation Space 养老区 Old/Tenure
    • Meta Space 元空间 Meta
    新生区 < = > 新生代 < = > 年轻代

    养老区 < = > 老年区 < = > 老年代
    永久区 < = > 永久代

    在这里插入图片描述
    在逻辑上永久代是划分在堆空间的,事实上堆空间没有永久代,永久代或者说元空间是方法区具体的落地实现。

    我是用的JDK是8版本,我们可以看一下刚刚jvisualvm,我们可以看到新生代,老年代和元空间各自的空间占用,但是新生代和老年代的空间已经占据了10M,元空间是占用了1G的大小。
    在这里插入图片描述

    我们也可以通过设置JVM参数来查看各个区域的内存信息和GC信息,使用-XX:+PrintGCDetails打印GC信息,设置好参数运行:

    在这里插入图片描述

    堆空间大小设置

    Java堆用于存储Java对象实例,那么堆大小在JVM启动的时候就已经设置好了,可以通过-Xms 和-Xmx进行设置。

    • -Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize
    • -Xmx 表示堆区的最大内存,等价于 -XX:MaxHeapSize

    一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError异常。

    通常会将-Xms 和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分配堆区的大小,从而提升性能。

    默认情况下,初始内存大小为物理电脑内存大小/64;最大内存大小为物理电脑内存大小/4。

    我们可以通过一下代码查看堆空间的起始大小和最大空间大小,并计算出我们的物理内存:

     public static void main(String[] args) {
            //堆起始内存
            long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
            //堆最大内存
            long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    
            System.out.println("-Xms:" + initialMemory + "M");
            System.out.println("-Xmx:" + maxMemory + "M");
    
            System.out.println("系统内存大小为:" + initialMemory * 64 / 1024 + "G");
            System.out.println("系统内存大小为:" + maxMemory * 4 / 1024 + "G");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    我电脑是内存16G,输出的和我们的16怎么差一点呢?别急,跟我往下看。
    现在上边的程序进行手动设置堆内存大小为600M : -Xms600m -Xmx600m。
    在这里插入图片描述
    再运行:
    在这里插入图片描述
    我们把程序进行改一下,让程序不停止。

    public static void main(String[] args) {
            //堆起始内存
            long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
            //堆最大内存
            long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    
            System.out.println("-Xms:" + initialMemory + "M");
            System.out.println("-Xmx:" + maxMemory + "M");
    
    //        System.out.println("系统内存大小为:" + initialMemory * 64 / 1024 + "G");
    //        System.out.println("系统内存大小为:" + maxMemory * 4 / 1024 + "G");
    
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行,之后打开dos命令窗口,执行jps拿到我们程序的进程id,然后执行jstat -gc pid.
    在这里插入图片描述

    我们将上边的数字加在一起运算:
    在这里插入图片描述
    发现正好是我们设置的600M。
    然后我们只加一个幸存者区的,在进行运算:
    在这里插入图片描述
    发现正好是我们输出的575M。
    为什么会这样呢?这就涉及到堆的内存结构,**年轻代空间是两个幸存者区和Eden区,而幸存者区大小一样并且只会有一个有数据。**也就是说幸存者区在某一时刻必定会有一个是没有数据的,也就是说不占空间,所以在计算的时候只算了一个幸存者区的空间,也就是上边的575M。

    OOM介绍和举例

    上边我们提到在堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError异常。而对象引用存在栈中,实际的对象存在堆中。如果我们一直在堆中添加对象就会出现OOM。
    我们来看下边的代码:

    public class Test1 {
    
    
        public static void main(String[] args) {
            ArrayList<Picture> list = new ArrayList<>();
            while (true) {
             try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                int i = new Random().nextInt(1024 * 1024);
                // picture 会放在堆空间
                Picture picture = new Picture(i);
                list.add(picture);
            }
        }
    
    
    
    }
    
    class Picture {
        private byte[] img;
    
        public Picture(int length) {
            this.img = new byte[length];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们在list中不断加添加Picture 对象,最终占用堆空间会达到最大值,最后报错OutOfMemoryError:
    在这里插入图片描述
    我们可以打开jvisualvm进行查看,可以看到红框的新生代老年代会一直增加,等到堆空间满了,就会报错OutOfMemoryError
    在这里插入图片描述
    在抽样器中也可以看到是byte数组占用了很大的内存:
    在这里插入图片描述

    年轻代和老年代

    上边我们一直在说年轻代,老年代,它们到底是什么样的呢,在堆中又是怎么划分呢?来随我看看吧!

    存储在JVM中的Java对象可以被划分为两类:

    • 一类是生命周期较短的瞬时对象,这类对象的创建和向往都非常迅速。
    • 一类是生命周期非常长,在某些极端的情况下还能够与JVM生命周期保持一致。

    Java堆区进一步细分的话,可以分为年轻代(YoungGen)和老年代(OldGen)
    其中年轻代又可以划分为Eden空间,Survivor0空间和Survivor1空间(有时候也叫作from区和to区。)

    在这里插入图片描述

    堆空间大小分配

    在这里插入图片描述
    堆空间的分配入上图所示。
    我们可以通过参数进行修改。配置新生代与老年代在堆结构的占比。

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3.
    • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5.
    • 一般默认值在开发中是不调的。
    • 在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1

    但是我们使用不设置的时候,通过工具查看 Eden空间和另外两个Survivor空间比例是6:1:1
    在这里插入图片描述

    • 通过选项-XX:SurvivorRatio调整Eden空间和另外两个Survivor空间比例,我们需要显式的指定XX:SurvivorRatio=8上边的比例才会是8:1:1。
    • 几乎所有的Java对象都是在Eden区被new出来的
    • 绝大部分的Java对象的销毁都在新生代进行,新生代80%的对象都是朝生夕死
    • 可以使用选项-Xmn设置新生代最大内存大小,这个参数一般不设置,如果它和-XX:NewRatio一起设置,则使用-Xmn设置的大小。

    新生代升为老年代流程图:
    在这里插入图片描述

    对象的分配过程

    为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生碎片。

    1. new的对象先放在伊甸园区,此区有大小限制。

    在这里插入图片描述
    2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用对象进行销毁,再加载新的对象放到伊甸园区。
    2. 然后将伊甸园区的剩余对象移动到幸存者0区,此时对象的存活年龄+1。
    在这里插入图片描述

    1. 如果再次触发垃圾回收,会将伊甸园区和幸存者0区的不再被其他对象所引用对象进行销毁,此时伊甸园区和幸存者0区存活的对象都会转移到幸存者1区,此时幸存者0区没有对象。
      在这里插入图片描述

    2. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区,每次年龄都会+1。
      在这里插入图片描述

    3. 那什么时候会去养老区呢?可以设置次数,默认15次,也就是年龄达到16 的时候就会转到养老区了。可以设置参数 -XX:MaxTenuringThreshold来进行设置。

    4. 如果幸存者区中相同年龄的所有对象大小的综合大于幸存者区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold的要求年龄。

    5. 在养老区相对悠闲,当养老区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。

    6. 若养老区进行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。

    总结

    针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
    关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。

    对象分配流程图

    上边的过程是一般对象分配的过程,但是也会出现垃圾回收后空间不够的情况,那么这时候应该怎么办呢?如果最开始的对象就在Eden区无法放置又怎么办呢?在java堆中,整个对象的分配如下边的流程图。
    在这里插入图片描述

    代码演示垃圾回收

    准备以下代码:

    public class Test1 {
    
    
        public static void main(String[] args) {
            ArrayList<Picture> list = new ArrayList<>();
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                int i = new Random().nextInt(1024 * 200);
                Picture picture = new Picture(i);
                list.add(picture);
            }
        }
    
    
    
    }
    
    class Picture {
        private byte[] img;
    
        public Picture(int length) {
            this.img = new byte[length];
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    上边的代码就是把一个比较大的对象一直放在ArrayList对象中,而ArrayList一直被占用没有垃圾,这样堆空间就会一直累加。

    然后配置好JVM参数:-Xms600m -Xmx600m。
    在这里插入图片描述
    配置好参数之后,我们就可以启动项目了。然后打开jvisualvm查看堆空间的变化情况。

    在这里插入图片描述
    一开始Eden区不断增长,等Eden空间满了之后发生一次YGC(此时Eden是空的),将存活的对象放在了幸存者区,之后不断有新对象创建,放在Eden区,等Eden空间满了之后再次发生YGC,此时由于幸存者区空间不够,会把对象放到老年代,而s1,s2会交替复制存活的对象,老年代则会一直接收从新生代过来的对象,直到老年代再也放不进去对象,进行FGC/Major GC.还是没有足够的空间,报错:java.lang.OutOfMemoryError: Java heap space

    这也就解释了为什么Eden是逐渐增长,幸存区交替出现,老年区台阶式的增长。新对象创建放在Eden,不断放就会一直增长,Eden满了触发YGC,Eden变空,存活对象进入幸存区,之后创建的对象又会放在Eden,慢慢Eden又满了,再次触发YGC,此时想要放到幸存区,发现空间不够,所以幸存区放到老年区,然后新对象放在另一半的幸存者区,此时老年区就会有对象,所以有了第一级台阶,如此反复,直到堆区空间占满。而元空间(Metaspace)存放的是类和其他的一些信息,一直不会变,所以元空间(Metaspace)基本保持不变。

    在这里插入图片描述

    Minor GC, Major GC, Full GC

    在java程序运行的时候会有GC线程运行进行垃圾回收,这样就会导致用户线程(执行业务代码)暂停(Stop The World, STW),所以我们要尽量减少垃圾回收。

    JVM在进行GC时,并非每次都对三个内存(新生代,老年代,方法区)区域一起回收,大部分时候回收的都是指新生代。

    针对HotSpot虚拟机,他里边的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC).
    部分收集不是完整收集整个Java堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾收集
    • 老年代收集(Major GC/ Old GC)只是老年代的垃圾收集。(很多时候Major GC和Full GC混淆使用,需要具体分辨是老年代还是整堆收集),CMS GC 会单独手机老年代的行为。

    混合收集(Mixed GC)手机真个新生代以及部分老年代的垃圾收集,G1会有这种行为。
    整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

    年轻代GC(Minor GC)触发机制:

    • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,幸存者区满不会引发GC。每次Minor GC会清理年轻代的内存。
    • 因为Java对象大多都具备朝生夕死的特性,。所以非常频繁,一般回收速度也比较快。
    • Minor GC会引发STW,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复执行。

    在这里插入图片描述

    老年代GC(Major GC/Full GC)触发机制

    • 指发生在老年代的GC对象从老年代消失时,我们说Major GC/Full GC发生了。
    • 出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
    • 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则会触发Major GC。
    • Major GC的速度一般会比Minor GC慢10倍以上,STW时间更长。
    • 如果Major GC后北村还不足,就报OOM了。

    Full GC触发机制

    触发Full G执行的情况如下:

    • 调用System.gc()时,系统建议执行Full GC,但不是必然执行。
    • 老年代空间不足
    • 方法区空间不足。
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用空间。
    • 由Eden区向幸存者区复制或者幸存者区相互复制时,对象大小大于复制目的地的空间大小,则把该对象转存到老年代,且老年代的可用内存小于改对象大小。

    Full GC是开发或调优中尽量要避免的,这样暂停时间会短一些。

    堆空间分代思想

    在这里插入图片描述
    不同对象的生命周期不同,70%-90%的对象都是临时对象。
    其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一起,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆所有区域进行扫描。而很多对象都是朝生夕死,如果分代的话,把新创建的对象放到某一地方,当GC的时候就先把这块存储朝生夕死的对象区域进行回收,就会腾出很大的空间来。

    TLAB

    堆区是线程共享区域,任何线程都乐意访问到堆区的共享数据。

    由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。

    为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

    TLAB全称Thread Local Allocation Buffer, 从内存模型的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内,这部分就是我们所说的TLAB。

    多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略

    在这里插入图片描述
    尽管不是所有对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
    在程序中,开发人员可以通过选项-XX:UseTLAB设置是否开启TLAB空间。
    默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间占用Eden空间百分比大小。
    一旦对象在TLAB空间分配内存空间失败时,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间分配内存。
    在这里插入图片描述

    堆空间常用的参数设置

    • -XX:+PrintFlagsInitial: 查看所有参数的默认初始值
    • -XX:+PrintFlagsFinal: 查看所有参数的最终值(可能会存在修改,不再是初始值)
    • -Xms: 初始堆空间大小(默认为物理呢村的1/64)
    • -Xmx: 最大堆空间大小(默认为物理呢村的1/4)
    • -Xmn:设置新生代的大小(初始值及最大值)
    • -XX:NewRatio: 配置新生代与老年代在堆结构的占比
    • -XX:SurvivorRatio:设置新生代中Eden和s0/s1空间的比例
    • -XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄
    • -XX:+PrintGCDetails:输出详细的GC处理日志
      • 打印GC简要信息:①-XX:+PrintGC ②-verbose:gc
    • -XX:HandlePromotionFailure: 是否设置空间分配担保

    HandlePromotionFailure

    在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。

    • 如果大于,则此次Minor GC是安全的
    • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
      • 如果 HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
        • 如果大于,则尝试进行一次 Minor GC,但这次Minor GC依然是有风险的
        • 如果小于,则改为进行一次Full GC
      • 如果 HandlePromotionFailure=false,则改为进行一次Full GC

    在JDK7及之后,HandlePromotionFailure参数不会在影响到虚拟机的空间分配担保策略。规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC

    逃逸分析和栈上分配

    随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配,变量替换优化技术将会导致一些微妙的变化,所有的对象都分配到腿上也逐渐变得不那么绝对了。

    在JVM中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能内优化成栈上分配。这样就无需在腿上分配内存,也无需进行垃圾回收了。这也是最常见的堆外存储技术。

    由淘宝定制的TaoBaoVM,其中创建的GCIH(GC Invisible Heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

    如何将堆上的对象分配到栈,需要使用逃逸分析手段。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

    通过逃逸分析,Java HotSpot编译器能够分析出一个新的对象的引用的适用范围从而决定是否哦要将这对象分配到堆上。

    逃逸分析的基本行为就是分析对象动态作用域:

    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为福阿生逃逸。例如作为调用参数传递到其他地方中。

    在这里插入图片描述
    上边没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。

    在这里插入图片描述
    上边的代码返回sb,可能被其他方法调用,会发生逃逸,如果不掏出方法们可以这样写:

    在这里插入图片描述
    因为StringBuffer是重写了toString,重新new了一个对象,所以返回的对象并不是sb本身。
    在这里插入图片描述

    如何快速判断是否发生了逃逸?大家就看new的对象实体是否有可能在方法外被调用。
    在JDK7之后,HotSpot中默认就已经开启了逃逸分析。
    经过上边的讲解,方法中能使用局部变量的,就不要使用在方法外定义。

    代码优化

    使用逃逸分析,编译器可以对代码做优化。

    栈上分配

    将堆分配转化为栈分配。若果一个对象在子程序中被分配,要是只限该对象的指针永华不会逃逸,对象可能是站分配的候选,而不是堆分配。
    JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可以被优化成栈上分配。分配完之后,继续在调用站内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无需进行垃圾回收了。

    发生逃逸的情况:给成员变量复制,方法返回值,实例引用传递。

    我们可以-XX:DoEscapeAnalysis设置是否开启逃逸分析,
    -XX:-DoEscapeAnalysis表示关闭逃逸分析,
    -XX:+DoEscapeAnalysis表示开启逃逸分析。
    我的JDK使用的是8.我们编写以下代码:

    public static void main(String[] args) {
    
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                alloc();
            }
            long end = System.currentTimeMillis();
           System.out.println("花费时间:" + (end - start) + "ms");
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
        }
    
        private static void alloc() {
            // 不会发生逃逸
            User user = new User();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    首先设置JVM参数:-Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails,不开启逃逸分析,然后执行

    花费97ms,打开jvisualvm:
    在这里插入图片描述
    然后我们设置VM参数:-Xms1G -Xmx1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails,再运行:
    在这里插入图片描述
    只花费了4ms,打开jvisualvm:
    在这里插入图片描述
    实例数比我们设置的10000000少很多,性能也提升了。

    我们设置JVM参数-Xms256M -Xmx256M -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
    在这里插入图片描述
    堆空间变小,发生了GC,花费57ms

    我们设置JVM参数-Xms256M -Xmx256M -XX:+DoEscapeAnalysis -XX:+PrintGCDetails

    在这里插入图片描述
    没有发生GC,并且只花费了3ms.

    通过上边的数据可以看到,通过栈上分配可以提高系程序的效率,还可以减少GC的情况。

    同步省略

    如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

    线程同步的代价是相当高的,同步的后果是降低并发性和性能。

    在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的的所对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

    在这里插入图片描述

    分离对象或标量替换

    有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在CPU寄存器。

    标量是指一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量。
    相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量,因为它可以分解成其他聚合量和标量。
    在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会吧这个对象拆解成若干个其中包含的若干个成员变量来替换,这个过程就是标量替换。

    在这里插入图片描述
    在这里插入图片描述
    标量替换参数设置:
    -XX:+EliminateAllocations 开启标量替换(默认)允许将对象打散分配在栈上。
    -XX:-EliminateAllocations 关闭标量替换

    准备代码

    public class Test1 {
    
        public static void main(String[] args) {
    
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000000; i++) {
                alloc();
            }
            long end = System.currentTimeMillis();
            System.out.println("花费时间:" + (end - start) + "ms");
    
    
        }
    
        private static void alloc() {
            // 不会发生逃逸
            User user = new User();
            user.id = 1;
            user.name = "zhangsan";
        }
    
    
    }
    
    class User {
        public  int id;
        public  String name;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    设置VM参数:-Xms100M -Xmx100M -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations
    运行结果:
    在这里插入图片描述设置VM参数:-Xms100M -Xmx100M -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations,开启标量替换。
    运行结果:
    在这里插入图片描述
    发现没有发生GC(相比关闭标量替换),并且花费时间很少。

    好了关于堆的介绍到这里就已经介绍完了,接下来会介绍JVM的其他相关内容。创作不易,各位老铁给个三连哦!

  • 相关阅读:
    Kubernetes云原生实战03 搭建高可用负载均衡器(Keepalived 和 HAproxy)
    vivo 海量微服务架构最新实践
    OBCP实验全面升级|官方为你送上备考攻略+福利
    恭贺弘博创新2023下半年软考(中/高级)认证课程顺利举行
    Java 9 的模块(Module)系统
    TVM-MLC LLM 调优方案
    LeetCode高频题:Android系统中WakeLock防止手机进入睡眠模式,统计出每个应用对WakeLock的不同贡献值
    2.ffmpeg安装(Ubuntu20.04 )
    怎么把mp4转换成amv格式?如何下载amv格式视频?
    Leetcode 112. 路径总和 java解决给定一个值判断二叉树根节点到叶子节点总和是否相等 算法
  • 原文地址:https://blog.csdn.net/weixin_40920359/article/details/127453407