直接分配给老年代的巨型对象的大小

20

最近我一直在阅读Java中不同代的对象分配问题。大多数情况下,新对象会被分配到Eden(年轻代的一部分),并且只有满足以下任何一个条件时,它们才会晋升到老年代。

(1) 对象的年龄达到了终身阈值
(2) 存活区空间已经满了,在从Eden(或)其他存活区域复制对象时

但是还有一种特殊情况,即直接将对象分配到老年代而不是从年轻代晋升。这种情况发生在我们尝试创建的对象非常庞大(可能是几MB量级)的情况下。


有没有办法知道巨大/庞大对象的大小/限制?我知道G1垃圾收集器的庞大对象标准,但我想知道在Java 6之前的大小限制。

谢谢你的时间 :)


请记住,GC代码可能也会对对象类型敏感。有些类通常被认为是长寿的,因此更容易直接进入老年代状态。 - Hot Licks
@HotLicks.. 你能否对这些长寿类更具体一些.. 一个小例子就可以了 :) - Arkantos
@Arkantos - 我已经有5年多没有在JVM上工作了,而且与GC的讨论也更久远了,所以目前我脑海中没有任何想法。我认为一些内部JVM类属于这个范畴——与进程和文件相关的东西——但我没有具体的例子,而且多年来事物的变化不可预测。我没有听到讨论过的一件事是对类进行分析,并确定哪些类适合在给定应用程序中长期存在。 - Hot Licks
但是一个普遍的观点是,人们不应该基于单一标准就认为关于终身雇用的决定是非黑即白的。 JVM 可能有多个标准,并且这些算法可能会随着 GC 开发人员的心血来潮而改变。 - Hot Licks
另一个点(稍微不太一般)是,将巨大对象直接分配到老年代空间的原因在于伊甸园空间(按设计)相对大小有限,将巨大对象分配到那里会导致过多的 GC 频率。 - Hot Licks
3个回答

29

HotSpot JVM在young generation中可以分配的对象最大大小几乎与Eden(YoungGen减去两个Survivor空间)的大小相同。

这是大致的分配过程:

  1. 如果tlab_top + size <= tlab_end,则使用线程本地分配缓冲区(TLAB)。
    这是最快的路径。分配仅涉及将tlab_top指针增加。
  2. 如果TLAB几乎已满,则在Eden中创建一个新的TLAB,并在新的TLAB中重试。
  3. 如果TLAB剩余空间不足但仍然太大而不能丢弃,则尝试直接在Eden中分配对象。因为Eden在所有线程之间共享,所以在Eden中进行分配也是一个指针增量(eden_top + size <= eden_end),使用原子操作。
  4. 如果在Eden中分配失败,则通常会进行一次小型垃圾收集。
  5. 如果即使进行Young GC后Eden中没有足够的空间,则尝试直接在老年代中分配。

@JigarJoshi 这可能是很久以前的遗留问题。在我们现在的时代,对每个对象分配执行额外的参数检查会过度杀伤力。 - apangin
听起来像是文档中的一个错误,感谢您的详细解释。 - jmj
谢谢你提供如此简单优雅的答案。因为保持简单,我给你点赞 :) - Arkantos
@apangin.. 我在这里添加了支持你的类比的答案.. 如果有需要添加的内容,请告诉我。 - Arkantos
@Arkantos 没问题。谢谢。 - apangin

7
您可以使用以下标志设置限制:
XX:PretenureSizeThreshold=size

默认值为0,我认为如果你不设置它,则默认情况下不会考虑值为0,这意味着默认情况下没有最大值作为阈值,对象仅基于GC存活数量进行晋升。

HotSpot版本

java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

要获取所有支持的虚拟机选项,您可以运行以下命令:

java -XX:+PrintVMOptions -XX:+AggressiveOpts -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal  -version

然后您可以参考热点VM选项文档,如果没有列出特定选项,则可以在谷歌上搜索。


byte[] array = new byte[300*1024*1024];

for(MemoryPoolMXBean memoryPoolMXBean: ManagementFactory.getMemoryPoolMXBeans()){
    System.out.println(memoryPoolMXBean.getName());
    System.out.println(memoryPoolMXBean.getUsage().getUsed());
}

输出:

$ java -Xmx1500m -Xms1500m -Xmn500m -XX:PretenureSizeThreshold=100000000 -XX:+PrintGCDetails JVMMemoryInspection
Code Cache
393664
PS Eden Space
330301752
PS Survivor Space
0
PS Old Gen
0
PS Perm Gen
2749520

太棒了 :) 这正是我在寻找的。正如你所提到的,0是默认值。我在我的笔记本电脑上通过-XX:+PrintFlagsFinal检查了该值,它是0,我甚至尝试使用jinfo运行示例程序,但仍然是0。让我在实际的应用服务器框中尝试一下,并回复您。也许在长时间运行的应用程序中,JVM使用人体工程学可能已经改变了这个值。 - Arkantos
是的,可能会因为服务器级别机器的人体工程学而发生变化。 - jmj
同时,如果您可以提供一些有关这些JVM标志的实际含义/作用的文档资源/链接,那将会是非常省时的:) 再次感谢。 - Arkantos
1
除非在命令行中明确指定,否则不会使用“PretenureSizeThreshold”。但即使设置了它,在慢速分配路径中也只会进行检查。JIT编译的代码首先尝试在TLAB或Eden中分配,然后才会转向慢速路径。 - apangin
1
测试很简单: byte[] array = new byte[300*1024*1024]; 使用 -Xmx1500m -Xms1500m -Xmn500m -XX:PretenureSizeThreshold=200000000 -XX:+PrintGCDetails 运行Java程序,看会发生什么。基本上,PretenureSizeThreshold 会被忽略,对象将分配在 Eden 区。如果将 -Xmn 设置为 400m,则数组将无法放入 Eden 区,并直接分配到旧生代。 - apangin
显示剩余2条评论

5
JVM标志:
-Xms1G -Xmx1G -Xmn500m -XX:PretenureSizeThreshold=100000000 -XX:+PrintGCDetails
将年轻代的大小固定为500MB,eden大约占384MB,所有大于384MB的对象直接进入OldGen,小于384MB的对象则分配在Eden中。您可以在下面找到代使用情况。
byte [] array = new byte [400 * 1024 * 1024];
PSYoungGen      total 448000K, used 30720K  
    eden space 384000K, 8% used  
    from space 64000K, 0% used  
    to   space 64000K, 0% used      
 ParOldGen       total 536576K, used 409600K  
   object space 536576K, 76% used 

byte[] array = new byte[300*1024*1024];

 PSYoungGen      total 448000K, used 337920K  
  eden space 384000K, 88% used  
  from space 64000K, 0% used  
  to   space 64000K, 0% used  
 ParOldGen       total 536576K, used 0K 
  object space 536576K, **0% used** 

对于400MB的分配,eden使用率为8%,而老年代使用率为76%。 对于300MB的分配,eden使用率为88%,而老年代使用率为0%。 因此,所有大小大于eden的对象将直接分配到老年代。
感谢apangin和Jigar提供宝贵的见解 :) 我认为-XX:PretenureSizeThreshold根本没有被考虑。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接