08-堆(三)

简介: 08-堆(三)

对象分配过程:TLAB


网络异常,图片无法展示
|


9 小结堆空间的参数设置


堆空间的参数设置


  • 官网说明


https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE


  • -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: 是否设置空间分配担保


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


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


JDK6 Update24(JDK7)之后,HandlePrpmotionFailure参数不会再影响到虚拟机的空间担保策略,观察OpenJDK的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。


10 堆是分配对象存储的唯一选择吗?


堆是分配对象存储的唯一选择吗?


在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:


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


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


此外,前面提到的基于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();
    // use v
    v = null;
}

没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除。

public static StringBuffer createStringBUffer(String s1,String s2) {
    StringBuffer sb new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

上述代码如果想要StringBuffer sb不逃出方法,可以这样写:

public static String createStringBuffer(String s1, String s2) {
    StringBudder sb = new StringBuffer();
    sp.append(s1);
    sp.append(s2);
    return sb.toString();
}

参数设置:


  • 在JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析。
  • 如果使用的是较早的版本,开发人员则可以通过:
  • 选项“-XX: +DoEscapeAnalysis”显式开启逃逸分析
  • 通过选项“-XX:PrintEscapeAnalysis"查看逃逸分析的筛选结果。


代码分析示例:

public class EscapAnalysis {
  public EscapAnalysis obj;
  /**
   * 方法返回EscapeAnalysis对象,发生逃逸
   */
  public EscapAnalysis getInstance() {
    return obj == null ? new EscapAnalysis() : obj;
  }
  /**
   * 为成员属性赋值,发生逃逸
   */
  public void setObj() {
    this.obj = new EscapAnalysis();
  }
  /**
   * 对象作用域仅在当前方法中有效,没有发生逃逸
   */
  public void useEscapeAnalysis() {
    EscapAnalysis s = new EscapAnalysis();
  }
  /**
   * 引用成员变量的值,发生逃逸
   */
  public void useEscapeAnalysis1() {
    EscapAnalysis e = getInstance();
  }
}

结论:开发中能使用局部变量的,就不要使用在方法外定义。


逃逸分析:代码优化


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


一、栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。


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


三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结果存在也可以被访问,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。


代码优化之栈上分配


  • JIT编译器在编译期间根据逃逸分析的结果,发现一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
  • 常见的栈上分配的场景
  • 在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。


实例代码

public class StackAllocation {
  public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 10000000; i++) {
      alloc();
    }
    long end = System.currentTimeMillis();
    System.out.println("耗时:" + (end - start) + "ms");
    // 为了方便查看堆内存中对象个数
    Thread.sleep(1000000);
  }
  private static void alloc() {
    // 为发生逃逸
    User user = new User();
  }
  static  class User{}
}

设置JVM参数


-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails


执行,耗时100ms,查看取样器,里面有一千万个对象


网络异常,图片无法展示
|


开启逃逸分析


-Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails


执行耗时6ms,取样器中没有看到对象


网络异常,图片无法展示
|


代码优化之同步省略(消除)


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


如下代码:

public void f() {
    Object hellis = new Object();
    synchronized(hollis) {
        System.out.println(helllis);
    }
}

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:

public void f() {
    Object hellis = new Object();
    System.out.println(helllis);
}

代码优化之标量替换


标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。


相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。


在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干的成员变量来替代。这个过程就是标量替换

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);
}
calssPoint{ 
    private int x;
    private int y;
}

以上代码,经过标量替换后,就会变为

private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x="+x+";point.y="+y);
}

可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。


标量替换为栈上分配提供了很好的基础。


标量替换参数设置:


参数-XX:+EliminateAllocations: 开启了标量替换(默认打开),允许将对象打散分配在栈上。


代码示例:

/**
 * -Xmx100m -Xms100m -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 = "1";
  }
  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");
  }
}


JVM参数:

-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations

结果

[GC (Allocation Failure)  25600K->848K(98304K), 0.0008837 secs]
[GC (Allocation Failure)  26448K->768K(98304K), 0.0009537 secs]
[GC (Allocation Failure)  26368K->720K(98304K), 0.0007731 secs]
[GC (Allocation Failure)  26320K->784K(98304K), 0.0007894 secs]
[GC (Allocation Failure)  26384K->768K(98304K), 0.0007600 secs]
[GC (Allocation Failure)  26368K->736K(101376K), 0.0007292 secs]
[GC (Allocation Failure)  32480K->696K(101376K), 0.0008760 secs]
[GC (Allocation Failure)  32440K->696K(100352K), 0.0004999 secs]
花费时间为:60ms

JVM参数:

-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

结果


花费时间为:5ms


参数说明

-Server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

这里使用参数如下:


  • 参数-Server: 启动Server模式,因为在Server模式下,才可以启用逃逸分析。
  • 参数 -XX:DoEscapeAnalysis: 启用逃逸分析
  • 参数 -Xmx10m: 执行了堆空间最大内存
  • 参数-XX:+PrintGC: 将打印GC日志
  • 参数-XX:+EliminateAllocations:  开启了标量替换(默认打开),允许将对象打散分配在栈上,比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。


逃逸分析小结:逃逸分析并不成熟


  • 关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟的。
  • 其根本愿意就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
  • 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过后才能就白白浪费掉了。
  • 虽然这项技术并不成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
  • 注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配哪些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据了解,Oracle HotSpot JVM并为这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
  • 目前很多书籍还是基于JDK 7 以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样复核前面一点的结束:对象实例都是分配在堆上。


本章小结


  • 年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
  • 老年代放置长生命周期的对象,通常都是从Survivor区域帅选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上,如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲区域,JVM就会直接分配到老年代。
  • 当GC只发生在年轻代中,回收年轻代对象的行为被称为Minor GC。当GC发生在老年代则被称为Major GC或Full GC。一般的Minor GC的发生频率要比Major GC高很多,即老年代中垃圾回收发生的频率将大大低于新生代。
目录
相关文章
|
存储 算法 索引
堆的实现(C版)
堆的实现(C版)
43 0
|
6月前
|
存储 程序员
什么是堆,什么是栈
什么是堆,什么是栈
89 0
|
6月前
|
存储 缓存 JavaScript
|
算法
堆的实现以及应用
我们说堆在物理上是一个数组,逻辑上它是一个完全二叉树,我们可以通过它的下标来计算父亲和孩子之间的关系。
|
存储 缓存 算法
08-堆(二)
08-堆(二)
140 0
|
存储 缓存 Java
08-堆(一)
08-堆(一)
78 0