1 | public class JvmGcTest { |
堆的组成
1 | public class JvmGcTest_1 { |
1 | Heap |
堆的组成
-
+
Heap
: 堆。def new generation
: 新生代。tenured generation
: 老年代。
-Metaspace
: 元空间,并不属于堆,-XX:+PrintGCDetails
将它的信息一起输出。
+Metaspace
: 元空间,实际上并不属于堆,-XX:+PrintGCDetails
将它的信息一起输出。
- GC: minor GC。
- Fulle GC: full GC。
- 正常情况下,新对象都是在 eden 中创建。 +
- 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。 +
- 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。 +
- 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。 +
- 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。 +
根据打印的信息,组成如下:
+空间占比
新生代中的空间占比 eden:from:to
在默认情况下是 8:1:1
,与观察到的数据 8192K:1024K:1024K
一致。
新生代的空间 eden + from + to
为 10240K,符合 -Xmn10M
设置的大小。total
显示为 9216K,即 eden + from
的大小,是因为 to
的空间不计算在内。新生代可用的空间只有 eden + from
,to
空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden
中已用 2010K,约占 eden
空间的 24%。
从地址指针分析空间
地址指针为 16 位的 16 进制的数字,64 位机器。[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000)
,老年代 [0x00000000ff600000, 0x0000000100000000)
,计算可得空间大小均为 10MB。eden
中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8)
,空间大小为 2058440 byte,约等于 2010K。
堆空间的比例
新生代中的空间占比 eden:from:to
在默认情况下是 8:1:1
,与观察到的数据 8192K:1024K:1024K
一致。
新生代的空间 eden + from + to
为 10240K,符合 -Xmn10M
设置的大小。total
显示为 9216K,即 eden + from
的大小,是因为 to
的空间不计算在内。新生代可用的空间只有 eden + from
,to
空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden
中已用 2010K,约占 eden
空间的 24%。
从内存地址分析堆空间
内存地址为 16 位的 16 进制的数字,64 位机器。[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000)
,老年代 [0x00000000ff600000, 0x0000000100000000)
,计算可得空间大小均为 10MB。eden
中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8)
,空间大小为 2058440 byte,约等于 2010K。
显而易见,新生代和老生代是一片完全连续的地址空间。
-1 | public static void main(String[] args) { |
堆的垃圾回收
1 | public static void main(String[] args) { |
1 | [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
GC 类型
-
+
Allocation Failure
,正常情况下,新对象总是分配在 Eden,分配空间失败,eden
的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
。[DefNew: 2013K->721K(9216K), 0.0105099 secs]
,新生代在垃圾回收前后空间的占用变化和耗时。2013K->721K(19456K), 0.0105455 secs
,整个堆在垃圾回收前后空间的占用变化和耗时。
GC 类型
Allocation Failure
,分配空间失败,eden
的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
。[DefNew: 2013K->721K(9216K), 0.0105099 secs]
,新生代在垃圾回收前后空间的占用变化和耗时。2013K->721K(19456K), 0.0105455 secs
,整个堆在垃圾回收前后空间的占用变化和耗时。
from 和 to 的角色变换
from
的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0)
,空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。可见在垃圾回收后,eden
区域存活的对象全部转移到了原 to
空间,from
和 to
空间的角色相互转换(从地址空间的信息可以看到此时 to
的地址指针比 from
的地址指针小)。eden
的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0)
,空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden
区域除了 byte[]
对象外,还存储了其他对象,比如为了创建 List<byte[]>
和 byte[]
对象而新加载的类对象。
1 | public static void main(String[] args) { |
from 和 to 的角色变换
from
的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0)
,空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden
区域存活的对象全部转移到了原 to
空间,from
和 to
空间的角色相互转换(从地址空间的信息可以看到此时 to
的地址指针比 from
的地址指针小)。eden
的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0)
,空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden
区域除了 byte[]
对象外,还存储了其他对象,比如为了创建 List<byte[]>
对象而新加载的类对象。
eden 空间足够时不发生 GC
1 | public static void main(String[] args) { |
1 | [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
eden 空间足够时不发生 GC
由于 eden
区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。eden
区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0)
,空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16
。显然第二次添加时,不再因为创建 List<byte[]>
和 byte[]
对象而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。
1 | public static void main(String[] args) { |
由于 eden
区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。eden
区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0)
,空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16
。显然第二次添加时,不再因为创建 List<byte[]>
而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh。
新生代空间不足,部分对象提前晋升到老年代
1 | public static void main(String[] args) { |
1 | [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
新生代空间不足,部分对象提前晋升到老年代
在第三次添加时,由于 eden
空间不足,因此又发生了第二次垃圾回收。[DefNew: 8565K->512K(9216K), 0.0046378 secs]
,新生代的空间占用下降到了 512K。
在第二次添加完成后,eden
[0x00000000fec00000, 0x00000000ff3bd8d0)
和 from
[0x00000000ff500000, 0x00000000ff5b45f0)
占用的空间为 8116432 + 738800 = 8855232
约 8647.7K,略大于 8565K,在垃圾回收后新生代剩下 512K,应该是在 from 中留下了第二次添加时的 512K。8565K->8396K(19456K), 0.0046540 secs
,堆的占用空间并未发生明显下降,部分对象是因为新生代空间不足,提前晋升到了老年代中。
优先将大对象先晋升到老年代应该是一种晋升策略。eden
中加入了第三次添加时的对象,但远大于 512K。此时 eden
、from
、tenured
中均有不好确认成分的空间占用。
1 | public static void main(String[] args) { |
在第三次添加时,由于 eden
空间不足,因此又发生了第二次垃圾回收。[DefNew: 8565K->512K(9216K), 0.0046378 secs]
,新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
在第二次添加完成后,eden
[0x00000000fec00000, 0x00000000ff3bd8d0)
和 from
[0x00000000ff500000, 0x00000000ff5b45f0)
占用的空间为 8116432 + 738800 = 8855232
约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。8565K->8396K(19456K), 0.0046540 secs
,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。eden
中加入了第三次添加时的对象,大于 512K 不少。
此时 eden
、from
、tenured
中均有不好确认成分的空间占用,比如 from 中多了 56 字节。
新生代空间不足,大对象直接在老年代创建
1 | public static void main(String[] args) { |
1 | Heap |
在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。
+内存不足 OOM
1 | public static void main(String[] args) { |
新生代空间不足,大对象直接在老年代创建
在新生代的空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。
-1 | public static void main(String[] args) { |
1 | [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
1 | waiting... |
大对象的划分指标
当创建的大对象 + 对象头的容量小于等于 eden
,如果 GC 后的存活对象可以放入 to
,那么还是会先在 eden
中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。
1 | public static void main(String[] args) { |
当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError
。
线程中发生内存不足,不会影响其他线程
1 | public static void main(String[] args) { |
1 | [GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
当 Thread-0
发生 OutOfMemoryError
后,main
线程仍然正常运行。
大对象的划分指标
当创建的大对象 + 对象头的容量小于等于 eden
,如果 GC 后的存活对象可以放入 to
,那么还是会先在 eden
中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。
1 | public static void main(String[] args) { |
内存不足
当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError
。
1 | public static void main(String[] args) { |
1 | waiting... |
1 | [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
线程中发生内存不足,不会影响其他线程
当 Thread-0
发生 OutOfMemoryError
后,main
线程仍然正常运行。
尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。
+注意事项
-
+
尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。