探索 Java 内存管理机制

首图.jpg

目录

  1. 什么是内存?
  2. 什么是 Java 内存模型?
  3. 什么是 JVM?
  4. JVM 是怎么划份内存的?
  5. 栈帧中的数据有什么用?
  6. 什么是可达性算法?
  7. Java 中有哪几种引用?
  8. 什么是垃圾回收器?
  9. 参考文献

前言

这篇文章是我本身回顾和再学习 Java 内存管理相关知识的过程当中整理出来的。html

整理的目的是让我本身能对 Java 内存管理相关的知识的认识更全面一些,分享的目的是但愿你们也能从这些知识中获得一些启发。算法

1. 什么是内存?

内存是计算机中重要的部件之一,是与 CPU 进行沟通的桥梁,是 CPU 能直接寻址的存储空间,由半导体器件制成。缓存

若是说数据是商品,那硬盘就是商店的仓库,内存就是商店的货架,仓库里的商品你是不能直接买的,你只能买货架上的商品。数据结构

每个程序中使用的内存区域至关因而不一样的货架,当一个货架上须要摆放的商品超过这个货架所能容纳的最大值,就会出现放不下的状况,也就是内存溢出。架构

2. 什么是 JVM?

JVM(Java 虚拟机)是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,经过在实际的计算机上仿真模拟各类计算机功能来实现的。函数

JVM 有本身的硬件架构,如处理器、堆栈、寄存器等,还有对应分指令系统。性能

假如一个程序使用的内存区域是一个货架,那 JVM 就至关因而一个淘宝店铺,它不是真实存在的货架,但它和真实货架同样能够上架和下架商品,并且上架的商品数量也是有限的。学习

假如货架是在深圳,那 JVM 的平台无关性就至关因而客人能够在各个地方购买你在淘宝上发布的商品,不是只有在深圳才能购买货架上的商品。优化

3. 什么是 Java 内存模型?

Java 内存模型的主要目标是定义程序中各个变量的访问规则,也就是在虚拟机中将变量存储到内存,以及从内存中取出变量这样的底层细节。atom

下面咱们就来看下 Java 内存模型的具体介绍。

3.1 主内存与工做内存

Java 内存模型规定了全部的变量都存储在主内存(Main Memory)中,每条线程有本身的工做内存(Working Memory),线程的工做内存中保存了线程使用到的变量的内存副本。

线程对变量副本的全部操做都必须在工做内存中进行,不能直接读写主内存中的变量。

不一样线程之间没法直接访问其余线程工做内存中的变量,线程间变量值的传递都要经过主内存来完成。

Java 内存模型.png

3.2 执行引擎

所谓执行引擎,就是一个运算器,可以识别输入的指令,并根据输入的指令执行一套特定的逻辑,最终输出特定的结果 执行引擎对于 JVM 的做用就像是 CPU 对于实体机器的做用,均可以识别指令,而且根据指令完成特定的运算。

3.3 主内存与工做内存的交互操做

Java 内存模型中定义了 8 种操做来完成主内存与工做内存之间具体的交互协议,虚拟机实现时必须保证每一种操做都是原子、不可再分的。

这 8 种操做又可分为做用于主内存的和做用于工做内存的操做。

3.3.1 做用于主内存的操做

  1. lock(锁定)

    做用于主内存的变量,它把一个变量标识为一条线程独占的状态。

  2. unlock(解锁)

    做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其余线程锁定。

  3. read(读取) 做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便 load 时使用。

  4. write(写入)

    做用于主内存的变量,它把 store 操做从工做内存中获得的变量值放入主内存的变量中。

3.3.2 做用于工做内存的操做

  1. load(载入)

    做用于工做内存的变量,它把 read 操做从主内存中获得的变量值放入工做内存的变量副本中。

  2. use(使用 ) 做用于工做内存的变量,它把一个工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用的变量的值的字节码执行时会执行这个操做。

  3. assign(赋值) 做用于工做内存的变量,它把一个执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码执行时执行这个操做。

  4. store(存储) 做用于工做内存的变量,它把工做内存中的一个变量的值传送到主内存中,以便随后的 write 操做使用。

4. JVM 是怎么划份内存的?

JVM 在执行 Java 程序的过程当中会把它管理的内存分为若干个数据区域,而这些区域又能够分为线程私有的数据区域和线程共享的数据区域。

运行时数据区.png

4.1 线程私有的数据区域

4.1.1 程序计数器

程序计数器(Program Counter Register)有下面三个特色。

  • 较小

    程序计数器是一块较小的内存空间,它能够看做是当前线程执行的字节码的行号指示器。

  • 线程私有

    为了线程切换后能恢复到正确的执行位置,每条线程都有一个私有的程序计数器。

  • 无异常

    程序计数器是惟一一个在 Java 虚拟机规范中没有规定任何 OOM 状况的区域。

