运行时数据区域

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存

程序控制器用来实现代码的流程控制,以及在多线程情况下实现上下文切换。生命周期和线程相同。

Java方法虚拟机栈用于实现方法调用。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。生命周期和线程相同。如果超出了栈的内存大小,则会爆出StackOverFLowError错误或者OutofMemoryError

堆是JVM所管理的内存中最大的一块,**唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**堆也是垃圾收集器管理的主要区域,因此也被称作GC堆。

方法区之前听到的一种说法和「类」区分开,存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量等信息。

字符串常量池是JVM为了提升性能和减少内存消耗针对String主们开辟的一块区域,主要目的是为了避免字符串的重复创建。

堆空间的基本结构

Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收。

在JDK7版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存「Young Generation」
  2. 老生代「Old Generation」
  3. 永生代

JDK8之后永久代已被元空间取代,元空间使用的是直接内存。

内存分配和回收原则

  • 对象优先在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
  • 大对象直接进入老年代,例如字符串、数组等。
  • 长期存活的对象将进入老年代。对象每熬过一次MinorGC,年龄就增加1岁,当它的年龄晋升到老年代的年龄阈值,就会被晋升到老年代中。
  • 空间分配担保是为了确保在Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间

各种GC:

  • 部分收集
    • 新生代收集「Minor GC」:只对新生代进行垃圾收集
    • 老年代手机「Major GC」:只对老年代进行垃圾收集
    • 混合收集「Mixed GC」:整个新生代和部分老年代
  • 整堆手机「Full GC」:收集整个Java堆和方法区

死亡对象判断方法

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

引用计数法

类似拓扑排序,给对象中添加一个引用计数器,出现循环引用就GG。不常用。

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。

jvm-gc-roots.d187e957

哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈 (栈帧中的本地变量表) 中引用的对象
  • 本地方法栈 (Native 方法) 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

引用类型总结

J VM将引用分为强引用、软引用、弱引用、虚引用四种「强度逐渐减弱」

绝不会回收强引用。内存空间不足了才会回收软引用。无论如何都会回收弱引用。

垃圾收集算法

标记-清除算法

该算法分为 “标记” 和 “清除” 阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题
  2. 空间问题(标记清除后会产生大量不连续的碎片)

标记-复制算法

该算法将内存分为大小相等的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程仍然与 「标记 - 清除」 算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法。根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择「标记-复制」算法。只需要付出少量对象的复制成本就可以完成每次垃圾收集。

而对于老年代,则必须选择「标记-清除」或「标记-整理」算法。

类加载过程

  • 加载
  • 连接
    • 验证
    • 准备
    • 解析
  • 初始化

双亲委派模型

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。

  • ClassLoader类使用委托模型来搜索类和资源
  • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
  • ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器

父子关系通常使用组合关系来复用父加载器的代码。

执行流程

  • 首先判断当前类是否加载过。已经被加载的类会直接返回,否则才会尝试加载
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
  • 只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载

好处

双亲委派模型保证了Java程序的稳定运行以及核心API不被篡改,可以避免类的重复加载。这是因为JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类。

参考文章:JavaGuide