JVM内存管理与垃圾回收

Java虚拟机是所有Java应用运行的基础,作为Java程序猿,熟悉虚拟机基本原理是必备技能。这里整理一下JVM的内存管理和垃圾回收机制。

1 JVM内存管理

闲话少说,直接上图,JVM内存模型基本如下:


JVM内存结构

JVM对内存主要划分了以上极大部分,包括常说的栈(JVM Stack)和堆。如上图,在JVM中,程序计数器、虚拟机栈和本地方法区为线程独享,而堆内存和方法区为各线程共享,下面逐一解释一下各部分的作用。

1.1 程序计数器(Program Counter Register)

程序计数器是较小的一块内存空间,是当前线程执行字节码的行号指示器,Java程序执行是,分支、循环、跳转、异常处理、线程回复等都需要这个计数器来完成。当执行的是Java方法,计数器记录的是虚拟机字节码的指令地址;当执行的是Native方法,计数器值为空(Undefined)。该区域不会发生OutOfMemoryError异常。

1.2 Java虚拟机栈(Java Virtual Machine Stacks)

虚拟机栈是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态链接、方法出口等信息,方法的调用至完成的过程对应着一个栈帧在虚拟机栈中的入栈和出栈过程。

局部变量表内以局部变量空间(Slot,长度为32位)划分,它存放了8种基本数据类型(boolean、byte、char、short、int、long、double、float)、对象引用(指向对象起始地址的引用指针)和returnAddress类型(指向了一条字节码指令的地址)。大部分数据类型占用一个Slot,其中64位的double和long占用2个Slot。

当请求深度超过栈的深度将跑出StackOverFlowError异常;如果虚拟机栈可动态扩展,当无法申请足够内存时就会抛出OutOfMemoryError异常。

1.3 本地方法栈(Native Method Stack)

本地方法栈和虚拟机栈类似,只不过它是为虚拟机用到的Native方法服务,故在常用的虚拟机(Sun HotSpot)中直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈类似,本地方法栈也会抛出StackOverFlowError异常和OutOfMemoryError异常。

1.4 方法区(Method Area)

方法区是各线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译(JIT)后的代码等数据。他有一个别名Non-Heap(非堆),与Java堆区分开来。

在HotSpot虚拟机中,此部分习惯称之为“永久代”(Permanent Generation),实际两者并不等价,HotSpot设计上使用永久代实现方法区,以方便垃圾收集器像管理Java堆一样管理这部分内存。

此部分当内存不足时会发生OutOfMemoryError异常。

1.4.1 Java 1.6 -> Java 1.7 -> Java 1.8 方法区的变化

正如上文所说,在JDK1.7之前,使用永久代实现方法区,由于内存回收困难易遇到性能问题和内存溢出问题。

在JDK1.7中,已经开始着手改善这个问题,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap,例如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但永久代仍存在于JDK1.7中,并没完全移除。

在JDK1.8中,永久代已经完全被移除,取而代之的是元空间(Metaspace),元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

1.4.2 运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等信息,还有一项常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在加载后进入方法区的运行时常量池存放。运行时常量池受方法区内存的限制,当常量池无法再申请到内存时会抛出OutMemoryError异常。

1.5 Java堆(Java Heap)

Java堆是Java虚拟机管理内存中最大的一块,被所有线程共享,同时也是Java垃圾回收的主要区域,所以这里将堆内存最后讲解。这里,再看一下Java堆的构成,如下图:


堆的构成

为了实现分代垃圾回收,Java堆细分成了新生代和老年代,其中新生代为了高效利用与垃圾回收,将其分成了Eden空间、From Survivor空间、To Survivor空间。由于新生代中的对象都是“朝生夕死”,所以并不需要每个空间1:1分配,通常分配比例为8(Eden):1(S0):1(S1)。每次使用Eden空间和其中一块Survivor空间,当垃圾回收时,将Eden空间和Survivor空间存活的对象一次性复制到另一块Survivor空间上,如此循环往复,这样每次新生代可使用内存容量为90%。对象在新生代躲过一次GC的话,其对象年龄便会加1,默认情况下,对象年龄达到15时,就会移动到老年代中。此外、大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。

Java对申请的内存可以使不连续的,当堆中的内存没有完成实例分配,并且无法再扩展时,将会抛出OutMemoryError异常。

1.7 直接内存/堆外内存(Direct Memory)

