深入理解 Java 虚拟机



前言

有输入肯定要有输出。

内存区域划分

  • 程序计数器
    唯一一个没有规定 OOME 的地方。执行 Java 方法时记录当前方法的 JVM 指令地址;执行 Native 方法时,计数器为空。
  • Java 虚拟机栈
  • 本地方法栈

  • 内存管理的核心区域,分为新生代和老年代,再细致一点分为 Eden 空间、From Survivor、To Survivor 等
  • 方法区
    所有线程共享的内存区域,存储元数据,如类结构信息、字段、方法等。无法满足内存分配需求时,抛出 OOME。
    • 运行时常量池
      方法区的一部分,存放编译期生成的各种字变量,还有运行时的符号引用等。例如 String.intern() 后放置的地方。
  • 直接内存
    JDK 1.4 引入 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。
  • Code Cache
    JIT Compiler 在运行时对热点方法的编译;GC 运行时部分需要占用的空间等,但 JVM 规范中并没有涉及。

OutOfMemoryError

堆溢出

不断创建对象并且无法回收

栈溢出

在单线程下,当内存无法分配时,虚拟机抛出通常是 StackOverflowError 异常。

方法区运行时常量池溢出

JVM 对于方法区的回收非常不积极,例如老版本的 JDK 处理 String.Intern() 时占用太多空间。随着元数据区的移除永久代,类元数据只受本地内存影响。

本机直接内存溢出

垃圾回收不会主动收集 Direct Buffer,需要自己手动调 System.gc()。另外。不会在 Heap Dump 中看见。

垃圾回收

判断对象是否存活,引用计数法很难解决循环引用的问题,所以才有了可达性分析

GC Roots 的对象包括以下几种:

  • 虚拟机栈中引用的对象
  • 方法去类静态属性引用的对象
  • 方法去常量引用的对象
  • Native 方法引用的对象

可达性分析算法

至少两次标记过程,第一次标记筛选出有必要执行的 finalize() 方法的对象,对象可在 finalize() 中自救一次,因为 finalize() 方法只会被系统自动调用一次。

回收算法

  • 标记-清除算法
    效率不高,会产生大量不连续的内存碎片
  • 复制算法
    新生代采用复制算法收集内存,大对象通过分配担保机制进入老年代
  • 标记-整理算法
    将存活的对象移至一端,减少内存碎片
  • 分代收集算法
    根据年代特点采用最适当的算法,需要上述算法支持

内存分配

都可通过参数设定

  • 对象优先在 Eden 中分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
    年龄计数器
  • 动态对象年龄判断
    Survivor 空间中相同年龄所有对象大小总合大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就直接进入老年代
  • 空间分配担保
    需要检查老年代的空间是否满足新生代所有对象空间,不够的话还要查看设置的值是否允许冒险,以决定 FullGC 是否有必要

类加载

  • 加载
    获取字节流,生成对象,提供数据访问入口
  • 验证
    是否符合虚拟机规范
  • 准备
    为变量分配值
  • 解析
    将符号引用替换为直接引用
  • 初始化
    执行类构造器 () 方法
------------- The End -------------
「不为五斗米折腰------真香」
0%