伊甸园空间
所以我的问题是,这是否真的是真的,如果是的话,为什么Java的堆分配这么快?
我一直在研究Java GC的工作方式,因为这对我来说很有趣。我一直在尝试用C和C ++扩展我的内存分配策略集合(有兴趣尝试在C中实现类似的东西),这是一种非常快速的方法,可以从内存中以突发方式分配很多对象。实用的观点,但主要是由于多线程。
Java GC分配的工作方式是使用一种非常便宜的分配策略,将对象最初分配给“ Eden”空间。据我所知,它使用的是顺序池分配器。
就算法和减少强制性页面错误而言,这比malloc
使用C语言或operator new
C语言中的通用语言要快得多。
但是顺序分配器有一个明显的缺点:他们可以分配大小可变的块,但不能释放任何单独的块。它们只是以填充的直接顺序方式进行对齐填充,并且只能清除一次分配的所有内存。它们通常在C和C ++中用于构造仅需要插入而无需删除元素的数据结构,例如搜索树,该搜索树仅在程序启动时需要构建一次,然后重复搜索或仅添加新键(未删除任何键)。
它们甚至还可以用于允许删除元素的数据结构,但是由于我们无法单独取消分配它们,因此实际上不会从内存中释放这些元素。这种使用顺序分配器的结构只会消耗越来越多的内存,除非它经过一些延迟的传递,即使用单独的顺序分配器将数据复制到新的压缩副本中(如果固定分配器获胜,这有时是非常有效的技术)出于某种原因而做不到-只是按顺序向上分配数据结构的新副本并转储旧结构的所有内存)。
采集
就像上面的数据结构/顺序池示例中一样,如果Java GC仅以这种方式分配,即使对于许多单个块的突发分配而言,它的超快速度也将是一个巨大的问题。在软件关闭之前,它将无法释放任何内容,此时它可以一次释放(清除)所有内存池。
因此,取而代之的是,在单个GC周期之后,遍历“ Eden”空间(顺序分配)中的现有对象,然后使用能够释放单个块的通用分配器分配仍被引用的对象。不再引用的对象将在清除过程中被简单地释放。因此,基本上就是“如果仍然引用对象,则将它们从Eden空间复制出来,然后清除”。
这通常会非常昂贵,因此它是在单独的后台线程中完成的,以避免显着停止最初分配所有内存的线程。
一旦将内存从Eden空间中复制出来并使用这种更昂贵的方案分配,该方案可以在初始GC周期后释放单个块,则对象将移至更持久的内存区域。如果这些单独的块不再被引用,则在随后的GC周期中释放它们。
速度
因此,粗略地说,Java GC在直接堆分配中可能会非常好于C或C ++的原因是因为它在请求分配内存的线程中使用了最便宜的,完全简化的分配策略。然后,它节省了我们在使用更通用的分配器(例如,malloc
对另一个线程进行简化)时通常需要做的更昂贵的工作。
因此,从概念上讲,GC实际上实际上必须做更多的工作,但是它是在线程之间分配的,因此,单个线程不会预先支付全部费用。它使分配内存的线程能够以超便宜的价格进行分配,然后推迟正确执行操作所需的实际开销,以便可以将各个对象实际上释放给另一个线程。在C或C ++中,当我们malloc
调用时operator new
,我们必须在同一线程内预先支付全部费用。
这是主要区别,这就是为什么Java仅使用天真调用malloc
或operator new
单独分配一堆小块就可以胜过C或C ++的原因。当然,当GC周期开始时,通常会有一些原子操作和一些潜在的锁定,但是可能已经做了很多优化。
基本上,简单的解释可以归结为在单个线程(malloc
)中支付较重的成本,而不是在单个线程中支付较便宜的成本,然后在可以并行运行的另一个线程()中支付较重的成本GC
。不利的是,这种方式意味着您需要根据需要从对象引用到对象两个间接访问,以允许分配器在不使现有对象引用无效的情况下复制/移动内存,并且一旦对象内存被占用,您可能会失去空间局部性。移出“伊甸园”空间。
最后但并非最不重要的一点是,该比较有点不公平,因为C ++代码通常不会在堆上单独分配大量对象。体面的C ++代码倾向于为连续块或堆栈中的许多元素分配内存。如果它一次在免费商店中分配一小批小对象,那么代码就很糟糕。