内存管理以提高性能
本内存管理教程介绍了一组广泛适用的,易于重用的内存管理技术,它们可以潜在地提高Java应用程序的性能。
在许多Java应用程序中,要花费大量时间从内存中读取和写入内存。有时甚至会花费大量时间仅用于分配和释放内存(例如实例化和垃圾回收对象)。仅更改读取,写入,分配和释放内存的方式会影响应用程序的性能。
Java对象实例化和垃圾回收真的快吗?
作为Java开发人员,我们经常被告知:
- 对象实例化很快
- 垃圾收集速度快
的确,对象的实例化和垃圾回收往往会随着Java的每个版本而变得更快。但是,在某些情况下,通过自己管理这些方面仍然可以做得更好,因为我们可以根据应用程序使用内存的方式专门调整内存管理。
Java对象访问速度快吗?
在Java中,我们无法控制实例化的对象在内存中的位置。如现代硬件教程中所述,顺序访问内存比任意访问内存要快。因此,访问分散在整个内存中的对象的集合要比访问彼此相邻的对象的集合慢。
此外,对象可能包含对其他对象(例如字符串)的引用,这将进一步分散对象在整个内存中的位置。
为了从顺序内存访问中获得加速,我们必须自己控制对象存储。我将在本教程的后面部分解释一些方法。
在Java 10中使用值类型可以解决此问题,但是在撰写本文时,我们仅使用Java 8.
内存管理方面
内存管理有两个方面:
- 内存分配和释放策略
- 数据结构设计
在以下各节中,我将介绍这两个内存管理方面的内容。
内存分配
Java对象分配的一个问题是,我们无法控制JVM是通过回收现有的已释放对象来分配新对象,还是JVM是否在新位置中分配该新对象。
因为我们无法控制先前释放的对象的重用,所以我们无法控制JVM将使用的最大内存。是的,我知道JVM标志,但是它们是一种解决方法,而不是可靠的解决方案。当我们控制对象分配,释放和重新分配时,我们可以设置限制,例如一次最多有10.000条消息在内存中。在释放一些消息对象之前,不会分配新的消息对象。我们无法使用JVM标志来控制粒度如此精细的内存分配。
对象池
控制对象分配和释放的显而易见的解决方案是使用对象池。是的,我知道有人说我们不会获得任何提速,甚至可能会失去速度,但是我仍然需要看到支持它的基准。无论如何,如果没有对象池,我们将无法控制分配的对象数量,从而无法控制最大内存使用量。
对象池要求我们知道何时再次释放对象。由于对象可能会传递给许多不同的组件,而且我们可能无法知道应用程序中哪个组件是最后一个使用特定对象的对象,因此可能并不总是知道这一点。
数据结构设计
设计数据结构时,应考虑将一起使用的数据一起放在内存中。这通常意味着我们无法使用对象来表示该数据。
一种替代方法是使用原语数组来表示该数据。我们可以将原始数组包装在一个"导航器"对象中,该对象可以访问存储在原始数组中的字段,而不是让对象将数据作为字段包含在内。
我们可以为原始数组选择两个模型:
- 记录存储
- 列存储
记录存储
记录存储实际上是一个长字节数组,其中包含"记录"。每个记录由几个字段组成,这些字段彼此依次存储在字节数组中。每个字段可以包含一个或者多个字节。
要导航记录存储,我们需要某种"导航器"组件,该组件可以在记录之间导航,并且还可以定位每个记录中的每个字段。
如果我们需要一次遍历一个记录并处理其所有字段,那么记录存储将是一个很好的选择。由于记录的所有字段都位于内存中的每个字段之后,因此可以快速访问这些字段。同样,由于所有记录都在内存中一个接一个地定位,因此从一个记录到另一个记录的迭代也很快。
如果我们需要根据字段子集的内容在记录中进行搜索,那么记录存储就不是很好。当仅基于1或者2个字段进行搜索时,我们必须跳过所有未使用的字段。我们不再像以前那样顺序访问内存。对于搜索用例,列存储可能更合适。
列存储
列存储与记录存储类似,因为它包含具有存储在数组中的字段的记录。但是,列存储不是将记录的所有字段都依次存储在同一数组中,而是每列(字段)使用一个数组。这就是为什么它被称为列存储的原因。
使用列存储,可以快速搜索列值与给定条件匹配的记录。我们只需在列数组中扫描要搜索的列。这比在记录存储中搜索要快,因为我们不必跳过未使用的字段。
记录的字段以相同的索引存储在所有列数组中。这意味着,当我们在与查询条件相匹配的列数组中找到一个列值时,我们可以轻松地找到同一记录的所有其他字段的值。我们只需读取存储在所有其他列数组中相同索引处的值。
列存储的另一个优点是,我们可以为每个列使用不同的原始类型。在记录存储中,我们几乎被迫使用字节数组来确保可以支持所有类型的字段。对于列存储,一列可以是short
,int
,long
等的数组,也可以是我们需要的其他任何数组。