此外,JVM使用的内存并不是虚拟机运行时数据区的一部分,例如上文提到的元空间,这部分内存称之为直接内存或堆外内存。在JDK1.4中,新加入了NIO(NewInput/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象座位这块内存的引用进行操作,这样在某些特定场景中能限制提高性能,避免了再Java堆和Native堆中来回复制数据。

2 垃圾回收

说起垃圾回收(Garbage Collection, GC),就必须考虑3件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?
2.1 如何确定对象可回收

在主流商用语言中,一般使用可达性分析算法来判断对象是否存活,假如不存活则可以对它进行垃圾回收。这个算法的基本思想就是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为引用链(Reference Chain),当一个对象的GC Roots没有任何引用链时,则证明该对象不可用。

在Java语言中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)的应用对象。

这里一直提到引用,在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为4种:

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)
2.2 常见的垃圾回收算法及回收策略

垃圾收集的方法论——几种算法的思想及发展历程

1)标记-清除(Mark-Sweep)算法:最基础算法,分为标记和清除两个阶段,效率较低,易产生内存空间碎片。

2)复制算法(Copying)算法:为了改善效率问题,提出了复制算法,将内存区域分为两块,每次使用一块,每次一块内存使用完了九江存活对象复制到另一个块上面,把使用完成的一次性清理掉。该方式解决了效率和内存碎片问题,但是可使用内存太低,而大部分对象都是“朝生夕死”故商业虚拟机都是不均等分割的,比如按照8:1:1分成了一块Eden空间和2块Survivor空间。

3)标记-整理(Maark-Compact)算法,标记整理算法和标记清除算法类似,但后续步骤不是对可回收对象进行清理,二是让存活对象都移向一端,然后清理掉端边界以外的内存。

当前商业虚拟机垃圾手机都猜用“分代收集”,该方法并不没有什么新思想,只不过是可以根据不同年代的特点采取不同的垃圾手机算法。

垃圾回收的具体实现——垃圾收集器

1)Serial 收集器:单线程新生代收集器,暂停所有用户线程;老年代采用标记-整理算法,暂停所有用户线程。该收集器简单高效,收集器易产生“Stop The World”,多用于Client模式下的虚拟机。

2)ParNew 收集器:ParNew收集器就是Serial的多线程版本,Server模式下虚拟机中首选的新生代收集器,能与CMS收集器配合使用。

3)Parallel Scavenge 收集器:侧重于达到可控吞吐量(Throughput)的新生代收集器,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。也称之为“吞吐量优先”收集器。

4)Serial Old 收集器:是Serial收集器的老年代版本,使用“标记-整理”算法,现用于CMS的备受收集器。

5)Parallel Old收集器:多线程办的的Parallel Scavenge老年代收集器,侧重吞吐量及CPU资源敏感的场合可以考虑Parallel Scavenge加Parallel Old收集器。

6)CMS (Concurrent Mark Sweep)收集器:CMS收集器是一种最短回收停顿时间的老年代收集器,它基于“标记-清除”算法实现,整个过程分为4个步骤:

  • 初始标记(CMS initial mark),标记GC Roots能直接关联到的对象,速度很快
  • 并发标记(CMS concurrent mark),GC Roots Tracing过程
  • 重新标记(CMS remark),修正并发标记期间用户程序继续运行导致变动的一小部分对象的标记记录
  • 并发清除(CMS concurrent sweep),清理掉可回收对象

Concurrent Mark Sweep 收集器运行示意图

CMS在初始标记和重新标记过程仍然会STW。不过由于整个过程耗时最长的并发标记和并发清除是和用户线程一起并发执行的,做一总体来说CMS收集器挺短时间较短,故也称之为并发低停顿收集器。

CMS收集器缺点:

  • 对CPU资源非常敏感;
  • 无法处理浮动垃圾,导致Full GC产生;
  • 基于“标记-清理”算法,会产生大量空间碎片。

7)G1(Garbage-First)收集器:G1收集器是当今收集器技术发展的最前沿成果,G1具备优势:

  • 并行与并发,充分利用多CPU,多核优势部分需要停顿的的GC动作在G1内可以与用户线程并发执行。
  • 分代收集,虽然G1可以独自管理整个堆内存,但它仍然保留分代的概念
  • 空间整合,它将真个Java的堆分成多个大小相等Region,新生代和老年代都是多个Region(不需要连续)的集合,所以不会产生空间碎片。G1收集器从整体上看是基于“标记-整理”算法,当从局部来讲(两个Region之间)是基于“复制”算法实现的。
  • 可预测的停顿,这是G1相对于CMS的一大优势,G1除了低停顿,还可以建立可预测的停顿时间模型。

G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC和 Full GC,在不同的条件下被触发。

Yong GC 和Full GC 比较好理解,这里说一下Mixed GC:

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

Mixed GC出发条件,有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次Mixed GC。

Mixed GC收集器运行示意图

Mixed GC收集器运作可大致分为以下几个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting Evacuation)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值