4.1.2 虚拟机栈

虚拟机栈能够说是 Java 方法栈,它有下面三个特色。

  • 描述方法执行

    虚拟机栈描述的是 Java 方法执行的内存模型,每一个方法在执行时都会建立一个栈帧(Stack Frame),栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。

    一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    关于栈帧在第 5 大节会有一个更多的介绍。

  • 线程私有

    与程序计数器同样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。

  • 异常

    在 Java 虚拟机规范中,对虚拟机栈规定了下面两种异常。

    1. StackOverflowError

      当执行 Java 方法时会进行压栈的操做,在栈中会保存局部变量、操做数栈和方法出口等信息。

      JVM 规定了栈的最大深度,若是线程请求执行方法时栈的深度大于规定的深度,就会抛出栈溢出异常 StackOverflowError。

    2. OutOfMemoryError

      若是虚拟机在扩展时没法申请到足够的内存,就会抛出内存溢出异常 OutOfMemoryError。

4.1.3 本地方法栈

本地方法栈(Native Method Stack)的做用与虚拟机栈很是类似,它有下面两个特色。

  • 为 Native 方法服务

    本地方法栈与虚拟机栈的区别是虚拟机栈为 Java 方法服务,而本地方法栈为 Native 方法服务。

  • 异常

    与虚拟机栈同样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

4.2 全部线程共享的数据区域

4.2.1 Java 堆

Java 堆(Java Heap)也就是实例堆,它有下面四个特色。

  • 最大

    对于大多数应用来讲,Java 堆是 JVM 管理的内存中最大的一块内存区域。

  • 线程共享

    Java 堆是全部线程共享的一块内存区域,在虚拟机启动时建立。

  • 存放实例

    堆的惟一做用就是存放对象实例,几乎全部的对象实例都是在这里分配内存。

  • GC

    堆是垃圾收集器管理的主要区域,因此有时也叫 GC 堆。

4.2.2 方法区

方法区(Method Area)存储的是已被虚拟机加载的数据,它有下面三个特色。

  • 线程共享

    方法区和堆同样,是全部线程共享的内存区域。

  • 存储的数据类型

    • 类信息
    • 常量
    • 静态变量
    • 即时编译器编译后的代码
  • 异常

    方法区的大小决定了系统能够保存多少个类,若是系统定义了太多的类,致使方法区溢出,虚拟机一样会抛出内存溢出异常 OutOfMemoryError。

方法区又可分为运行时常量池和直接内存两部分。

  1. 运行时常量池

    运行时常量池(Runtime Constant Pool)是方法区的一部分。

    Class 文件中除了有类的版本、字段、方法和接口等描述信息,还有一项信息就是常量池(Constant Pool Table)。

    常量池用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。

    运行时常量池受到方法区内存的限制,当常量池没法再申请到内存时会抛出 OutOfMemoryError 异常。

  2. 直接内存

    直接内存(Direct Memory)有下面四个特色。

    • 在虚拟机数据区外

      直接内存不是从虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。

    • 直接分配

      在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道与缓冲区的 I/O 方式,它可使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做,这样能避免在 Java 堆和 Native 堆中来回复制数据。

    • 受设备内存大小限制

      直接内存的分配不会受到 Java 堆大小的限制,可是会受到设备总内存(RAM 以及 SWAP 区)大小以及处理器寻址空间的限制。

    • 异常

      直接内存的容量默认与 Java 堆的最大值同样,若是超额申请内存,也有可能致使 OOM 异常出现。

5. 栈帧中的数据有什么用?

当 Java 程序出现异常时,程序会打印出对应的异常堆栈,经过这个堆栈咱们能够知道方法的调用链路,而这个调用链路就是由一个个 Java 方法栈帧组成的。

咱们来看下栈帧中包含的局部变量表、操做数栈、动态链接和返回地址分别有着什么做用。

栈帧.png

5.1 局部变量表

局部变量表(Local Variable Table)中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。

局部变量表中存放的编译期可知的各类数据有以下三种。

  1. 基本数据类型

    如 boolean、char、int 等

  2. 对象引用

    reference 类型,多是一个指向对象起始地址的引用指针,也多是指向一个表明对象的句柄或其余与此对象相关的位置

  3. returnAddress 类型

    指向了一条字节码指令的地址。

5.2 操做数栈

操做数栈(Operand Stack)也叫操做栈,它主要用于保存计算过程的中间结果,同时做为计算过程当中临时变量的存储空间。

操做数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操做。

