数据封装是面向对象编程的基本属性之一。对象是逻辑上“一起”的几个数据的最自然的表示。类及其实例可以根据需要包含任意数量的数据字段。如果一个类有大量的数据字段,那么它可能很难读取、维护等。但是你有没有考虑过用很少的数据字段编写Java类然后创建大量实例?
在Java中,内存中的对象不是“虚”的。也就是说,除了“payload”(在相应类及其超类中声明的数据字段)之外,每个对象都使用一些额外的内存。下面的图表说明了Oracle JDK附带的HotSpot JVM中的对象结构(该对象具有数据字段 Object foo; int bar 等):
每个对象都有一个标题,其中包含指向对象类的指针以及一些称为标记的额外字节。需要类指针,以便:
(a)可以在运行时调度虚方法。
(b)垃圾收集器在扫描堆时可以确定指向给定对象内其他对象的指针的位置。
垃圾收集器使用标记字来存储一些簿记信息,也可以通过同步机制(回想一下,在Java中,任何对象都可以用作synchronized块中的参数)。每个标题部分的大小可以在4到8个字节之间变化,具体取决于最大堆大小。值得记住的是,在最常见的情况下(-Xmx低于32GB),对象头大小为12个字节,更大的是16个字节。想学习交流HashMap,nginx、dubbo、Spring MVC,分布式、高性能高可用、MySQL,redis、jvm、多线程、netty、kafka、的加尉(同英):1253431195 扩列获取资料学习,无工作经验不要加哦!
还有,每个对象开销的另一个来源是对象对齐(填充),这意味着在没有特殊JVM标志的情况下,内存中的每个对象都从一个与8成比例的地址开始,如下图所示:
因此,每个对象的有效大小与8个字节成比例。在最坏的情况下,只有最小4字节有效负载的对象(例如实例java.lang.Integer)需要16个字节,而具有8字节有效负载(java.lang.Long)的对象需要24个字节!这是3.4倍的内存开销。实际上,通常会看到未经优化的Java应用程序因为每个对象的开销而浪费了20…40%的内存。此外,大量且活得足够长的小对象不仅浪费内存,还对垃圾收集器施加压力,因为它必须在每次迭代时扫描并移动更多的对象。
如何解决这个问题?目前并没有统一的方法。您需要了解哪些对象会导致显著的内存开销,然后寻找特定的解决方案。下面,我们概述了几种最常见的情况以及解决这些问题的可能方法。
拆箱
当表示数字的对象占用大量内存时,通常的解决方案就是不使用它们。例如,Integer 不是普通的 int。一个合法的情况是,当我们需要能够发信号通知该字段是“未初始化”或“未定义”时使用 null。当存在多个这样的字段等时,您可以使用单独的布尔标志或位向量。
另一个常见原因是当您需要数据结构(如散列图或包含数字的列表)时。例如 java.util.HashMap<Integer, String>,你可以使用一个专门的第三方 Int2ObjectHashMap等。想学习交流HashMap,nginx、dubbo、Spring MVC,分布式、高性能高可用、MySQL,redis、jvm、多线程、netty、kafka、的加尉(同英):1253431195 扩列获取资料学习,无工作经验不要加哦!
包装
包装类的实例。这通常是为了提供对后者中包含的数据的访问,具有一些限制或副作用(例如,记录对象内容的每个修改)。一个示例包装类 java.util.Collections.UnmodifiableList,它是Collections.unmodifiableList(List) 方法返回的非公共类。大多数包装类除了指向包装对象的字段外几乎没有其他字段,在许多情况下,应用程序只使用一些包装器的方法。因此,如果在某个包装类的应用程序实例中占用大量内存,则可能需要更改代码以直接引用包装对象并以不同方式提供包装器功能。
集合实现类
例如,java.util.HashMap描绘了内部实现(注意,这也分别涉及 java.util.HashSet和java.util.LinkedHashMap的封装和扩展)。在所有这些集合中,添加键值对会导致分配java.util.HashMap N o d e 类 的 实 例 ( Node类的实例( Node类的实例(Entry在JDK 7及更早版本中调用 )。Node对象有两个目的:它存储了键,以加快键查找的哈希码,它支持碰撞链中的链接列表的形式(或二进制搜索树从JDK 8开始)。在一般情况下,会加快 get()和 put()操作。然而,当应用程序严重依赖哈希映射或哈希集时,这些对象每个占用32个字节,也可能会造成很多内存浪费。为了消除这种浪费,可考虑改用另一种开放的HashMap实现,不使用内部的“节点”对象,例如,Object2ObjectOpenHashMap。
只引用一个类
例如:
如果类 Bar只被类引用 Foo,并且实例 Bar占用了大量内存。那么可以考虑通过方法来提供Bar对象。当然,这样可能让代码变得更长并且可能更不易维护,但是可以节省内存。
可以表示为普通数字表的对象
这对于小型固定大小的数组尤其重要,但它也可以应用于仅具有或主要是数字数据字段的类。当在运行时仅添加这些对象但从未删除时,最有效。请考虑以下代码:
为避免 每个对象保留一个 byte[]数组和一个 Message实例 Event,我们可以将其转换为:
检测浪费内存的小对象
如何判断您的应用程序是否因小对象而浪费内存?最好的方法是生成应用程序的堆转储,然后使用工具对其进行分析。堆转储本质上是正在运行的JVM堆的完整快照。它可以通过调用jmap实用程序在任意时刻进行 ,也可以将JVM配置为在失败时自动生成 OutOfMemoryError。想学习交流HashMap,nginx、dubbo、Spring MVC,分布式、高性能高可用、MySQL,redis、jvm、多线程、netty、kafka、的加尉(同英):1253431195 扩列获取资料学习,无工作经验不要加哦!
堆转储是一个大小与JVM堆大小相同的二进制文件,因此只能使用特殊工具读取和分析它。有许多这样的工具,包括开源和商业的。最流行的开源工具是Eclipse MAT;还有VisualVM和一些其他的工具。商业工具包括通用Java分析器:JProfiler和YourKit,以及专门为堆转储分析构建的一个工具,称为 JXRay。
与大多数其他工具不同,JXRay立即分析堆转储以解决大量常见问题,例如重复字符串和其他对象,次优数据结构以及“固定的对象开销”,这就是我们上面讨论的内容。该工具生成一个报告,其中包含HTML格式的所有收集信息。这种方法的优点是,您可以随时随地查看分析结果,并轻松与他人分享。
从JXRay获得报告后,在浏览器中打开它并展开相关部分,示例如下:
因此,在此转储中,由于对象标题和对齐,浪费了高达38.5%的已使用堆。上表列出了其实例对此开销有显着贡献的所有类。要查看这些对象的来源(哪些对象引用它们,一直到GC根目录),请转到报告的“按内存去的地方”一节,展开它,然后单击表中的行。 。以下是上述类之一的示例
总结
总而言之,许多小对象可能成为Java应用程序的负担,因为JVM本身强加的固定对象开销与其“有效负载”相当。结果,可能浪费大量存储器,并且可以观察到高GC压力。衡量此类对象影响的最佳方法是获取堆转储并使用JXRay之类的工具对其进行分析。