JVM内存-堆空间 详解

前言

在我的上一篇博客 深入理解JVM-JVM内存模型概述中对于堆空间介绍如下

堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
所有的对象实例以及数组都应当在运行时分配在堆上。
堆内存主要分为 年轻代和老年代,年轻代分为 eden区和Survivor0区和Survivor1区。

堆内存结构细分

如图:
在这里插入图片描述

左侧 为 年轻代(Young Generation Space) 右侧为 老年代 (Old Generation Space)。
年轻代又分为 Eden区和Survivor区 (from /to 即 Survivor0和Survivor1 区)。
注:在逻辑上 元空间(方法区)是属于堆的,实际上堆和方法区是独立的内存结构。

堆空间的分区比例默认值如下

  • Eden:From:to -> 8:1:1
  • 新生代:老年代 - > 1 : 2

下面为 运行 java 程序时, jvm打印的 GC日志 ,由图中的信息可知堆的内存空间划分即为如上所说:
在这里插入图片描述
可以通过在jvm 运行配置中增加 -XX:+PrintGCDetails , 使JVM打印GC日志信息。

堆内存大小设置

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。

  • “-Xms"用于表示堆区的起始内存,等价于-xx:InitialHeapSize
  • “-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize

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

通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在ava垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

默认情况下

  • 初始内存大小:物理电脑内存大小/64

  • 最大内存大小:物理电脑内存大小/4

可以通过下面代码试验参数的是否有效

/**
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *  -X:是jvm运行参数
 *  ms:memory start
 * -Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * @author: dcette
 * @create: 2020-08-06-20:44
 */
public class HeapSpaceInitial {
    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");
    }
}

堆空间分代思想

为什么要把Java堆分代?不分代就不能正常工作了吗?
经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。

新生代:有Eden、两块大小相同的survivor(又称为from/to,s0/s1)构成,to总为空。
老年代:存放新生代中经历多次GC仍然存活的对象。

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

内存分配策略

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

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

  • 优先分配到Eden
    • 开发中比较长的字符串或者数组,会直接存在老年代,但是因为新创建的对象 都是 朝生夕死的,所以这个大对象可能也很快被回收,但是因为老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断
    • 如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold 中要求的年龄。

空间分配担保:-xx:HandlePromotionFailure

  • 也就是经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。

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

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

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

小结:堆空间的参数设置

  • -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:HandlePromotionFalilure:是否设置空间分配担保
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值