• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

JVM学习总结(二)内存管理

互联网 diligentman 47分钟前 1次浏览

垃圾收集需要完成的三件事

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?
  1. 哪些内存需要回收?

Java堆和方法区这两个区域有着显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有在运行期间,我们才能知道程序创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。

在堆里面存放着Java世界中所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

引用计数算法

在对象中添加一个引用计数器,没有一个地方引用它时,计数器值就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象就是不可能再被使用的。在Java领域,主流都Java虚拟机里面没有选用该算法管理内存,因为这个算法要配合大量额外处理才能保证正确地工作,譬如单纯用引用计数就很难解决对象之间相互引用的问题。

可达性分析算法

这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明该对象是不肯能再被使用了。

在Java里,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI引用的对象,
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(Synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

引用类型

  1. 强引用,只要存在强引用关系,垃圾收集器就永远不会回收掉被引用的对象。
  2. 软引用,是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用,也是用来描述哪些非必须对象,但是它的强度比软引用更弱,被弱引用关联的对象仅仅可以存活到下一次垃圾收集发生为止。
  4. 虚引用,也被为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,要宣告一个对象死亡,至少要经历两次标记的过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。加入对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

方法区回收

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与Java堆中的对象非常类似。回收不再使用的类需要满足如下三个条件:

  1. 该类的所有实例都已经被回收,也就是Java堆中不存在该类机器任何派生子类实例。
  2. 加载该类的类加载器已经被回收。
  3. 该类对呀的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该方法。

垃圾收集算法

分代收集假说

  1. 绝大多数对象都是朝生夕灭的。
  2. 熬过越多次垃圾收集过程的对象越难以消亡。
  3. 跨代引用相对于同代引用来说仅占极少数。

依据假说三,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了快带引用的小块内存里的对象才会被加入到GC Roots进行扫描。

标记清除算法

算法运行过程:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。该算法的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间碎片化的问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后再程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集工作。

标记复制算法

标记复制算法是为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。在HotSpot中Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局,称为"Appel式回收"。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块survivor。发送垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。因为无法保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的逃生门的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要老年代进行分配担保。

标记整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策:

如果移动存活对象,尤其在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能运行。但如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。


喜欢 (0)