JVM动态内存划分与垃圾回收算法

JVM动态内存划分与垃圾回收算法——《深入理解Java虚拟机》读书总结

为了实现运行时数据的动态管理,Java虚拟机实现了一套内存管理机制:首先对运行时内存进行划分,规定了内存划分的概念模型,并针对特定区域提供垃圾回收算法。使Java程序员不必关心内存管理细节,但凡事有利也有弊,当出现内存处理相关问题时,是需要我们对底层实现细节有所了解的,这样才能快速定位、解决问题。

本文主要介绍三部分内容:
- 运行时内存区域划分
- 对象存储与访问
- 垃圾回收算法


运行时内存区域划分

需要再次强调的是:本文中所提及到的内存区域划分,是针对JVM规范中的概念模型的总结,其不同JVM实现产品中可能存在差异,比如HotSport虚拟机实现中将虚拟机栈与本地方法栈统一实现。

JVM内存划分
不同于我们以往所认为的,将内存简单的分为堆和栈两部分的划分方式。从图中可见,JVM运行时数据区主要分为五大部分:程序计数器、虚拟机栈、本地方法栈、Java堆、方法区。下面进行详细说明。

  1. 程序计数器
    可以将其理解为行号指示器,主要控制指令执行流程,是顺序、分支、循环三大流程语句的执行基础。由于每个处理器同时只能执行一个线程中的某一条指令,因此需要每个线程都保存一个当前线程的程序计数器。

  2. 虚拟机栈
    每个方法调用都以栈桢为单位,通过入栈、出栈的操作实现方法调用的过程。栈桢中存储局部变量表、操作数栈、动态链接、方法出口等信息。

  3. 本地方法栈
    作用同于虚拟机栈,只不过本地方法栈主要用于本地方法的使用。


  4. 主要用于存储对象和数组的数据,根据垃圾回收策略,主要分为新生代和老年代,新生代划分为Eden、FromSurvior、ToSurvior。同样也有其他的分类方式,比如说从内存分配角度来看,可以分为多个线程私有的分配缓冲区TLAB(Thread Local Allocation Buffer)。另外,Hotspot虚拟机将方法区放入Java堆内存中,以永久代的方式进行管理,少写了针对方法区垃圾回收的方法,但效果不尽如人意。

  5. 方法区
    方法区主要存储虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。

另外,图中标出的直接内存并不是JVM规范中定义的内存区域,只是JVM可以通过NIO调用本地函数库的方式访问堆外内存以提高调用效率,这部分不受JVM对内存的大小限制,但还是受到本机总内存大小限制的,因此在对java程序进行内存划分时,不能占满本机所有内存,需要考虑这部分内存的需要。


对象的创建与访问

主要包括对象的存储结构、创建流程与访问方式,具体整理见下图
对象的创建与访问

对象主要存储于堆内存中,其创建流程主要分为五个步骤:
1. 检查类是否加载,若没有加载过则执行类加载的过程,类加载完成后则可以确定对象所需内存大小;
2. 分配内存,根据内存区域是否规整,分为“指针碰撞”(Bump the Pointer)和“空闲列表”(Free List)两种方法,指针碰撞针是指划分内存时由于内存规整只移动内存指示器的方式,空闲列表则表示通过一个记录表记录内存块的使用情况的方式。内存区域是否规整则由垃圾回收算法决定;
3. 初始化0值,这也是为什么我们对象创建好后属性拥有默认值的原因;
4. 设置对象头,根据对象不同状态,设置对象的运行时数据;
5. 执行Java类的init方法。

对象创建完后,其在内存中的数据结构主要包括对象头和类元指针信息。如果对象是一个数组,则还需要存储数组长度的数据。

对象的访问方式分为句柄和直接指针方式
1. 句柄访问:Java堆中会划分出一块句柄池,栈空间本地变量表中的reference类型变量指向句柄,句柄中包括指向对象的地址和方法区中类型数据的地址。
2. 直接指针:reference类型变量直接指向堆中对象内存的地址,对象中需要存储指向类型数据的地址。
两种方式各有优缺点,句柄方式对于移动对象操作,只需要改变句柄存储的地址就可以了,无需改变reference的数据,但多占用了内存,访问对象时增加了一次指针定位的开销;直接指针方式则效率更高,但访问类型数据和移动对象时则需更多操作。HotSpot采用直接指针访问方式。


垃圾回收算法

垃圾回收是早于Java而提出来概念,这一part主要包括三个问题:回收哪些内存?何时回收?怎么回收?
垃圾回收算法

回收哪些内存?

