Java虚拟机

Java内存区域与内存异常:

Java虚拟机在执行Java程序的时候会将其管理的内存划分为若干数据区。
image

  1. 程序计数器(线程私有)

    该区域是一块较小的区域,它主要用于记录Java虚拟机进行多线程操作的时候为每个线程记录自己的执行位置。
    如果线程执行的是Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址
    如果线程执行的是Native方法,则计数器为空
    该内存区域是Java虚拟机规范中唯一一个没有规定任何OOM情况的区域

  2. Java虚拟机栈(线程私有)

    Java虚拟机栈的生命周期与线程相同,每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机中入栈道出栈的过程。
    StackOverflowErro异常:如果线程请求的栈深度大于虚拟机所允许的深度则抛出该异常。
    OutOfMemoryError异常:如果虚拟机栈可以动态扩展且扩展时无法申请到足够的内存,则抛该异常。

  3. 本地方法栈

    本地方法栈和Java虚拟机栈作用类似,虚拟机栈为虚拟机执行Java方法而服务,而本地方法栈为虚拟机调用Native方法而服务。

  4. Java堆(线程共享)-Non-Heap(非堆)

    该区域在虚拟机启动时创建,唯一目的:存放对象实例(几乎所有的对象实例都存在该区域)
    Java堆是垃圾收集器管理的主要区域。
    如果在堆中没有内存完成实例分配,并且堆也无法再继续扩展时,抛出OOM异常。

  5. 方法区(线程共享)

    该区域主要存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

  6. 运行时常量池

    该区域是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池。用于存放编译期生成的各种字面量和符号引用

  7. 直接内存

    直接内存并非虚拟机运行时数据存储的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存频繁的使用也会导致OOM异常。
    JDK1.4新加入里NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免类在Java堆和Native堆中来回复制数据。

垃圾收集器与内存分配策略

如何判断对象是否存活?

1.引用计数算法

思路:给对象中添加一个引用计数器,每当有地方引用时,计数器加1;当引用实效时,计数器减1;当计数器为0时就是对象不能再引用的时候。
但是虚拟机并非通过引用计数算法来判断对象是否存活的。
缺点:如过循环引用则计数器永远不会为0。

2.可达性分析算法

思路:通过一系列称作”GC Roots”的对象作为起始点,从这些子节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

如图:

image

Java中,可作为GC Roots的对象包括:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    栗子:方法里的局部变量:User user = new User();
  2. 方法区中类静态属性引用的对象。
    栗子:private static User user = new User();
  3. 方法区中常量引用的对象。
    栗子:private static final User user = new User();
  4. 本地方法栈中JNI(Native)引用的对象。
  • 引用

JDK1.2后,Java对引用进行了扩充:强引用(Strong Reference)、软引用(Soft Refrence)、弱引用(Weak Refrence)、虚引用(Phantom Refrence)

4种引用强度依次减弱

1.强引用(Strong Reference):

只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。

2.软引用Soft Refrence):

有用但非必需的对象。软引用关联的对象在OOM发生之前,这些对象会被列入回收范围中进行第二次回收,如果这次回收还没有足够的内存则抛出OOM异常。

3.弱引用(Weak Refrence):

与软引用一样,但其强度比软引用弱些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

4.虚引用(Phantom Refrence):

也叫幽灵引用、幻影引用,它是最弱的引用。无法通过虚引用获取到一个对象的实例,为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

  • 生存还是死亡?

可达性分析算法中不可达的对象也并非是“非死不可”,她们只是处于“缓刑”阶段。要真正宣告一个对象死亡必须经历两次标记:

  1. 如果对象在可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记且进行一次筛选(筛选条件:此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”)。

  2. 如果该对象被标记为有必要执行finalize()方法,那么这个对象会被放置在F-Queue对列中,稍后由虚拟机自动建立的、低优先级Finalizer线程去执行它。稍后GC会对F-Queue中对象进行第二次小规模标记,如果对象在finalize()方法中拯救了自己那么它就会被移出“即将回收”的集合。否则将会被回收。

finalize()方法是对象自救的最后一次机会。

  • 垃圾收集算法
  1. 标记-清除算法

    首先标记需要回收的对象,在标记完成后统一回收所有被标记的对象。
    缺点:标记和清除效率低,而且在标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续需要分配大内存的对象时无法找到足够连续内存而不得不提前触发另一次垃圾收集工作。

  2. 复制算法

    为了解决效率问题和碎片问题。复制算法将内存划分为大小相等的2块。每次分配内存的时候只使用其中的一块儿,当这一块儿用完了就将存活的对象复制到另一半内存上,然后将当前内存空间一次性清理掉。从而解决了内存碎片和效率问题。
    缺点:该算法代价是将内存缩小了原来的一半。

  3. 标记-整理算法

    复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将降低,老年代一般不能直接选用该算法。
    根据老年代的特点,“标记-整理”算法诞生,标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向另一端移动,然后直接清理掉端边界以外的内存。

  4. 分代收集算法

    根据对象存活周期不同将内存划分为几块,一般将Java堆分为新生代和老年代。
    新生代每次垃圾收集时发现都有大批对象死去,只有少量存货,则选用复制算法。
    老年代因为对象存活率高、没有额外空间对它进行分配担保,必需使用“标记-清理”或“标记-整理”算法进行回收。

  • 垃圾收集器
名称 线程模式 特点 算法 应用
Serial收集器 单线程 必需暂停其他工作线程 新生代-“复制算法” Client模式
Serial Old收集器 多线程 必需暂停其他工作线程 老年代-“标记-整理”算法 Client模式
ParNew收集器 多线程 除Serial它是唯一可以与CMS收集器配合工作的 新生代-“复制算法” Client模式
Parallel Scavenge收集器 并行多线程 关注达到一个可控制的吞吐量 新生代-“复制算法” 后台运算
Parallel Old收集器 并行多线程 关注达到一个可控制的吞吐量 老年代-“标记-整理”算法 Server模式
CMS收集器 多线程并发 并发收集低停顿 “标记-清除”算法 互联网或B/S系统服务端
G1收集器 多线程并行并发 并行并发、分代收集、空间整合、可预测的停顿 “标记-整理”、“复制算法” 最新成果暂未应用到生产环境
  • 内存分配与回收策略
  1. 对象优先在Eden分配
  2. 大对象直接进入老年代
  3. 长期存活的对象进入老年代
  4. 动态对象年龄判断
  5. 空间分配担保

本文所有知识均来自周志明-《深入理解Java虚拟机》一书学习

如果帮到了你,想打赏支持,喏~