JVM(九) —— 运行时数据区之堆的详细介绍(四)

由于堆的内容较多,分为几个篇章进行,请看
JVM(六) —— 运行时数据区之堆的详细介绍(一)
JVM(七) —— 运行时数据区之堆的详细介绍(二)
JVM(八) —— 运行时数据区之堆的详细介绍(三)

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();
    }

首先设置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;

}

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹升茉莉清

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值