深入理解Java虚拟机——堆内存的结构分析

  • 1. 先简单介绍几个常用的 jvm 参数
  • 1.1 设置堆空间大小的参数
  • 1.1.1 -Xms (-XX:InitalHeapSize)
  • 1.1.2 -Xmx (-XX:MaxHeapSize)
  • 1.1.3 -Xmn、-XX:NewSize
  • 1.1.4 -XX:MaxNewSize
  • 1.1.5 -XX:+PrintGCDetails(jstat -gc pid)
  • 1.2 默认堆空间的大小
  • 1.2.1 验证初始结果
  • 1.3 -Xms 与 -Xmx 参数大小设置
  • 1.3.1 官方建议如何设置
  • 1.3.2 手动修改默认设置举例 + 内存占用情况分析
  • 2. Java 堆 + 内存分配
  • 2.1 Java堆概述 + Java堆内存为什么这么划分?
  • 2.1.1 垃圾收集算法(简单说)
  • 2.1.1.1 分代收集理论
  • 2.1.1.2 简单说垃圾收集
  • 2.1.1.3 标记——复制算法
  • 2.1.2 再次理解1.3.2 例子中的内存占用情况
  • 2.2 内存分配与回收策略
  • 2.2.1 对象优先在 Eden 分配
  • 3. 参考书籍


1. 先简单介绍几个常用的 jvm 参数

1.1 设置堆空间大小的参数

  • 下面的 -X 是 jvm 的运行参数,msmemory start 的缩写

1.1.1 -Xms (-XX:InitalHeapSize)

  • -Xms:设置堆空间(年轻代+老年代)的初始内存大小,即:堆最小空间
    具体语法以及值得设置原则,咱看官网怎么说:
  • java堆空间不释放 java堆空间结构_Java

  • -XX:InitalHeapSize
    -XX:InitalHeapSize 选项也可以用来设置初始堆大小。如果它出现在命令行中的-Xms之后,那么初始堆大小将被设置为-XX:InitalHeapSize指定的值。

1.1.2 -Xmx (-XX:MaxHeapSize)

  • -Xmx:设置堆空间(年轻代+老年代)的最大内存大小,即:堆最大空间
    下面的例子展示了如何使用不同的单位将最大允许分配的内存大小设置为80 MB:
-Xmx83886080

-Xmx81920k

-Xmx80m

-Xmx选项等价于-XX:MaxHeapSize

  • - xx: MaxHeapSize

1.1.3 -Xmn、-XX:NewSize

  • -Xmn 或者 -XX:NewSize :年轻代堆的初始大小设置
    -Xmnsize
-Xmn256m
-Xmn262144k
-Xmn268435456
  • -XX:NewSize=size

1.1.4 -XX:MaxNewSize

-XX:MaxNewSize=size

设置年轻代(托儿所)的堆的最大大小(以字节为单位)。默认值是按照人体工程学设置的(The default value is set ergonomically)。

1.1.5 -XX:+PrintGCDetails(jstat -gc pid)

  • 收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志(即:在每次GC中启用打印详细消息),并且在进程退出的时候输出当前的内存各区域分配情况。默认情况下,该选项是禁用的。
  • 也可以在命令行获取内存使用情况
jsp
jstat -gc pid

1.2 默认堆空间的大小

  • 初始堆大小为物理内存的1/64,最高为1gb,最大堆大小为物理内存的1/4,最多为1gb
  • 这可不是我随便说的,官方说的,来看看官方怎么说吧,信谁不如新官网:

1.2.1 验证初始结果

  • 首先,电脑–>属性,查看电脑内存

1.3 -Xms 与 -Xmx 参数大小设置

1.3.1 官方建议如何设置

  • 一般情况下,我们是将 -Xms 与 -Xmx 两个参数配置相同的值,为什么设置相同的值呢?
    如果初始堆内存和最大堆内存设置的值不一样,则会出现频繁扩容堆空间的情况(假如堆空间的使用超过初始值小于最大值),这样GC的也就很频繁,频繁的扩容和释放会给系统带来很大的压力,所以一般情况下设置成一样大,避免GC的次数(GC很耗费用户线程的占用),提高程序性能
  • 不妨来看看官方咋说:
  • 也就是说,目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能

1.3.2 手动修改默认设置举例 + 内存占用情况分析

  • ① 先设置参数 -Xms100m -Xmx100m
    欸,为什么不一样呢,我们来分析一下:
  • ② 分析程序运行时的内存使用情况
    使用命令 jstat -gc pid 查看某一进程的内存使用情况
  • ③ 如果想知道为什么,看到 2.1.2 的时候你就明白了,请继续,往下看自会有答案!

2. Java 堆 + 内存分配

2.1 Java堆概述 + Java堆内存为什么这么划分?

  • 接下来我们根据周大大的《深入理解Java虚拟机》来了解一下Java堆相关的知识(所以大家有时间可以研读一下:周志明的《深入理解Java虚拟机》)
  • 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配[插图]”,而这里笔者写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换[插图]优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
  • 对于Java堆内存的划分,如下:

    那么,为什么这么划分,如果你之前没用了解过肯定很好奇,如果你之前了解过但是了解的少,可能也只是知其然但不知其所以然,那就不妨继续往下探究吧:

2.1.1 垃圾收集算法(简单说)

2.1.1.1 分代收集理论
  • 在介绍新生代和老年代之前,我们根据《深入理解Java虚拟机》先来了解一下分代收集理论
    当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(GenerationalCollection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
    1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
    2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
    这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域(好比老年代,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
  • 所以,设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
2.1.1.2 简单说垃圾收集
  • 基于上面所说,在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域——因而才有了“Minor GC”、“Major GC”、“Full GC”这样的回收类型的划分;也才能够针对不同的区域安排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。
  • 那么具体怎么回收的,我们还需要了解一下标记-复制算法,再坚持坚持,曙光就在眼前了
2.1.1.3 标记——复制算法
  • HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了“Appel式回收”(一种更优化的半区复制分代策略)这种策略来设计新生代的内存布局。
  • Appel式回收的具体做法是把新生代分为一块较大的Eden空间 和 两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间
  • HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的
  • 当然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)
  • 内存的分配担保就是,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的。

2.1.2 再次理解1.3.2 例子中的内存占用情况

  • 看到这里了,现在应该对这个为什么很清楚了吧
  • 对这个内存的使用情况,应该也清晰了吧
  • 原因就是:
    ① S1 区 和 S2区 各占 4096/1024 = 4M
    ② 根据我们对新生代的垃圾回收算法(标记——复制算法)可知,每次分配内存只使用Eden和其中一块Survivor,即S0区和S1区始终有一个是空的,空着的那个用来垃圾回收时存储仍然存活的对象(即:将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上)。
    ③ 所以,分配的内存为:
    100M - 4M(其中一个Survivor区的) = 96M

2.2 内存分配与回收策略

  • 在经典分代的设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。对象分配的规则并不是固定的,《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪一种垃圾收集器,以及虚拟机中与内存相关的参数的设定。

2.2.1 对象优先在 Eden 分配

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。HotSpot虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数。
  • 如果细心的话,其实上面通过 jstat -gc pid 命名,我们已经看出优先分配的是伊甸园区
  • 我们不妨也试试-XX:+PrintGCDetails这个参数: