JVM学习-堆空间(四)

发布于:2024-05-20 ⋅ 阅读:(243) ⋅ 点赞:(0)
堆是分配对象存储的唯一选择吗?
  • 随着JIT编译器的发展与逃逸分配技术逐渐成熟,栈上分配、标量替换优化将会导致一些微妙变化,所有对象都分配到堆上也渐渐变得不那么“绝对”了
  • 在Java虚拟机中,对象是在Java堆中分配内存的,有一种特殊情况,那就是如果逃逸分配后发现,一个对象并没有逃逸出方法的话,可能会优化成栈上分配,这样就无需在堆上分配内存,也无须进行垃圾回收了,这就是最常见的堆外存储技术
  • 前面提到基于OpenJDK尝试定制 的TaobaoVM,其中创新的GCIH(GC invisible heap)技术实现了off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的
逃逸分析
  • 如何将堆上的对象分配到栈,需要使用逃逸分析手段
  • 这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
  • 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
  • 逃逸分析的基本行为是分析对象动态作用域
    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
//没有发生逃逸,可以在栈上分配,随着方法结束,栈空间就被移除
public void my_method() {
  V v = new V();
  v = null;
}

public static StringBuffer createStringBuffer(String s1,String s2) {
        StringBuffer strBuffer = new StringBuffer();
        strBuffer.append(s1);
        strBuffer.append(s2);
        return strBuffer;
}
    //上述代码如果要让StringBuffer strBuffer不逃出方法,可以修改如下
public static String createStringBuffer(String s1,String s2) {
        StringBuffer strBuffer = new StringBuffer();
        strBuffer.append(s1);
        strBuffer.append(s2);
        return strBuffer.toString();
}
  • 当一个对象在方法中被定义,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其他地方中
/**
 * Administrator
 * 2024/5/19
 * 如何快速判断是否发生逃逸分析,看new的对象实体是否有可能在方法外被调用
 */
public class EscapeAnalysis {
    public EscapeAnalysis obj;
    //方法返回EscapseAnalysis对象,发生逃逸
    public EscapeAnalysis getInstance() {
        return obj == null ? new EscapeAnalysis() : obj;
    }
    //成员属性赋值,发生逃逸
    public void setObj() {
        this.obj = new EscapeAnalysis();
    }
    //对象的作用域仅在当前方法中有效,没有发生逃逸
    public void userEscapseAnalysis() {
        EscapeAnalysis e = new EscapeAnalysis();
    }
    //引用成员变量的值,发生逃逸
    public void useEscapeAnalysis1() {
        EscapeAnalysis e = getInstance();
    }
}
参数设置
  • 在JDK 6u23版本之后,Hotspot中默认开启了逃逸分析
  • 如果使用较早版本,通过
    • 选项“-XX:+DoEscapseAnalysis”显示开启逃逸分析
    • 选项“-XX:+PrintEscapeAnalysis”查看逃逸分析的筛选结果
      开发中能使用局部变量的,就不要使用在方法外定义
逃逸分析代码优化
  • 栈上分配,将堆分配转化为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的侯选,而不是堆分配
    • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可以优化为栈上分析,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需垃圾回收了
    • 在逃逸分析中,已经说明了,分别是给成员变量赋值,方法返回值,实例引用传递会发生逃逸分析
//  -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
public class StackAllocaion {
    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(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void alloc() {
        User user = new User();  //未发生逃逸
    }
    static class User {

    }
}
//执行结果
花费时间:78ms

在这里插入图片描述

//调整堆空间大小256m,会发生gc
//  -Xms256G -Xmx256G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 65536K->808K(76288K)] 65536K->816K(251392K), 0.0010955 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 66344K->728K(76288K)] 66352K->736K(251392K), 0.0006559 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
花费时间:47ms
//  -Xms1G -Xmx1G -XX:+PrintGCDetails--开启逃逸分析
//执行结束 
花费时间:0ms
//  -Xms256G -Xmx256G -XX:+PrintGCDetails
//执行结束 
花费时间:0ms

在这里插入图片描述

  • 同步省略,如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
    • 线程同步的代价相当高,同步的后果是降低并发性和性能
    • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步,这样能大大提高并发性和性能。这个取消同步的过程叫同步省略,也叫锁消除
public void f() {
        Object hollis = new Object();
        synchronized (hollis) {
            System.out.println(hollis);
        }
   }
   代码中对hollis这个对象进行加锁,但是hollis对象的生命同期只有f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,如下
public void f() {
        Object hollis = new Object();
        System.out.println(hollis);
   }
  • 分离对象或标量替换,有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
    • 标量(Scalar)是一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量,相对还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量;在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,即么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替,这个过程就是标量替换
    • 标量替换参数设置:参数-XX:+EliminateAllocations:开启了标量替换(默认开启),允许将对象打散分配在栈上
public class ScalarTest {
    public static void main(String[] args) {
        alloc();
    }
    private static void alloc() {
        Point point = new Point(1,2);
        System.out.println("point.x=" + point.x + ";point.y=" + point.y);
    }
    class Point {
        private int x;
        private int y;
    }
}
//上述代码,经过标量替换后
private static void alloc() {
        int x = 1;
        int y = 2;
        System.out.println("point.x=" + point.x + ";point.y=" + point.y);
    }
//Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了,标量替换可以大大减少堆内存的使用,因为对象一旦不需要创建对象了,那么就不再需要分配堆内存,标量替换为栈上分配提供了很好的基础
//参数设置:-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:PrintGC -XX:-EliminateAllocations
public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }
    public static void alloc() {
        User u = new User();
        u.id = 5;
        u.name = "www.lotus.com";
    }

    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");
    }
}
//执行结果
[GC (Allocation Failure)  25600K->784K(98304K), 0.0009080 secs]
[GC (Allocation Failure)  26384K->752K(98304K), 0.0008422 secs]
[GC (Allocation Failure)  26352K->768K(98304K), 0.0006881 secs]
[GC (Allocation Failure)  26368K->736K(98304K), 0.0007941 secs]
[GC (Allocation Failure)  26336K->768K(98304K), 0.0007463 secs]
[GC (Allocation Failure)  26368K->784K(101376K), 0.0007665 secs]
[GC (Allocation Failure)  32528K->700K(101376K), 0.0007581 secs]
[GC (Allocation Failure)  32444K->700K(100352K), 0.0003688 secs]
花费时间为:47ms
//参数设置:-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:PrintGC -XX:+EliminateAllocations
//执行结果
花费时间为:0ms