Java内存管理(一)

好久没有写博客了,深感羞愧,今天聊一下Java的内存管理

简单介绍

Java相比传统语言(C,C++)的一个优势在于其能够自己主动管理内存。从而将开发人员管理内存任务剥离开来。


本文大体描写叙述了J2SE 5.0 release中JVM对于内存是怎样管理的。

而且为选择和配置相应的收集器,配置收集器的參数提供了一些建议和參考。

手动VS自己主动内存管理

内存管理是能够识别哪些释放的对象不再使用。释放掉这些对象所占用空间的一个过程。

在非常多编程语言中,内存管理是开发人员的责任。可是管理内存的任务具有一定的复杂性。会导致非常多错误,影响到应用的行为,而且令程序崩溃掉。结果,开发人员非常大的一部分时间都是在debug和修正这些错误。

在手动内存管理中一个常常发生的问题就是dangling references。非常有可能当在释放某个对象占用的空间时,仍然包括其它对该销毁对象的引用,在这个时候,当这些引用指向了新的对象时,运行结果是无法预期的。
还有一个常见的问题就是space leaks。产生泄露的原因在于,当内存被分配了,可是却没有引用的情况下,以后就无法再次释放掉了。举个样例。假设开发人员试着释放一个链表,可是写的程序出了点小bug,值释放了头结点。那么链表中后面的对象就无法找到了。也就再被回收掉。一旦泄露过多,整个内存就会崩溃掉。

而相对于手动管理内存。在面向对象编程语言中,通常使用是自己主动管理内存技术。也称之为垃圾收集器。自己主动的内存管理对接口进行了更高层次的抽象。

垃圾收集器攻克了dangling reference问题。由于假设一个对象还被引用的话,是不会被垃圾收集器回收掉的。同一时候,垃圾收集器也攻克了space leaks问题。由于那些泄露的空间。属于没有被引用的对象,会被垃圾收集器回收掉。

垃圾收集器的概念

垃圾收集器主要有一下一些职责:

  • 分配内存
  • 确保引用的对象仍然还在内存中
  • 释放掉那些不可达对象所占用的空间

有引用对象通常称之为存活的对象。没有引用的对象通常称之为死亡对象,也被觉得是垃圾。

检索和释放掉死亡对象的过程就称之为垃圾收集。

垃圾收集器攻克了非常多非常多的内存管理问题,可是并非全部。

当然了。开发人员能够持续不断的创建对象,而且始终保持对他们的引用,直到没有可用的内存为止。垃圾收集本身也是一个复杂的任务,须要消耗相当的时间和资源的。

关于组织对象,分配和释放空间的算法都是由垃圾收集器处理的,是被隐藏在开发人员的视线之外的。空间一般是从一个非常大的内存池来释放的。称之为

垃圾收集的调度一般是取决于垃圾收集器本身的。

通常来说,整个堆或者堆的子集会在其填充满或者到达一定占比阈值的时候进行垃圾回收。

分配的任务包括在堆中找到一块没有使用的内存,当然。这一任务并不简单。这个动态分配空间的算法基本的问题就是避免碎片,尽量保证分配空间和释放空间的高效。

令人惬意的垃圾收集特性

垃圾收集器必须既保证安全。而且充分理解代码。也就意味着,存活的数据必须不能够被错误的释放,而垃圾不应该在几个回收周期之后,仍然存活。

当然。假设垃圾收集器能够高效的运行,不会在应用正在运行的过程中,进行长时间的停顿。肯定是非常好的。然而,在绝大多数的系统中,通常都会须要在空间。时间,频率上做出权衡的。举个样例,假设堆空间非常小的话,垃圾收集的速度会非常快。可是堆会更快的充满对象,也会须要进行更为频繁的垃圾收集。相反。假设堆空间配置的较大的话,那么堆充满须要的时间会更久,垃圾收集也不会运行的非常频繁。可是单次的垃圾回收须要的时间会更久。

垃圾回收假设能够有效限制分片的话,无疑也是非常好的。

当回收掉部分垃圾对象所占用的内存空间之后,空暇的空间可能以小块的形式存在于多个区域的。当出现这样的情况时,当再次为一个较大的对象申请空间的时候,可能会无法获得足够的空间。

一种消除碎片的方式叫做叫做内存紧缩。

扩展性相同是垃圾收集器所须要的。

分配操作不应该成为多进程。多线程应用的扩展性瓶颈,收集操作相同不应该成为瓶颈。

设计选择

