在本文中,我们将学习如何估计所有可能的 java 对象或原语。这些知识至关重要,特别是对于生产应用。您可能会认为现在大多数服务器都有足够的内存来满足所有可能的应用程序需求。在某种程度上你是对的——硬件,与开发人员的薪水相比,它相当便宜。但是,仍然很容易遇到消耗严重的情况,例如:
- 缓存,尤其是具有长字符串的缓存。
- 具有大量记录的结构(例如,具有从大型XML文件构建的节点的树)。
- 从数据库复制数据的任何结构。
在下一步中,我们开始估计从原始结构到更复杂的结构的 Java 对象。
Java语言
Java Kilomitive 的大小是众所周知的,并且从盒子里提供:
适用于 32 位和 64 位系统的最小内存字
32 位和 64 位的内存字的最小大小分别为 8 个和 16 个字节。任何较小的长度都四舍五入 8。在计算过程中,我们将考虑这两种情况。
由于内存(字大小)结构的性质,任何内存都是 8 的倍数,如果不是,系统将自动添加额外的字节(但对于 8/32 系统,最小大小仍然是 16 和 64 字节)
Java对象
Java 对象内部没有字段,根据规范,它只有称为标头的元数据。标头包含两部分:标记单词和克拉斯指针。
功能目的 | 大小 32 位操作系统 | 大小 64 位 | |
标记单词 | 锁定(同步),垃圾收集器信息,哈希代码(来自本机调用) | 4 字节 | 8 字节 |
克拉斯指针 | 块指针,数组长度(如果对象是数组) | 4 字节 | 4 字节 |
总 | 8 字节(0 字节偏移量) | 16 字节(4 字节偏移量) |
以及它在 Java 内存中的样子:
Java Primitive Wrappers
在Java中,除了原语和引用(最后一个是隐藏的)之外,所有内容都是对象。所以所有的包装类都只包装相应的基元类型。所以包装器大小一般=对象头对象+内部基元字段大小+内存间隙。下表显示了所有基元包装器的大小:
Java数组
Java Array与对象非常相似——它们在基元值和对象值方面也有所不同。数组包含标题、数组长度及其单元格(到基元)或对其单元格的引用(对于对象)。为了澄清起见,让我们绘制一个原始整数和大整数(包装器)的数组。
基元数组(在本例中为整数)
对象数组(在本例中为位整数)
因此,您可以看到基元数组和对象数组之间的主要区别 — 带有引用的附加层。在这个例子中,大多数内存丢失的原因 - 使用一个整数包装器,它增加了12个额外的字节(是原始字节的3倍!
Java类
现在我们知道如何计算 Java 对象、Java 原语和 Java 语言包装器和数组。Java 中的任何类都只不过是一个混合了所有上述组件的对象:
- 标头(32/64 位操作系统为 8 或 12 个字节)。
- 基元(类型字节取决于基元类型)。
- 对象/类/数组(4 字节引用大小)。
Java字符串
Java 字符串是该类的一个很好的例子,所以除了标头和哈希之外,它还将 char 数组封装在里面,所以对于长度为 500 的长字符串,我们有:
但是我们必须考虑到 Java String 类有不同的实现,但一般来说,主要大小由 char 数组保持。
如何以编程方式计算
使用运行时检查大小freeMemory
最简单但不可靠的方法是比较内存初始化前后总内存和可用内存之间的差异:
long beforeUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
Object[] myObjArray = new Object[100_000];
long afterUsedMem=Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
使用 Jol 库
最好的方法是使用Aleksey Shipilev编写的Jol库。该解决方案将使您惊喜地发现,我们可以如此轻松地研究任何对象/基元/数组。为此,您需要添加下一个 Maven 依赖项:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
并输入任何您想估计的东西:ClassLayout.parseInstance
int primitive = 3; // put here any class/object/primitive/array etc
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(primitive).toPrintable());
作为输出,您将看到:
# Running 64-bit HotSpot VM.
# Using compressed oop with 0-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
java.lang.Integer object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x200021de
12 4 int Integer.value 3
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
使用探查器
作为一个选项,您可以使用探查器(JProfiler,VM Visualizer,JConsole等)来观察此结构或其他结构消耗了多少内存。但此解决方案是关于分析内存而不是对象结构。在下一段中,我们将使用 JProfiler 来确认我们的计算是否正确。
创建数据库缓存类并计算其大小
作为一个实际的例子,我们创建类来表示来自某个数据库表的数据,其中有 5 列,每个表中有 1.000.000 条记录。
public class UserCache{
public static void main(String[] args){
User [] cachedUsers = new User[1_000_000];
while(true){}
}
private static class User{
Long id;
String name; //assume 36 characters long
Integer salary;
Double account;
Boolean isActive;
}
}
所以现在我们创建了1M用户,对吧?好吧,不管它在 User 类中是什么 — 我们只是创建了 1M 引用。内存使用量:1M * 4 字节 = 4000 KB 或 4MB。甚至没有开始,但支付了 4MB。
分析 64 位系统的 Java 内存
为了确认我们的计算,我们执行我们的代码并将JProfile附加到它。作为替代方案,您可以使用任何其他分析器,例如VisualVM(它是免费的)。如果您从未分析过您的应用程序,则可以查看本文。下面是 JProfiler 中配置文件屏幕的外观示例(这只是一个与我们的实现无关的示例)。
提示:分析应用时,可以不时运行 GC 来清理未使用的对象。所以分析的结果:我们有参考指向4M记录,大小为4000KB。当我们剖析时User[]
下一步,我们初始化对象并将它们添加到我们的数组中(名称是唯一的 UUID 36 长度大小):
for(int i = 0;i<1_000_000;i++){
User tempUser = new User();
tempUser.id = (long)i;
tempUser.name = UUID.randomUUID().toString();
tempUser.salary = (int)i;
tempUser.account = (double) i;
tempUser.isActive = Boolean.FALSE;
cachedUsers[i] = tempUser;
}
现在让我们分析这个应用程序并确认我们的期望。您可能会提到某些值不精确,例如,字符串的大小是 24.224 而不是 24.000,但我们计算所有字符串,包括内部 JVM 字符串和与对象相关的相同字符串(估计为 16 字节,但在配置文件中,显然是 32,因为 JVM 内部也使用)。Boolean.FALSEBoolean.TRUE
对于 1M 条记录,我们花费 212MB,它只有 5 个字段,所有字符串长度都受到 36 个字符的限制。所以正如你所看到的,对象是非常贪婪的。让我们改进 User 对象并用原语替换所有对象(字符串除外)。
仅通过将字段更改为基元,我们就节省了 56MB(约占已用内存的 25%)。此外,我们还通过删除用户和基元之间的其他引用来提高性能。
如何减少内存消耗
让我们列出一些节省内存消耗的简单方法:
压缩的 OOP
对于 64 位系统,您可以使用压缩的 oop 参数执行 JVM。
有兴趣大家可以去学习下。
将数据从子对象提取到父对象
如果设计允许将字段从子类移动到父类,则可能会节省一些内存:
具有基元的集合
从前面的示例中,我们看到了基元包装器如何浪费大量内存。原始数组不像Java集合接口那样用户友好。但还有另一种选择:Trove、FastUtils、Eclipse Collection 等。让我们比较一下simpleand 来自 Trove 库的内存使用情况。ArrayList<Double>TDoubleArrayList
TDoubleArrayList arrayList = new TDoubleArrayList(1_000_000);
List<Double> doubles = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
arrayList.add(i);
doubles.add((double) i);
}
通常,关键区别隐藏在双基元包装器对象中,而不是在 ArrayList 或 TDoubleArrayList 结构中。因此,简化 1M 记录的差异:
JProfiler证实了这一点:
因此,只需更改集合,我们就可以轻松地在 3 倍内减少消耗。
版权声明:内容来源于互联网和用户投稿 如有侵权请联系删除