JVM 堆空间学习

文章目录

      • 堆空间:内存细分
      • 堆空间大小设置:
        • 查看设置的参数
        • 年轻代与老年代
        • 配置新生代与老年代在堆结构的占比
        • 对象分配过程--详细
        • Minor GC、Major GC、Full GC
        • 常用的调优工具
        • TLAB(Thread Local Allocation Buffer)
        • 测试堆空间常用的参数总结:
        • 逃逸分析
          • 逃逸分析的基本行为就是分析对象动态作用域

堆空间:内存细分

java7及以前版本,堆内存逻辑上分为:新生区(Young generation space)-养老区(tenure generation space)-永久区(permanent space)
java8及以后版本,堆内存逻辑上分为:新生区-养老区-元空间(meta space)
新生区又被划分为:Eden区和Survivor区(有时也称为from区、to区)


堆空间大小设置:

  • -Xms表示堆区的起始内存(年轻代+老年代),等价于-XX:InitialHeapSize
    -X 是jvm的运行参数
    ms 是memory start
  • -Xmx表示堆区的最大内存(年轻代+老年代),等价于-XXMaxHeapSize

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

一般会将-Xms -Xmx两个参数配置相同的值,目的是为了能够在java垃圾回收机制清理完堆区后不需要分割计算堆区的大小,从而提高性能。
默认情况下
初始内存大小:物理电脑内存/64
最大内存大小:物理电脑内存/4


查看设置的参数

方式一 命令:
jps:各程序运行的进程
jstat -gc 进程:进程使用空间
方式二
-XX:+PrintGCDetails


年轻代与老年代

存储在jvm中的java对象可以分为两类(周期):

  • 生命周期比较短,创建和消亡都很迅速
  • 生命周期比较长,极端情况甚至与jvm的生命周期一致

在这里插入图片描述

刚刚创建的对象会放入Eden区,后期回收有些eden的对象被回收了,有些没有被回收就会放到survivor区。(survivor0和survivor1 只有一个有数据,另一个为空),在经过一段时候如果这个对象还是存活,那么就会到老年代。

配置新生代与老年代在堆结构的占比
  • 默认:-XX:NewRatio=2 ,表示新生代占1,老年代占2,新生代占整个堆的1/3;
  • 修改:-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5;
    命令查看
    jps:查看进程
    jinfo -flag NewRatio 进程 能够查看-XX:NewRatio=2的配置
    在这里插入图片描述

Eden与survivor的比例为:8:1:1 , 但是实际上看不是8:1:1 可能是自适应分配了内存
-XX:-UseAdaptiveSizePolicy (关闭自适应的内存分配策略),这个参数基本不好使。
想要配成8:1:1,-XX:SurvivorRatio=8
几乎所有的java对象都是在Eden区创建,如果对象太大Eden装不下可能会直接放入老年代。
绝大多数对象的销毁都是在新生代。
-xmn 设置新生代空间的大小,这个一般都用默认就好


对象分配过程–详细

在这里插入图片描述

当Eden区空间满时,运行GC,将垃圾删除,非垃圾将移入S0区,并将对象的年龄记为1(判断对象进入永久代的参数)
在这里插入图片描述
当Eden区空间再次满时,GC执行,将Eden中的垃圾删除,非垃圾将移入到survivor区中空的区域(S1)
在这里插入图片描述
当Eden区空间再次满时,运行GC,此时在survivor区的对象年龄已经达到15次没有被回收且这一次还是无法被回收,将被移入老年代,其余的对象年龄没有达到这个阈值的将被移入S0(survivor幸存者区),Eden区不能回收的也进入S0。如此反复。
15为阈值,可以用参数设置:-XX:MaxTenuringThreshold=N
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集;

具体一点如下图:
在这里插入图片描述
JvisualVM可查看具体内存信息
比如下面这段程序的内存形态:

/*** -Xms600m -Xmx600m*/
public class HeapInstanceTest {byte[] buffer = new byte[new Random().nextInt(1024*200)];public static void main(String[] args) {ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();while (true){list.add(new HeapInstanceTest());try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}}}
}

在这里插入图片描述

Minor GC、Major GC、Full GC