Java中采用可达性分析算法实现判断对象是否存活的目的,Java从一系列称作GC Roots的对象节点开始枚举对象,所走过的路径称为“引用链”,若一个对象到GC Roots没有任何引用链相连时,则判定对象为可回收对象。
对象之间的引用除了引用和没有引用外,还有一些中间状态可选。引用类型自JDK1.2以后进行了扩展,以描述一些“食之无味,弃之可惜”状态中的对象,类似于系统中缓存的应用。按引用强度由强到弱分为了四种,在针对不同类型引用进行可达性分析、内存回收时会区分对待。
1. 强引用:指普遍存在的一般引用,类似Object obj = new Object() 这类;
2. 软引用:非必需的对象,其关联对象在内存即将发生溢出异常之前会被进行二次回收,JDK提供SoftReference类实现;
3. 弱引用:同样描述非必需对象,在下次垃圾回收时,无论内存是否足够,都会回收该引用对象,JDK提供WeakReference类实现;
4. 虚引用:对对象生命周期不构成影响,在对该类引用对象进行垃圾回收时会收到一个系统通知,JDK提供PhantomReference类实现。

对于不可达对象,仍有存活机会,在垃圾回收两次标记过程中,第一次标记时会进行一次筛选,若对象重写了finalize()方法,则会将其加入F-Queue队列中,JVM通过自建立的、低优先级的Finalizer线程去执行。若对象finalize()方法中将其自身被引用至GC Roots引用链上,则其可以避免回收,但是,JVM不保证finalize()方法能够执行成功,该方法只是为了Java刚诞生时让C++开发人员更容易接受它。

何时回收?

STW(Stop The World):这个名字听起来很酷,但并不是什么好事……它是指在JVM进行枚举根节点(可达性分析中GC Roots引用链的确定)时需要对象间的引用保持在一个一致的条件(比如说一个对象刚被创建出来,还没有与其引用类型变量关联上,此时进行可达性分析,可能将其识别为可回收对象……),这就要求不得不停止所有的Java执行线程,这件事被称作STW。
HotSpot虚拟机中通过使用一种OopMap的数据结构存储引用关系来实现准确式GC(JVM可知内存位置上数据的具体类型),因此并不需要检查完全部上下文和全局引用位置。引起OopMap变化的指令非常多,只有当Java线程执行到SafePoint(安全点-具有让程序长时间执行特征)的地方,OopMap数据暂时固定下来时,才能进行GC,这些地方包括方法调用、循环跳转、异常跳转等。
让程序跑到安全点上停下来有两种方式:抢先式中断、主动式中断,抢先式中断是指JVM直接停掉所有执行线程,若发现没有在安全点上的线程,则将其恢复,让其继续执行到安全点,然后停止;主动式中断是设置一个中断标志,各线程执行时轮询这个标志,发现标志为真,则中断挂起。
针对线程处于Sleep或Blocked状态的线程,无法响应JVM中断请求,执行至安全点,这时便需要用到SafeRegion(安全区域-一段代码片段中,引用关系不发生变化),在线程进入这段代码时,标识自己进入安全区域,JVM发起GC,不用管处于SafeRegion中的线程。线程离开SafeRegion时,要检查是否完成了根节点枚举,完成后才能离开。

怎么回收?

  1. 标记-清除
    它是最基本的收集算法,其余两种都基于此算法实现。该算法分为标记、清除两个步骤,在确定回收哪些内存时,提到过两次标记过程,标记出要回收的对象,标记完成后,对标记回收的对象进行清除。其存在执行效率低、容易出现内存碎片的问题。

  2. 复制
    针对效率问题,复制算法将内存区域划分为一块Eden空间、两块较小的Survivor区,每次仅使用一块Eden、一块Survivor内存区,垃圾回收后,将存活对象复制到另外一块Survivor区间内,若存活对象占用空间超过另一块的Survivor内存区,则将其分配到担保的内存区间中(HotSpot中具体指老年代)
    复制算法存在存活率较高时复制效率低、需要额外内存空间进行担保的特点,适用于新生代的内存回收。

  3. 标记-整理
    该方法标记过程和标记-清除算法一样,整理过程是将存活对象移动到一端,然后对边界以外的对象进行直接清理,解决了内存碎片的问题,适用于老年代。

HotSpot虚拟机中的垃圾收集器按垃圾收集算法、并发、并行的特点进行分类,在此暂不过多介绍。

总结

本文对Java虚拟机运行时内存进行了概念模型的详细介绍,并针对堆内存中的对象创建流程、对象数据结构、访问定位做了详细说明。最后针对垃圾回收算法,围绕着回收哪些对象、何时回收、怎么回收展开描述,经过整理这些内容,对JVM内存结构、垃圾回收有了理论上的认识,这些理论、原则在不同垃圾收集器、JVM参数不同配置等条件下不是固定的,需要我们按实际情况加以理解、运用。

发布了159 篇原创文章 · 获赞 225 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/lyg673770712/article/details/80837046
今日推荐