乞丐是如何节约Java内存的
作者:米哈伊尔·沃龙佐夫
为什么要减少内存占用
本文将为您提供有关 Java 内存消耗优化的一般建议。
内存使用优化在 Java 中很重要。系统性能主要限于内存访问性能而非 CPU 主频,否则,为什么 CPU 生产商要实现所有这些 L1、L2 和 L3 缓存?这意味着通过减少应用程序内存占用,您很可能会通过让 CPU 等待更少量的数据来提高程序数据处理速度。即:节省内存会提高性能!
Java 内存布局方式
我们先复习一下小学二年级学的 Java 对象的内存布局:任何 Java Object
占用至少 16 个字节,其中 12 个字节被 Java 对象头占用。除此之外,所有 Java 对象都按 8 字节边界对齐。这意味着,一个包含 2 个字段:int 和 byte的对象:将占用 17 个字节(12 + 4 + 1),而不是 24 个字节(17 个由 8 个字节对齐)。
如果 Java 堆在 32G 以下且开启选项 XX:+UseCompressedOops
(从JDK6_u23开始UseCompressedOops 被默认打开了),则每个Object引用占用 4 个字节。否则,Object引用占用 8 个字节。
所有原始数据类型都占用其确切的字节大小:
byte,boolean | 1个字节 |
---|---|
short,char | 2个字节 |
integer,float | 4个字节 |
long,double | 8个字节 |
Array/String
数字包装器的内存消耗 ,那将会更方便。最常见的 Java 类型内存消耗 数组消耗 12 个字节加上它们的长度乘以它们的元素大小(当然,还有 8 个字节对齐的额外占用)。
从 Java 7 build 06 开始,
String
,包含 3 个字段 - 一个char[]带有字符串数据的int字段加上 2 个带有 2 个由不同算法计算的哈希码的字段。这意味着 String 本身需要 12 (header) + 4 ( char[]reference) + 4 2 (int) = 24 字节(如您所见,它完全适合 8 字节对齐)。除此之外,char[]带有String数据占用 12 + 长度 2 个字节(加上对齐)。这意味着 String 占用 36 + length*2 字节对齐 8 个字节(顺便说一下,这比Java 7 build 06 String之前的内存消耗少 8 个字节)。数字包装占用 12 个字节加上基础类型的大小。
Byte/Short/Character/Integer/Long
由 JDK 缓存,因此对于 -128~127 范围内的值,实际内存消耗可能会更小。无论如何,这些类型可能是基于集合的应用程序中严重内存开销的来源:Byte, Boolean | 16 bytes |
---|---|
Short, Character | 16 bytes |
Integer, Float | 16 bytes |
Long, Double | 24 bytes |
- 优选原始类型而不是它们的 Object 包装器。使用包装器类型的主要原因是 JDK Collections,因此请考虑使用像 Trove 这样的原始类型集合框架之一。
- 控制您拥有的 Object 数量。例如,优先考虑基于数组的结构,而不是基于指针的结构,如: ArrayList/ArrayDeque/LinkedList
第一种方法是使用
Map
标准 JDK 中的一个。我们粗略估计一下这个结构的内存消耗。每个Integer
占用 16 个字节加上 4 个字节用于Integer
映射的引用。每 20 个字符长String占用 36 + 20*2 = 76 个字节(见上文String
描述),对齐到 80 个字节。加上 4 个字节作为参考。总内存消耗大约为(16 + 4 + 80 + 4) * 1M = 104M。更好的方法是用 String 字符串包装第 1 部分UTF-8 编码用 byte[]替换(参见将字符转换为字节文章)。我们的 Map 将是
Map
. 假设所有字符串字符都属于 ASCII 集 (0-127),这在大多数英语国家都是如此。byte[20]占用 12 (header) + 20*1 = 32 字节,方便地适合 8 字节对齐。整个 Map 现在将占据(16 + 4 + 32 + 4) * 1M = 56M,比上一个示例少 1 半。现在让我们使用Trove
TIntObjectMap
。int[] 与 JDK 集合中的包装器类型相比,它正常存储键值。现在每个键将占用 4 个字节。总内存消耗将下降到(4 + 32 + 4) * 1M = 40M。最终的结构会更复杂。所有
String
值将byte[]一个接一个地存储(我们仍然假设我们有一个基于文本的 ASCII 字符串),中间用一个字节0作分隔符。整体byte[]将占据 (20 + 1) * 1M = 21M。我们的 Map 将存储字符串的偏移量,byte[]
而不是字符串本身。为此目的我们将使用 Trove 的 TIntIntMap
。它将消耗 (4 + 4) * 1M = 8M。此示例中的总内存消耗将为8M + 21M = 29M。顺便说一句,这是第一个依赖该数据集不变性的示例。我们能取得更好的结果吗?是的,我们可以,但代价是 CPU 消耗。显而易见的“优化”是在将值存储到一个大的byte[]. 现在我们可以将键值存储在中int[]并使用二分搜索来查找键值。如果找到一个键,它的索引乘以 21(请记住,所有字符串都具有相同的长度)将为我们提供一个值在byte[]. 与哈希映射情况下的查找相比,此结构“仅”占用21M + 4M(对于int[])= 25M,其代价是查找复杂度从O(1) 变成 O(log N)。
这是我们能做的最好的吗?不!我们忘记了所有值都是 20 个字符长,所以我们实际上不需要byte[]之间的分隔符. 这意味着如果我们同意以O( log N )进行查找,我们可以使用24M 内存来存储我们的“Map”。与理论数据大小相比,完全没有开销,并且比原始解决方案(
Map
)所需的量少了近 4.5 倍!谁告诉你 Java 程序很耗内存?总结 优先考虑原始类型而不是它们的 Object 包装器。使用包装器类型的主要原因是 JDK 集合,因此请考虑使用像 Trove 这样的原始类型集合框架之一。
尽量减少您拥有的 Object 数量。例如,偏向基于数组的结构,而不是基于指针的结构,如.
ArrayList/ArrayDeque/LinkedList
推荐阅读 【乞丐是如何节约Java内存的】如果您想了解更多关于聪明的数据压缩算法的信息,值得阅读 Jon Bentley 的“Programming Pearls”(第二版)。这是一个非常出人意料的算法的精彩集合。例如,在第 13.8 节中,作者描述了 Doug McIlroy 如何设法在 64 KB 的 RAM 中安装一个 75,000 字的拼写检查器。那个拼写检查器把所有需要的信息都保存在这么小的内存中,而且不使用磁盘!可能还需要注意的是,《Programming Pearls》是 Google SRE 面试的推荐准备书之一。
推荐阅读
- java如何实现图片转化为数据流
- 如何取消电脑开机密码
- 微信如何绑定QQ邮件?微信绑定QQ邮件的办法_微信
- 微信亲密充是啥?微信亲密充怎样开通_微信
- 2017微信表情包首波:我也许是假的_微信
- 微信朋友圈“猜出你就是神”图片猜成语答案是啥?_微信
- 洋葱头一键装机如何运用,本文教您运用洋葱头一
- 一键还原系统,本文教您如何运用一键还原软件还
- 安装系统 黑云一键重装系统,本文教您如何迅速安装系统
- 电脑系统如何一键还原,本文教您电脑系统一键还