JVM自动内存管理机制分析

来源:互联网 发布:网络自由与秩序申论 编辑:程序博客网 时间:2024/05/22 14:52

最近这段时间学习了Java虚拟机相关的一些知识,对Java程序的运行和JVM的内存管理策略有了更深的理解。这篇文章就对此做一下简单的总结。

JVM内存模型

JVM内存模型

要理解JVM的内存管理策略,首先就要熟悉JVM的内存模型,如上图所示,在执行Java程序的时候,虚拟机会把它所管理的内存划分为多个不同的数据区,称为运行时数据区。在程序执行过程中对内存的分配、垃圾的回收都在运行时数据区中进行。对于Java程序员来说,其中最重要的就是堆区和JVM栈区了。注意图中的图形面积比例并不代表实际的内存比例。

  • 方法区是各个线程共享的内存区域,用于存储虚拟机加载进来的类信息、常量、静态变量和即时编译器编译后的代码等数据。相信大家也都听过运行时常量池的概念,这个常量池也是方法区的一部分,主要用于存放编译期生成的各种字面量和符号引用。
  • 堆区是JVM所管理的内存中最大的一块,这个区域是被所有线程共享的。主要用于存放对象实例,而所谓的垃圾回收也主要是在堆区进行。
  • 栈区则主要存放一些对象的引用和编译期可知的基本数据类型,这个区域是线程私有的,即每个线程都有自己的栈。
  • 程序计数器则是用来记录程序运行到什么位置的,显然它应该是线程私有的,相信这个学过微机原理与接口课程的同学都应该能够理解的。

内存的分配

在连续剩余空间中分配内存——指针碰撞

用一个指针指向内存已用区和空闲区的分界点,需要分配新的内存时候,只需要将指针向空闲区移动相应的距离即可。

在不规整的剩余空间中分配内存——空闲列表

如果剩余内存是不规整的,就需要用一个列表记录下哪些内存块是可用的,当需要分配内存的时候就需要在这个列表中查找,找到一个足够大的空间进行分配,然后在更新这个列表。

分配方式的选择

指针碰撞的分配方式明显要优于空闲列表的方式,但是使用哪种方式取决于堆内存是否规整,而堆内存是否规整则由使用的垃圾收集算法决定。如果堆内存是规整的,则采用指针碰撞的方式分配内存,而如果堆是不规整的,就会采用空闲列表的方式。

垃圾回收

上文已经提到,JVM的垃圾回收主要运行于堆区。下面我就来具体讲一下垃圾回收的原理。

找到需要回收的对象

要对对象进行回收,首先需要找到哪些对象是垃圾,需要回收。有两种方法可以找到需要回收的对象,第一种叫做引用计数法。具体方法就是给对象添加一个引用计数器,计数器的值代表着这个对象被引用的次数,当计数器的值为0的时候,就代表没有引用指向这个对象,那么这个对象就是不可用的,所以就可以对它进行回收。但是有一个问题就是当对象之间循环引用时,其中每个对象的引用计数器的值都不为0,但是这些对象又是作为一个孤立的整体在内存中存在,其他的对象不持有这些对象的引用,这种情况下这些对象就无法被回收,这也是主流的Java虚拟机没有选用这种方法的原因。另一种方法就是把堆中的对象和对象之间的引用分别看作有向图的顶点和有向边。这样只需要从一些顶点开始,对有向图中的每个顶点进行可达性分析(深度优先遍历是有向图可达性算法的基础),这样就可以把不可达的对象找出来,这些不可达的对象还要再进行一次筛选,因为如果对象需要执行finalize()方法,那么它完全可以在finalize()方法中让自己变的可达。这个方法解决了对象之间循环引用的问题。上面提到了“从一些对象开始”进行可达性分析,这些起始对象被称为GC Roots,可以作为GC Roots的对象有:

  1. 栈区中引用的对象
  2. 方法区中静态属性或常量引用的对象

上文中提到的引用均是强引用,Java中还存在其他三种引用,分别是,软引用弱引用虚引用,当系统即将发生内存溢出时,才会对软引用所引用的对象进行回收;而被弱引用所引用的对象会在下一次触发GC时被回收;虚引用则仅仅是为了在对象被回收时能够收到系统通知。

垃圾收集算法

已经找到了需要回收的对象,那么具体采用什么样的方式进行回收呢?下面就介绍几种算法。

标记-清除算法

通过可达性分析算法找到可以回收的对象后,要对这些对象进行标记,代表它可以被回收了。标记完成之后就统一回收所有被标记的对象。这就完成了回收,但是这种方式会产生大量的内存碎片,就导致了可用内存不规整,于是分配新的内存时就需要采用空闲列表的方法,如果没有找到足够大的空间,那么就要提前触发下一次垃圾收集。

标记-整理算法

标记的过程和标记-清除算法一样,但是标记完成之后,让所有存活的对象都向堆内存的一端移动,最后直接清除掉边界以外的内存。这样对内存进行回收之后,内存是规整的,于是可以使用指针碰撞的方式分配新的内存。

复制算法

上面所讲的两种算法都使用了先标记的方式,其实当对象数量很多时,这种算法的效率并不高。于是就产生了这种复制算法。它将可用内存分成两个部分,每次只使用其中的一部分,当其中一块用完时,就将仍然存活的对象复制到另外一块上,再把原来的那一块内存清理掉。这样回收的结果同样能得到规整的剩余空间,但是会浪费一部分内存。根据目前通过概率统计方面的研究,新生代中的对象的回收率能够达到90%以上,因此,便可以将新生代划分为三个部分,分别为Eden、Survivor from、Survivor to,大小比例为8:1:1。每次只使用Eden和其中的一块Survivor,回收时将存活的对象复制到另一块Survivor中,这样就只有10%的内存被浪费,但是如果存活的对象总大小超过了Survivor的大小,那么就把多出的对象放入老年代中。

分代收集算法

把Java堆分成新生代和老年代,新生代使用复制算法,老年代使用标记-清理或标记-整理算法。这样可以根据各个代自己的特点,选用合适的收集算法,提高内存收集的效率。在新生代中长期存活的对象会逐渐向老年代过渡,新生代中的对象每经历一次GC,年龄就增加一岁,当年龄超过一定值时,就会被移动到老年代。

垃圾收集器

上文讲了JVM垃圾回收的原理和使用的算法,接下来就该讲JVM使用的具体的垃圾回收器了。垃圾回收器在JVM中作为一个守护线程运行,它不能过多的占用系统资源,否则将会极大的影响用户体验。在从GC Roots开始对对象进行可达性分析时,需要STOP THE WORLD,因为如果不这么做,程序一边修改引用,GC收集器一边进行标记,那么标记的结果肯定是有问题的,所以收集器应当采取适当的措施减少这个停顿的时间。

  • Serial收集器:新生代使用复制算法,老年代使用标记-整理算法,单线程运行
  • Concurrent Mark Sweep(CMS)收集器:它的工作过程为初始标记->并发标记->重新标记->并发清除,初始标记和重新标记阶段都需要Stop The World,但是这两个阶段速度都很快,在耗时最长的并发标记阶段可以和用户线程并行工作。该收集器缺点就是内存回收的结果不是规整的可用空间,但是可以通过开关参数来设置对回收后的内存进行碎片整理。

垃圾收集器还有很多,比如ParNew收集器,最前沿的成果之一Garbage-First收集器等等这里就不一一介绍了,每种收集器都有自己的优点和不足,开发者应该选择适用于当前需求的收集器。

总结

以上内容很多总结自对《深入理解Java虚拟机》一书的学习,在此与技术爱好者分享。

0 0
原创粉丝点击