JavaJAVA

JVM(四)内存与垃圾回收|运行时数据区(中)

2020-07-23  本文已影响0人  TiaNa_na

  在JVM(三) 运行时数据区(上)一文中,已经介绍过程序计数器java虚拟机栈两个模块。下面接着介绍本地方法栈

目录
 1 本地方法栈
  1.1 核心概述
 2 堆(heap)
  2.1 核心概述
   2.1.1内存细分
  2.2 设置堆内存大小与OOM
   2.2.1 设置堆内存大小
   2.2.2 OOM
  2.3 对象分配过程
   2.3.1 对象分配过程概述
   2.3.2 对象分配特殊情况
  2.4 Minor GC、Major GC、Full GC
  2.5 堆空间分代思想
  2.6 内存分配策略
  2.7 为对象分配内存:TLAB(线程私有缓存区域)
   2.7.1 为什么有TLAB(Thread Local Allocation Buffer)
   3.7.2 什么是TLAB
   2.7.3 对象分配过程(考虑TLAB后)
  2.8 小结堆空间的参数设置
  2.9 堆是分配对象的唯一选择么
   2.9.1 逃逸分析概述
   2.9.2 代码优化
   2.9.3 代码优化方法测试
   2.9.4 逃逸分析总结


1 本地方法栈


  学习本地方法栈之前,需要了解本地方法接口。值得注意的是:本地方法接口本身并不属于运行时数据区的一部分。

本地方法接口与本地方法栈在JVM中的位置
  请先移步 内存与圾回收|本地方法接口+字符串常量池 了解完本地方法接口后,再来看本地方法栈。

1.1 概述

2 堆(heap)


2.1 核心概述

example01:

public class HeapDemo {
   public static void main(String[] args) {
       System.out.println("start...");
       try {
           Thread.sleep(1000000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("end...");
   }
}

运行时通过-Xms10m -Xmx10m指定堆空间大小


通过Java VisualVM工具可以查看此进程内存分配情况。下载VisualVM并安装Visual GC插件可看到下图:

堆空间一定是所有线程共享的么?
不是,TLAB线程在堆中独有的

example 02:

public class SimpleHeap {
   private int id;//属性、成员变量

   public SimpleHeap(int id) {
       this.id = id;
   }

   public void show() {
     System.out.println("My ID is " + id);
  }
   public static void main(String[] args) {
       SimpleHeap sl = new SimpleHeap(1);
     SimpleHeap s2 = new SimpleHeap(2);

       int[] arr = new int[10];

     Object[] arr1 = new Object[10];
   }
}
2.1.1内存细分

  b.JDK 8以后: 新生区+养老区+元空间

约定:新生代==新生区==年轻代 养老区==老年区==老年代 永久区==永久代

2.2 设置堆内存大小与OOM

2.2.1 设置堆内存大小

-Xms 用于表示堆的起始内存,等价于 -XX:InitialHeapSize
(-X 是jvm的运行参数,ms 是memory start)
-Xmx 用于设置堆的最大内存,等价于 -XX:MaxHeapSize
以上命令设置堆空间大小=新生代+老年代,不包括永久代或元空间。

example 03:

   public static void main(String[] args) {

     //返回Java虚拟机中的堆内存总量
      long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
    //返回Java虚拟机试图使用的最大堆内存量
      long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

      System.out.println("-Xms : " + initialMemory + "M");
      System.out.println("-Xmx : " + maxMemory + "M");

       try {
           Thread.sleep(10000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

通过Edit Configurations->VM Options设置-Xms600m -Xmx600m
控制台输出的结果为-Xms : 575M -Xmx : 575M
查看设置的参数:
方式一:终端输入jps, 然后jstat -gc 进程id


方式二:(控制台打印)Edit Configurations->VM Options 添加XX:+PrintGCDetails

设置堆空间大小为600m,实际输出为575m,其原因是:在新生代存储数据时,幸存者s0和s1只选用一个。
2.2.2 OOM
/**
 * -Xms600m -Xmx600m
 * OOM举例 :example 04
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    private byte[] pixels;

    public Picture(int length) {
        this.pixels = new byte[length];
    }
}

2.3 对象分配过程

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

2.3.1 对象分配过程概述

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

2.3.2 对象分配特殊情况
对象分配特殊情况

2.4 Minor GC、Major GC、Full GC

JVM在进行GC时,并非每次都针对上面三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收都是指新生代。
针对hotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:部分收集(Partial GC)整堆收集(Full GC)

注意,很多时候Major GC 会和 Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收

2.4.1 GC策略触发机制

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

触发Full GC执行的情况有以下五种:
①调用System.gc()时,系统建议执行Full GC,但是不必然执行
②老年代空间不足
③方法区空间不足
④通过Minor GC后进入老年代的平均大小小于老年代的可用内存
⑤由Eden区,Survivor S0(from)区向S1(to)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明:Full GC 是开发或调优中尽量要避免的,这样暂停时间会短一些

/**
* example 05 
* 测试MinorGC 、 MajorGC、FullGC
* VM options配置参数 -Xms9m -Xmx9m -XX:+PrintGCDetails
* 字符串存堆空间
*/
public class GCTest {
   public static void main(String[] args) {
       int i = 0;
       try {
           List<String> list = new ArrayList<>();
           String a = "test";
           while (true) {
               list.add(a);
               a = a + a;
               i++;
           }

       } catch (Throwable t) {
           t.printStackTrace();
           System.out.println("遍历次数为:" + i);
       }
   }
}
控制台输出结果

2.5 堆空间分代思想

为什么要把Java堆分代?不分代就不能正常工作了么?
其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.6 内存分配策略

  如果对象在Eden出生并经过第一次Minor GC后依然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,把那个将对象年龄设为1.对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过选项 -XX:MaxTenuringThreshold来设置

  针对不同年龄段的对象分配原则如下:

/** 测试:大对象直接进入老年代
 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 * example 06
 */
public class YoungOldAreaTest {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024 * 1024 * 20];//20m

    }
}
控制台输出

2.7 为对象分配内存:TLAB(线程私有缓存区域)

2.7.1 为什么有TLAB(Thread Local Allocation Buffer)
2.7.2 什么是TLAB

说明

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

2.8 小结堆空间的参数设置

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

2.9 堆是分配对象的唯一选择么

  在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:
  随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
  在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后,发现一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
  此外,基于OpenJDK深度定制的TaoBaoVM,其中创新GCIH(GCinvisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

2.9.1 逃逸分析概述
//example 07
public static StringBuffer createStringBuffer(String s1,String s2){
    StringBuffer buffer = new StringBuffer();
    buffer .append(s1);
    buffer .append(s2);
    return buffer ;
}

由于上述方法返回的buffer 在方法外被使用,发生了逃逸,上述代码如果想要buffer 不逃出方法,可以这样写:

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

因此,开发中能使用局部变量的,就不要使用在方法外定义

JDK 6u23版本之后,HotSpot中默认就已经开启了逃逸分析
如果使用了较早的版本,开发人员可以通过
-XX:DoEscapeAnalysis 显式开启逃逸分析( +DoEscapeAnalysis: 开启 ; -DoEscapeAnalysis:关闭)
-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果

2.9.2 代码优化

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

2.9.3 代码优化方法测试

①栈上分配

/**
 * 栈上分配测试 example 08 
 * + 开启  -关闭
 * 关闭逃逸分析-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 * 开启逃逸分析-Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
 */
public class StackAllocation {
    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");
        // 为了方便查看堆内存中对象个数,线程sleep
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static void alloc() {
        User user = new User();//未发生逃逸
    }

    static class User {
    }
}
/**
 * 同步省略 example 09
 */
public class SynchronizedTest {
    public void f() {
        Object hollis = new Object();
        synchronized(hollis) {
            System.out.println(hollis);
        }
    }

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问控制,开启逃逸分析后,在JIT编译阶段就会被优化掉。优化为👇

    public void f2() {
        Object hollis = new Object();
        System.out.println(hollis);
    }
}

③分离对象或标量替换

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

/**
 * 标量替换 example 10
 */
public class ScalarTest {
    public static void main(String[] args) {
        alloc();   
    }
    public static void alloc(){
        Point point = new Point(1,2);
    }
}
class Point{
    private int x;
    private int y;
    public Point(int x,int y){
        this.x = x;
        this.y = y;
    }
}

经过标量替换后,就会变成👇

public static void alloc(){
    int x = 1;
    int y = 2;
}

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

2.9.4 逃逸分析总结
上一篇 下一篇

猜你喜欢

热点阅读