在设计和选择垃圾回收算法的时候。通常须要作出一些抉择:

  • 选择串行回收还是并行回收。当使用串行回收的时候,每一个时间节点都仅仅会发生一件事情。

    举个样例,甚至是在多个CPU可用的情况下,也仅仅会有一个CPU来运行垃圾收集操作。而当使用并行收集的情况下。垃圾回收操作会分成不同的子模块。由不同的CPU并行运行。并行的操作会令回收操作速度更快。可是会有更高的复杂性成本以及潜在的碎片情况。

  • 并行回收VS全局暂停回收。当stop-the-world垃圾收集器运行的时候,应用的运行会在进行垃圾回收的时候全然暂停。当然,也能够同一时候并行运行垃圾回收操作和应用本身的处理。

    通常来说,并发收集器会将绝大多数任务并行运行完毕。可是有时也仍然会有较少的暂停应用的情况。

    stop-the-world垃圾收集器比并发收集器要更简单一些。由于在收集时,会将堆锁定,对象在此期间是不会发生变动的。当然。缺点是有些应用是不希望应用暂停的。相应的,使用并发收集器的话,应用暂停的时间会更短,可是收集器必须额外考虑,当应用在使用对象的时候,是否该运行更新操作。这回为并发收集带来额外的工作,在堆较大的时候,会带来一定的性能影响。

  • 压缩VS不压缩VS拷贝。当垃圾收集器决定了那些内存中的对象是存活的,那些是垃圾的时候,能够选择压缩内存,将存活的对象收集到一起,又一次利用剩余的空间。在压缩之后,能够非常easy的给新的对象分配空间。

    能够使用一个指针来跟踪分配对象的结尾。相对于压缩收集算法,非压缩收集算法会释放垃圾对象所在的位置。

    可是并不会将存活的对象压缩到一起,所以不会像压缩算法那样。能够留出较大的空间在新分配对象的时候使用。

    非压缩算法的优点是垃圾收集的速度非常快,可是内存碎片问题会比較严重。一般来说。非压缩算法的分配成本也要高于压缩算法。

    由于必须要搜索一块足够大的连续内存空间来给新的对象。

    还有一个算法就是拷贝收集。讲全部存活的对象复制到还有一块内存区域。优点在于,之前使用的内存区域就能够当成是全然全新的了。

    劣势就是须要拷贝所需的内存空间。

性能上的度量

在考虑垃圾收集器性能的时候,有下面一些方面须要考虑:

  • 吞吐:指的是不在垃圾回收上面使用的时间占比。

  • 垃圾收集负载:是吞吐的对立面。也就是垃圾回收上面的时间占比。
  • 暂停时间:当在运行垃圾回收的时候。应用停止运行的时间。

  • 收集频率:收集多久运行一次,这个值通常和应用的运行时相关的。

  • 占用的空间:对空间的占用的衡量,比方堆得大小。
  • 迅捷:当一个对象成为了垃圾对象和它占用空间可用的时间间隔。

交互式的应用须要较低的暂停时间,而总运行时间要比非交互式的应用要求要搞。而实时应用会在垃圾回收的暂停时间和垃圾回收的时间占比上都有较高的要求。而在个人计算机或者是嵌入式系统中,占用空间可能是应用更应该考虑的问题。

分代收集

当使用了分代收集技术的时候,内存是分成不同的代的,也就是将不同年纪的对象分放到不同的对象池中。举个样例。Java中最常使用的配置有两个不同的年代:年轻代,老年代,分别用来存放年轻的对象和年老的对象。

在每一个不同的代中,能够使用不同的垃圾回收算法。而每一个算法能够在其自己的年代中依据该年代的特性进行优化。

每一代的垃圾收集器都有例如以下的一种假设,称之为weak generational hypothesis,觉得多数语言中实现的应用(包括Java),有例如以下特点:

  • 大多数分配的对象都不会存活非常长的时间。
  • 少数存活非常久的对象会一直存在着。

如图所看到的:
这里写图片描写叙述

年轻代进行的垃圾回收相对来说。会相对更频繁,而且运行也更迅速,由于年轻代对象通常较小,而且会引用非常多生命周期非常短的对象。

而一些对象在几次年轻代回收都没有回收掉的话,就会晋升成为老年代对象。例如以下图:老年代通常比年轻代要大,其占用的增长速度会变慢。

所以,老年代垃圾回收不会非常频繁。可是回收的时间要更久一些。

这里写图片描写叙述

为年轻代选择的垃圾回收算法一般会优先考虑速度,由于年轻代的回收通常来说是更频繁。还有一方面。老年代考虑的算法一般是更考虑空间的有效性。由于老年代会占用很多其它的堆内空间,老年代算法须要更好的处理低密度垃圾回收。

J2SE JVM中的垃圾回收器

J2SE JVM中包括四种垃圾收集器。全部的垃圾收集器都是分代的。本节描写叙述了回收的分代和类型,以及讨论为何对象的空间分配一般是高效和迅速的。然后为每种垃圾收集器提供了具体的信息。

HotSpot分代

在JVM中,内存被分成三代来管理的。各自是前面提到的年轻代,老年代以及永久代。绝大多数对象都是被初始化到年轻代的。而老年代中包括的对象一般是多次回收都没有回收掉的年轻代对象,以及部分非常大的对象,这些对象是直接分配到老年代的。

永久代中包括一些对JVM方便进行垃圾收集管理的信息。比方描写叙述类和方法的对象,还有类和方法本身。

年轻代包括一个叫做Eden的区域和两个稍小的survivor区域。例如以下图。