当一个方法刚开始执行时,操做数栈是空的,在方法执行的过程当中,会有各类字节码执行往操做数栈中写入和提取内容,也就是出栈/入栈操做。

好比下面的这张图中,当调用了虚拟机的 iadd 指令后,它就会在操做数栈中弹出两个整数并进行加法计算,并将计算结果入栈。

操做数栈.png

5.3 动态链接

每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接(Dynamic Linking)。

5.4 方法返回地址

当一个方法开始执行后,只有两种方式能够退出这个方法,一种是正常完成出口,另外一种是异常完成出口。

  1. 正常完成出口

    执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。

    是否有返回值和返回值的类型将根据遇到哪一种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

  2. 异常完成出口

    在方法执行过程当中遇到异常,而且这个异常没有在方法体内获得处理,就会致使方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。

    一个方法使用异常完成出口的方式退出,任何值都不会返回给它的调用者。

不管采用哪一种退出方式,在方法退出后,都须要返回到方法被调用的位置,程序才能继续执行。

6. 什么是可达性算法?

在主流的商用程序语言(Java、C# 和 Lisp 等)的主流实现中,都是经过可达性分析(Reachability Analysis)断定对象是否存活的。

这个算法的基本思路就是经过一系列“GC Roots”对象做为起始点,从这些节点开始向下搜索,搜索走过的路径就叫引用链。

当一个对象到 GC Roots 没有任何引用链相连时,则证实此对象是不可用的。

好比下图中的 object五、object六、object7,虽然它们互有关联,可是它们到 GC Roots 是不可达的,因此它们会被断定为可回收对象。

引用链

在 Java 中,不一样内存区域中可做为 GC Roots 的对象包括下面几种。

  1. 虚拟机栈

    虚拟机栈的栈帧中的局部变量表中引用的对象,好比某个方法正在使用的类字段。

  2. 方法区

    1. 类静态属性引用的对象
    2. 常量引用的对象
  3. 本地方法栈

    本地方法栈中 Native 方法引用的对象。

7. Java 中有哪几种引用?

不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象的引用链是否可达,断定对象是否存活都与引用有关。

在 JDK 1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用和虚引用四种,这四种引用强度按顺序依次减弱。

7.1 强引用

强引用有下面几个特色。

  • 广泛存在

    强引用是指代码中广泛存在的,好比 "Object obj = new Object()" 这类引用。

  • 直接访问

    强引用能够直接访问目标对象。

  • 不会回收

    强引用指向的对象在任什么时候候都不会被系统回收,虚拟机即便抛出 OOM 异常,也不会回收强引用指向的对象。

    使用 obj = null 不会触发 GC,可是在下次 GC 的时候这个强引用对象就能够被回收了。

  • OOM 隐患

    强引用可能致使内存泄漏。

7.2 软引用

软引用有下面几个特色。

  • 有用但非必需

    软引用用于描述一些还有用但非必需的对象。

  • 二次回收

    对于软引用关联的对象,在系统即将发生内存溢出前,会把这些对象列入回收范围中进行二次回收。

  • OOM 隐患

    若是二次回收后尚未足够的内存,就会抛出内存溢出异常。

  • SoftReference

    在 JDK 1.2 后,Java 提供了 SoftReference 类来实现软引用。

7.3 弱引用

弱引用有下面几个特色。

  • 比软引用弱

    弱引用的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次 GC 前。

  • 发现即回收

    在 GC 时,只要发现弱引用,无论系统堆空间使用状况如何,都会将对象进行回收。

  • 无关紧要

    软引用、弱引用适合保存无关紧要的缓存数据。

  • WeakReference

    JDK 1.2 后,提供了 WeakReference 类来实现弱引用。

7.4 虚引用

虚引用是最弱的一种引用关系,它有如下几个特色。

  • 没法获取

    一个对象是否有虚引用的存在,都不会对其生存时间构成影响,也没法经过虚引用取得一个对象实例。

  • 收到通知

    为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知。

  • PhatomReference

    在 JDK 1.2 后,提供了 PhantomReference 类来实现虚引用。

8. 什么是垃圾回收器?

地上有脏东西是不可避免的,可是每天都要扫地又太麻烦了,有没有什么办法可让咱们不用扫地呢?

扫地机器人就能够帮咱们作这件事,而垃圾回收器 GC(Garbage Collector)就至关因而扫地机器人。

咱们 Java 开发者不用像 C++ 开发者那样关心内存释放的问题,可是咱们也不能挡着扫地机器人的路。

当咱们操做不当致使某块内存泄漏时,GC 就不能对这块内存进行回收。

GC 可不是个好伺候的主,若是你让“GC 很忙”,那它就会让你“应用很卡”。

拿 Android 来讲,进行 GC 时,全部线程都要暂停,包括主线程,16ms 是 Android 要求的每帧绘制时间,而当 GC 的时间超过 16ms,就会形成丢帧的状况,也就是界面卡顿。

垃圾回收器回收资源的方式就是垃圾回收算法,下面咱们来看下四个主要的垃圾回收算法。

8.1 标记-清除算法

标记-清除算法(Mark-Sweep)至关因而先把货架上有人买的、没人买的、空着的商品和位置都记录下来,而后再把没人买的商品统一进行下架。

标记-清除算法.png

  • 工做原理

    • 第一步:标记全部须要回收的对象

    • 第二步:标记完成后,统一回收全部被标记的对象

  • 缺点

    • 效率低

      标记和清除的效率都不高

    • 内存碎片

      标记清除后会产生大量不连续的内存碎片,内存碎片太多会致使当程序须要分配较大的对象时,没法找到足够的连续内存而不得不提早触发 GC

8.2 复制算法

为了解决效率问题,复制(Copying)收集算法出现了。

复制算法.png

  • 工做原理

    复制算法把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

    当使用中的这块内存用完了,就把存活的对象复制到另外一块内存上,而后把已使用的空间一次清理掉。

    这样每次都是对半个内存区域进行回收,内存分配时也不用考虑内存碎片等复杂问题。

  • 优势

    复制算法的优势是每次只对半个内存区域进行内存回收,分配内存时也不用考虑内存碎片等复杂状况,只要一动堆顶指针,按顺序分配内存便可。

  • 缺点

    • 浪费空间

      把内存缩小一半来使用太浪费空间。

    • 有时效率较低

      在对象存活率高时,要进行较多的复制操做,这时效率就变低了

8.3 标记-整理算法

在复制算法中,若是不想浪费 50% 的空间,就须要有额外的空间进行分配担保,以应对被使用内存中全部对象都存活的低端状况,因此养老区不能用这种算法。

根据养老区的特色,有人提出了一种标记-整理(Mark-Compact)算法。

标记-整理算法.png

  • 工做原理

    标记-整理算法的标记过程与标记-清除算法同样,但后续步骤是让全部存活的对象向一端移动,而后直接清理掉边界外的内存。

8.4 分代收集算法

现代商业虚拟机的垃圾回收都采用分代收集(Generational Collection)算法,这种算法会根据对象存活周期的不一样将内存划分为几块,这样就能够根据各个区域的特色采用最适当的收集算法。

在新生区,每次垃圾收集都有大批对象死去,只有少许存活,因此能够用复制算法。

养老区中由于对象存活率高、没有额外空间对它进行担保,就必须使用标记-清理或标记-整理算法进行回收。

堆内存可分为新生区、养老区和永久存储区三个区域。

堆内存区域.png

  1. 新生区

    新生区(Young Generation Space)是类的诞生、成长和消亡的区域。

    新生区又分为伊甸区(Eden space)、幸存者区(Survivor space)两部分。

    • 伊甸区

      大多数状况下,对象都是在伊甸区中分配的,当伊甸区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC。

      Minor GC 是指发生在新生区的垃圾收集动做,Minor GC 很是频繁,回收速度也比较快。

      当伊甸区的空间用完时,GC 会对伊甸区进行垃圾回收,而后把伊甸区剩下的对象移动到幸存 0 区。

    • 幸存 0 区

      若是幸存 0 区满了,GC 会对该区域进行垃圾回收,而后再把该区剩下的对象移动到幸存 1 区。

    • 幸存 1 区

      若是幸存 1 区满了,GC 会对该区域进行垃圾回收,而后把幸存 1 区中的对象移动到养老区。

  2. 养老区

    养老区(Tenure Generation Space)用于保存重新生区筛选出来的 Java 对象。

    当幸存 1 区移动尝试对象到养老区,可是发现空间不足时,虚拟机会发起一次 Major GC。

    Major GC 的速度通常比 Minor GC 慢 10 倍以上。

    大对象会直接进入养老区,好比很大的数字和很长的字符串。

  3. 永久存储区

    永久存储区(Permanent Space)是一个常驻内存区域,用于存放 JDK 自身携带的 Class Interface 元数据。

    永久存储区存储的是运行环境必需的类信息,被装载进该区域的数据是不会被垃圾回收器回收掉的,只有 JVM 关闭时才会释放此区域的内存。

参考文献

  1. 视频

    Top团队大牛带你玩转Android性能分析与优化

  2. 书籍

    《深刻理解Java虚拟机(第2版)》

    《实战Java虚拟机》

    《揭秘 Java 虚拟机》

相关文章
相关标签/搜索