长沙营销网站建站公司,商城推广方案,99设计网站,正规的抖音推广平台当Java应用程序消耗大量内存时#xff0c;它本身就会出现问题#xff0c;并可能导致GC压力增加和GC暂停时间过长。在我之前的一篇文章中#xff0c;我讨论了Java中常见的内存浪费源#xff1a;重复字符串。两个 java.lang.String 对象#xff0c; a 并 b 在重复时 a ! b 它本身就会出现问题并可能导致GC压力增加和GC暂停时间过长。在我之前的一篇文章中我讨论了Java中常见的内存浪费源重复字符串。两个 java.lang.String 对象 a 并 b 在重复时 a ! b a.equals(b)。换句话说在JVM存储器中有两个或更多单独的字符串具有相同的内容。此问题经常发生尤其是在业务应用程序中。在这样的应用程序中字符串代表了许多真实世界的数据然而相应的数据域例如客户名称国家名称产品名称是有限的并且通常很小。根据我的经验在未经优化的Java应用程序中重复的字符串通常会浪费5到30的堆。但是你有没有想过其他类的实例包括数组有时也会重复浪费了相当多的内存如果没有请继续阅读。对象复制方案只要某种类型的不同对象的数量有限但应用程序不断创建此类对象而不尝试缓存/重用现有对象就会发生内存中的对象复制。以下是我看到的对象复制的几个具体示例在Hadoop文件系统HDFSNameServer中 byte[] 数组而不是 Strings用于存储文件名。当在不同目录中存在具有相同名称的文件时相应的 byte[] 阵列是重复的。有关 详细信息请参阅此票证。在一些监视系统中从被监视实体机器应用程序组件等接收的周期性“事件”或“更新”被表示为具有两个主要字段的小对象时间戳和值。当许多更新同时到达并且所有更新都具有相同的值例如0或1表示被监视实体的健康状况良好时会创建许多重复对象。该 蜂房数据仓库曾经有过以下问题。当针对具有大量分区的同一DB表执行多个并发查询时每个分区的元数据的单独的每个查询副本被加载到内存中。分区元数据表示为 java.util.Properties 实例。因此针对2000个分区运行50个并发查询Properties 用于为这些分区中的每个分区创建50个相同的副本 或者总共100,000个这样的集合这消耗了大量内存。有关详细信息请参阅此票证。这只是几个例子。其他不太明显的包括存储相同消息的多个相同字节缓冲区具有表示某些频繁出现的数据组合的相同内容的多个通常是小的对象集等等。 摆脱重复的对象如上所述字符串是一类特别容易重复的对象。很久以前JDK开发人员已经实现了这个问题并使用String.intern方法解决了这个问题。上面提到的文章详细讨论了它。简而言之此方法使用具有有效弱引用的全局字符串缓存池。如果它还没有在缓存中它会保存并返回给定的字符串实例或者返回具有相同值的缓存字符串实例。String.intern() 曾经不是很好的 性能和可扩展性 从JDK 7开始大幅改进。因此当没有过度使用时它可能是许多应用程序的良好解决方案。然而在讨论这篇文章在高度并发或性能关键的应用程序中它可能成为瓶颈可能需要一种不同的“手动”实习方法。让我们考虑其他对象实习。请记住以下讨论仅适用于不可变对象即在创建后不会更改的对象。如果对象内容可以改变消除重复变得更加困难并且需要定制的逐案解决方案。最广泛使用的现成实习功能由Guava库通过com.google.commmon.collect.Interners类提供。此类有两个返回内部实例的关键方法 newStrongInterner() 和 newWeakInterner()。弱内部函数最终会释放不再需要的对象未在任何地方强烈引用通常使用较少的内存因此更频繁地使用。它被实现为具有类似于标准JDK的弱键的并发哈希集 ConcurrentHashMap。在许多情况下这是一个很好的选择有助于通过较小的CPU性能开销大幅减少内存占用这通常会减少GC时间。但是请考虑以下情况一些C类有2000万个实例每个实例占32个字节其中1000万个实例彼此完全相同另外1000万个实例都是截然不同的。在实践中这种尖锐的划分几乎从未发生过但是大约一半的物体仅表示少数独特的值而在另一半中大多数物体很少或没有重复这种情况非常常见。简化的划分使我们的计算更容易。在这种情况下当我们不实习任何C实例时它们使用32 * 20M 640M字节。但是如果我们intern() 为每个人调用番石榴会发生什么 呢前1000万个对象将成功减少到只有一个C实例占用的内存可以忽略不计。但是对于剩下的1000万个对象中的每一个都没有节省因为它们中的每一个都是唯一的。尽管如此Guava弱内部人员将维持一个内部表格其中包含1000万个条目以容纳这些对象中的每一个。该表将使用com.google.common.collect.MapMakerInternalMap$WeakKeyDummyValueEntry 每个实习对象的一个类实例 以及引用每个条目的内部数组中的一个插槽。a的大小 WeakKeyDummyValueEntry是40个字节因此每个实习对象需要40 4 44个字节。44 * 10M 440M。添加到32 * 10M 320MC的唯一实例仍然占用现在总内存占用量为760M字节换句话说我们使用更多的内存比以前而不是更少。这种情况可能有点极端但在实践中一般规则仍然存在如果在给定的一组对象中唯一对象的百分比很高那么传统的内部存储器可以节省内存存储对每个对象的引用给予它可能很小如果不是负面的话。我们可以做得更好吗固定大小的阵列无锁FALF内部事实证明如果我们不需要每个唯一对象的单个副本而只是想节省内存并且可能仍然有一些重复的对象 - 换句话说如果我们同意“机会性地”重复删除对象 - 有一个简单而有效的解决方案。我没有在文献中看到它所以我把它命名为“固定大小数组无锁FALFInterner”。此interner实现为一个小的固定大小基于开放哈希映射的对象缓存。当存在高速缓存未命中时给定插槽中的高速缓存对象始终用新对象替换。没有锁定和没有同步因此没有相关的开销。基本上这个缓存是基于这样的想法具有值X的具有许多副本的对象具有更高的机会保持在缓存中足够长的时间以保证在错过驱逐X之前自己的几个缓存命中并且用具有对象的对象替换它。一个不同的值Y.这是这个interner的一个可能的实现/** Fixed size array, lock free object interner */staticclassFALFInternerT{staticfinalintMAXIMUM_CAPACITY130;privateObject[]cache;FALFInterner(intexpectedCapacity) {cachenewObject[tableSizeFor(expectedCapacity)];}Tintern(Tobj) {intslothash(obj)(cache.length-1);TcachedObj(T)cache[slot];if(cachedObj!nullcachedObj.equals(obj))returncachedObj;else{cache[slot]obj;returnobj;}}/** Copied from java.util.HashMap */staticinthash(Objectkey) {inth;return(keynull)?0: (hkey.hashCode())^(h16);}/*** Returns a power of two size for the given target capacity.* Copied from java.util.HashMap.*/staticinttableSizeFor(intcap) {intncap-1;n|n1;n|n2;n|n4;n|n8;n|n16;return(n0)?1: (nMAXIMUM_CAPACITY)?MAXIMUM_CAPACITY:n1;}}
为了比较FALF interner和Guava weak interner的性能我写了一个简单的多线程基准测试。代码生成并实现从遵循高斯分布的随机数派生的字符串。也就是说具有某些值的字符串比其他字符串更频繁地生成因此将导致更多重复。在这个基准测试中FALF interner运行速度比Guava interner快约17。但是并非全部。当我测量内存占用时 jmap -histo:live 基准测试完成后运行但在退出之前事实证明使用FALF interner使用的堆大小比Guava interner小近30倍这是固定大小的小缓存与弱散列映射的内存占用量的差异其中每个独特对象都有一个条目。公平地说FALF interner通常需要比传统的一刀切的内部调整器更多的调整。首先由于缓存大小是固定的您需要仔细选择它。一方面为了最大限度地减少未命中这个大小应该足够大 - 理想情况下等于你实习生类型的唯一对象的数量。另一方面我们的目标是最小化已用内存因此在实践中您可能会选择更大更小的大小该大小大致等于具有大量重复项的对象的数量。另一个重要的考虑因素是为被拦截对象选择散列函数。在一个小的固定大小的缓存中尽可能均匀地在插槽中分布对象非常重要以避免不经常使用许多插槽时的情况而有一个小组其中每个插槽由几个对象值争用很多副本。这种争用将导致缓存遗漏许多重复因此更大的内存占用。当这种情况发生在具有非常简单hashCode() 方法的类的实例时 例如代码类似于java.lang.String 类中的代码 它可能表明该散列函数实现是不合适的。更高级的哈希函数就像com.google.common.hash.Hashing提供的哈希函数之一 类可以大大提高FALF interner的效率。检测重复对象到目前为止我们还没有讨论过开发人员如何确定应用程序中的哪些对象有很多重复项因此需要进行实习。对于大型应用程序这可能是非平凡的。即使您可以猜测哪些对象可能重复也很难估计其确切的内存影响。根据经验解决此问题的最佳方法是生成应用程序的堆转储然后使用工具对其进行分析。堆转储本质上是正在运行的JVM堆的完整快照。它可以通过调用jmap 实用程序在任意时刻进行 也可以将JVM配置为在失败时自动生成它 OutOfMemoryError。如果你谷歌“JVM堆转储”你会立即看到一堆关于这个主题的相关文章。堆转储是一个大小与JVM堆大小相同的二进制文件因此只能使用特殊工具读取和分析它。有许多这样的工具包括开源和商业。最流行的开源工具是Eclipse MAT; 还有VisualVM和一些不那么强大鲜为人知的工具。商业工具包括通用Java分析器JProfiler和YourKit以及JXRay - 专门为堆转储分析构建的工具。与大多数其他工具不同JXRay会立即分析堆转储以解决大量常见问题包括重复字符串和其他对象。目前对象比较浅薄。也就是说只有两个对象例如 ArrayListsx0, x1, x2, ... 以相同的顺序引用完全相同的对象组时才被认为是重复的 。换一种说法两个对象 a 和 b 被认为是平等的都指向其他对象 a 和 b 使用位是相等的有点。JXRay运行一次给定的堆转储并生成一个包含HTML格式的所有收集信息的报告。这种方法的优点是您可以随时随地查看分析结果并轻松与他人分享。这也意味着您可以在任何机器上运行该工具包括数据中心中功能强大但功能强大的“无头”机器。从JXRay获得报告后在您喜欢的浏览器中打开它并展开相关部分。你可能会看到这样的事情因此在此转储中24.7的已使用堆被重复的非数组非集合对象浪费上表列出了其实例对此开销贡献最大的所有类。要查看这些对象的来源哪些对象引用它们一直到GC根向下滚动到报告的“昂贵数据字段”或“完整参考链”子部分展开它然后单击相关表中的一行。以下是上述类之一的示例 TopicPartition从这里我们可以很好地了解哪些数据结构可以管理有问题的对象。总而言之重复对象即具有相同内容的同一类的多个实例可能成为Java应用程序的负担。它们可能会浪费大量内存和/或增加GC压力。衡量此类对象影响的最佳方法是获取堆转储并使用JXRay之类的工具对其进行分析。当重复对象是不可变的时您可以使用现成的或自定义的内部实现来减少此类对象的数量从而减少其内存影响。可变复制对象更难以摆脱并且可能只能通过逐个定制的解决方案来消除。