这里写图片描写叙述

大多数对象都是直接初始化在Eden区域的。

(前面提到过,少数非常大的对象可能直接分配到老年代的)survivor空间持有那些至少一次从年轻代垃圾回收下存活的对象。

垃圾收集器会给这些对象再进入老年代之前一些机会,让他们在进入老年代之前仍然在年轻代中,能够被回收掉。在不论什么给定的时间,一个Survivor的空间(从图中标记为From)持有这样的对象。而还有一个直到下一次垃圾回收之前都是空的。

垃圾回收类型

当年轻代对象空间慢了,年轻代的垃圾收集就開始了(有的时候,也称之为minorGC)。当老年代或者永久代对象空间慢了,运行的垃圾回收称之为majorGC。通常来说,年轻代是优先收集的,使用的回收算法也是依据其年代的特点来特别设计的。由于通常年轻代对垃圾的识别和回收对效率要求更高。老年代的回收算法是同一时候运行在老年代和永久代的。一旦发生内存压缩。每一代都是分别进行内存压缩的。

有的时候,老年代已经空间不足,无法继续接受年轻代的对象了。在这样的情况下。除了CMS收集器。全部的手机都不会运行,年轻代的回收算法也不会运行。相反。会在整个堆上使用老年代回收算法。(CMS老年代算法属于特殊情况,由于它不会对年轻代进行收集)

高速分配

在非常多情况下,内存中都有非常大的连续空间用来给对象使用。

这些内存块的空间分配是配合简单的bump-the-pointer技术是十分高效的。bump-the-pointer技术就是通过一个指针来跟踪上一次释放对象空间的结尾。当新的分配请求过来的时候。JVM仅仅须要推断指针和当前代结尾之间的空间是否足够就能够了。假设能够的话,就挪动指针,而且初始化对象。

对于多线程应用来说。分配操作是必须保证线程安全的。假设使用全局锁来保证分配操作是线程安全的,那么分配操作进入某一代将会成为一个性能上的瓶颈。

相反。JVM使用了一个技术叫做Thread-Local Allocation Buffer技术(TLABs).该技术会将分配操作先写入线程本身的缓冲区中,来提高多线程分配操作的吞吐量。由于,一旦每一个线程将分配操作写入到自己的缓冲区的话。就能够使用bump-the-pointer技术实现高速分配,而且全程是不须要锁来进行堵塞操作的。当然,偶然的情况下,当线程内部的缓冲区已经填满了,无法写入很多其它的对象的时候,就必须使用同步操作来保证分配的线程安全性了。当然,使用TLABs同一时候也有一些降低空间浪费的技术。

TLABs的空间的浪费平均不到Eden区的1%。

使用TLABs技术和bump-the-pointer技术令分配操作性能非常高。大概仅仅须要10个本地指令的时间。

串行收集器

当使用串行收集器的时候,不管是年轻代还有老年代的手机,都是串行收集的(使用一个CPU),收集的过程中。会停止应用的一切运行。

串行收集——年轻代

下图展示了年轻代使用串行收集器收集的一些操作。存货的对象从Eden复制到空的survivor空间,也就是图中的TO区域,当然。假设对象太大是不会进入到To区域的,而是直接进入老年代。

在survivor中From区域的对象中。仍然相对年轻的对象复制到To空间。而比較老的对象会进入到老年代。注意,假设To空间满了,没有复制到To区域的Eden和From区域的对象将直接进入老年代,而不会管这些对象究竟经过了多少次年轻代的回收。而其它没有拷贝的EdenFrom区域的对象,就不再是存活的对象了。

这里写图片描写叙述

在一次年轻代收集完毕之后,不管是Eden还是survivor的From区域,就都是空的了,仅仅有survivor的To区域还有存活的对象。这个时候FromTo两者的职责会调换过来,參考下图:

这里写图片描写叙述

串行收集——老年代

老年代使用串行收集器收集的算法是mark-sweep-compact收集算法,在mark阶段,收集器识别出全部的存活的对象。而在sweep阶段,会清除垃圾。

收集器会运行滑动压缩,将存活的对象依次向老年代空间的起始位置滑动(永久代也一样)。而在老年代的末尾处留出较大的连续空间。当回收完毕以后。老年代仍然支持bump-the-pointer技术来实现高速分配。參考下图:

这里写图片描写叙述

何时使用串行收集器

串行收集器一般来说仅仅有运行在Client端的应用,而且这些应用对于应用暂停时长没有太多的需求的情况下会使用。以今天的设备来说。串行收集器能够在不到半秒的时间内,收集64M的堆空间。

J2SE5.0的公布时间为2005年的6月。以上測试结果以当时的硬件性能为准。

串行收集器的选择

在J2SE 5.0中。在非server的JVM中,默认的收集器就是串行收集器。假设使用其它的JVM的话,能够通过例如以下參数来指定使用串行收集器:

-XX:+UseSerialGC
原文地址:https://www.cnblogs.com/zsychanpin/p/7374870.html