部分收集:

  • 新生代收集:Minor GC / Young GC,只是新生代的垃圾回收
  • 老年代收集:Major GC / Old GC ,只是老年代的垃圾回收
    • 目前只有CMS GC会有单独收集老年代的行为
    • 很多时候Major GC会和Full GC混合使用,需要具体分析是老年代回收还是整堆回收
  • 混合收集:Mixed GC回收整个新生代以及部分老年代的垃圾
    • 目前只有G1 GC会有这种行为
  • 整堆收集:Full GC回收整个java堆和方法区的垃圾

常用的调优工具
  • JDK命令行
  • Eclipse:Memory Analyzer Tool
  • Jconsole
  • VisualVM
  • Jprofiler
  • Java Flight Recorder
  • GCviewer
  • GC Easy
    自行百度吧😄

TLAB(Thread Local Allocation Buffer)

前言:

  • 堆区是线程共享区域,任何线程都是可以访问到堆区中的共享数据
  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
  • 为了避免多个线程操作同一个地址,需要使用加锁等机制,进而影响分配速度。

从内存模型的角度,JVM在Eden区域为每个线程分配了一个私有缓存区域。
使用TLAB可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,这种方式也称作为“快速分配策略”。
所有OpenJDK衍生出来的JVM,基本都提供了TLAB的设计
默认情况下,TLAB空间的内存比较小,占整个Eden空间的1%,可以通过参数“-XX:TLABWasteTargetPercent”设置占用Eden空间的百分比。
可以用参数“-XX:UseTLAB”设置是否开启TLAB空间,默认开启。
JVM会首选往TLAB内存分配,但一旦在TLAB分配失败,就会直接放入Eden区,并且JVM会尝试通过使用加锁机制确保数据操作的原子性。


测试堆空间常用的参数总结:
  • -XX:+PrintFlagsInitial 查看所有的参数的默认初始值
  • -XX:PrintFlagsFinal 查看所有的参数的最终值(不是初始值,可能存在修改)
  • 查看具体某个参数的指令(CMD):
    • jps:查看当前运行中的进程
    • jinfo -flag SurvivorRadio 进程id(查看其他参数更改Survivor即可)
  • -Xms 初始堆空间内存(默认为物理内存的1/64)
  • -Xmx 最大堆空间内存(默认为物理内存的1/4)
  • -Xmn 设置新生代的大小(初始值及最大值)
  • -XX:NewRatio 配置新生代与老年代在堆结构的占比
  • -XX:SurvivorRatio 设置新生代中Eden和S0/S1空间的比例
  • -XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
  • -XX:+PrintGCDetails 输出详细的GC处理日志
  • 输出简要的GC处理日志: 1⃣️ -XX:+PrintGC , 2⃣️ -verbose:gc
  • -XX:HandlePromotionFailure 是否设置空间分配担保 (JDK7以后这个参数已经不在使用)

官方参数配置查询链接

逃逸分析

问:堆事分配对象存储的唯一选择吗?
答:不是
有一种特殊情况,就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸处方法的话,那么就可能被优化成栈上分配。这样做的好处是无需在堆上分配内存,也无需进行垃圾回收,这就是堆外存储技术。
JDK7以后,hotspot中默认开启逃逸分析,如果用更早的版本可以使用命令:“-XX:+DoEscapeAnalysis”;
-XX:+PrintEscapeAnalysis” 查看逃逸分析的筛选结果。

逃逸分析的基本行为就是分析对象动态作用域
  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,他被外部所引用,则认为发生逃逸。例如:作为调用参数传递到其他方法中。

如:

    public void a(){V v = new V();//...v = null;}

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

public static StringBuffer createSB (){StringBuffer sb = new StringBuffer();sb.append(1);sb.append(2);return sb;}

上面代码发生了逃逸
可以改成:

public static String createSB (){StringBuffer sb = new StringBuffer();sb.append(1);sb.append(2);return sb.toString();}

所以如果开发中使用局部变量,就不要使用在方法外定义。
使用逃逸分析,编译器可以堆代码做如下优化:

  • 栈上分配。将对分配转化为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈分配的候选,而不是堆分配。
  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以访问到,那么对象的部分(或是全部)可以不存储在内存,而是CPU寄存器。
    一个无法再非分解成更小的数据的数据,Java中原始数据类型就是标量。还能分解的称为聚合量,Java对象就是聚合量。
    标量替换的好处:可以大大减少堆内存的占用。一旦不需要创建对象了,那么就不需要分配堆内存了。
    标量替换为栈上分配提供了很好的基础。
    注意:只有启动server模式,才可以启用逃逸分析。参数-server。默认为server模式
    在这里插入图片描述