Java-JVM知识整理

JVM 运行时分区

程序计数器, 方法区, 堆区, 虚拟机栈, 本地方法栈

JVM 参数前缀含义

为了了解后面 JVM 参数, 先了解下 JVM 参数前缀, 一个三种, 如下

  • - : 标准 JVM 选项
  • -X : 非标准 VM选项, 不保证所有 JVM 支持
  • -XX : 高级特性, 但属于不稳定的选项

程序计数器

当前线程执行的字节码指令的地址, 如分支, 循环等基础功能都需要依赖此完成, 如果执行的是原生的 (Native) 方法, 计数器的值为空, 每个线程都有一个程序计数器, 也是唯一没有定义 OutMemoryException 异常的区域

虚拟机栈和本地方法栈

Java 虚拟机栈也是线程私有的, 每一个方法都会创建一个栈帧, 里面存放方法的局部变量, 操作数栈, 动态链接, 方法出口等信息, 该区域会出现 OutMemoryException 异常和 StackOverflow 异常, 本地方法栈和虚拟机栈是一样的, 只不过调用的是 native 修饰的方法, 在虚拟机规范中并没有规定虚拟机栈和本地方法栈使用何种语言和方式实现, 像 HotSpot 虚拟机直接将这两者合在一起实现

  • 每个栈帧的大小和此区域的大小直接影响线程创建的数量和程序调用的方法数量, 使用 -Xss 设置帧栈容量大小 (助记: stack size)
  • JVM 没有提供栈容量大小的参数, 但是可以调整堆区和方法区的大小来设置, 假设 JVM 分配的总内存为 N, 则栈容量 = N - 最大堆区容量 - 最大方法区容量 - 程序计数器容量大小 (较小, 可忽略)

堆区

所有线程共享, 所有对象和数组都在堆区分配, 初始堆区为物理机内存的 1/64, 最大堆区为物理机内存的 1/4, 初始堆区可通过 -Xms (助记: memory size) 调整, 罪袋堆区可通过 -Xmx (助记: memory size max = msx, 计算机中约定表示方式用 x 表示大, 为了长度对齐, 使用了 -Xmx 表示最大堆区内存) 调整, 此区域会出现 OutMemoryException 异常

  • 堆区又分为新生代, 老年代和永久代
  • 新生代可以使用 -Xmn (助记: memory new)来调整初始大小和最大大小, 或者使用 -XX:NewSize, 最大值使用 -XX:MaxNewSize 来调整
  • 老年代没有可设置的参数, 可以通过设置新生代和永久代的大小来设置
  • 新生代和老年代还可以通过 -XX:NewRatio 来设置比例, 例如 -XX:NewRatio = 4, 表示新生代和老年代的比例为 1:4
  • 新生代由 Eden Space, Survivor 0 Space, Survivor 1 Space 三个区域组成, 这三个区域默认占比为 8:1:1, 可以通过 -XX:SurvivorRatio 来设置比例, 例如 -XX:SurvivorRatio = 8, 表示两个 Survivor Space : Eden Space = 2 : 8

方法区

Java 虚拟机规范规定了方法区是堆区的一个逻辑部分, 方法区通常还有另外两个叫法, 非堆 (Non-Heap) 和永久代 (Permanent Generation, 仅针对 HotSpot 虚拟机而言), 该区域也是所有线程共享的, 用于存放已加载类的信息, 常量, 静态变量, 编译后的代码信息等数据, 在 JDK 1.7 之前, 还会在永久代中划分一部分区域出来当作字符串常量池, JDK 1.7 的时候字符串常量池就被移出了, 到了 JDK 1.8, 永久代被元数据空间所替代

  • 持久代可以通过 -XX:PermSize 设置, 最大值可以通过 -XX:MaxPermSize 设置

对象存活算法

判断对象是否可以被回收的算法, 有引用计数法和可达性分析法两种

引用计数法

所有对象添加一个计数器, 每有一个地方引用就加 1, 引用失效时就减 1, 当被检测的对象的计数器为 0 时, 表示该对象没有被其他任何对象引用, 可以被回收了

  • 优点 : 实现简单
  • 缺点 : 无法解决对象之间相互引用的问题

可达性分析法

从 GC Roots 节点开始搜索, 当没有任意一条路径 (也称为引用链) 可以到达被检测的对象时, 表示该对象没有被其他任何对象引用, 可以被回收了, 这也当前主流虚拟机使用的算法

GC 算法

  • 标记-清除算法, 标记清除的效率都不高, 且容易产生碎片, 导致后面后面分配大对象时无法找到足够的连续内存导致 GC 发生并出现内存溢出异常
  • 标记-整理算法, 解决标记-清除算法容易产生碎片的缺点, 需要移动元素, 性能稍低
  • 复制算法, 分成两个相同大小的区域, 需要不需要清除的对象整理并移动到另一个区域, 完成后清除当前区域, 效率高, 但是浪费空间

分代 GC 算法

  • 不同的区域使用不同的算法, 新生代使用复制算法, 老年代使用标记整理算法
  • 新生代绝大部分的对象都是 “朝生夕死”, 真正能存活下来的对象比较少, 为了不浪费空间, 新生代分成了 Eden Space, Survivor 0 Space, Survivor 1 Space 三个区域, 每次使用 Eden Space 和一个 Survivor 区, 当需要进行 GC 时, 将 Eden Space 和当前使用的 Survivor 区中存活的对象复制到另一个 Survivor 区, 然后直接清除 Eden Space 和 Survivor Space, 这样可以大大提高空间使用率, 被浪费的空间最多为 10% (默认占比 1:1:8), 如果存活的对象占用的内存大于 Survivor Space, 这些对象就会直接放入老年代中, 如果老年代空间不够, 就会进行一次 Major GC, 如果还是不够就会出现 OOM 异常

对象何时进入老年代

  • 一般的对象在经过一定的年龄后, 会被移动到老年代, 这个年龄是经过 N 次 Minor GC 后还存活的对象, 默认是 15 次, 可以通过 -XX:MaxTenuringThreshold 设置
  • 进行 Minor GC 时, 如果存活的对象占用的内存大于 Survivor Space 的空间, 所有的对象会被直接放入老年代
  • 在新生代创建大对象时会直接放入老年代, 可以通过设置 -XX:PretenureSizeThreshold 设置这个值

常见垃圾回收器类型

  • 串行 (Seial) 回收器, 单线程回收器, 简单, 易实现, 效率高, 也是默认的回收器
  • 并行 (ParNew) 回收器, 串行回收器的多线程版本, 能有效利用 CPU, 减少回收时间
  • 吞吐量优先 (Parallel) 回收器, 侧重于吞吐量的控制
  • 并发标记清除 (CMS) 回收器, 最少回收停顿, 是基于标记清除算法实现的, 通常用于老年代的回收
  • G1 回收器, 基于分区算法

垃圾收集回收主要区域

  • 堆区, 新生代的 Minor GC, 和整个堆区的 Major GC (包括回收方法区)
  • 方法区, 该区域不是垃圾回收的主要区域, 该区域有大量的常量, 一般回收效率低

引用类型和回收规则

Strong Referance

强引用, 程序中 new 出来的对象即强引用, 只要引用还在, 来及回收器就不会回收这些对象

Soft Referance

软引用, JDK 中使用 SoftReferance 类实现, 通常描述一些有用但是非必需的对象, 在将要发生内存溢出之前的一次 GC 时会被清除

Weak Referance

弱引用, JDK 中使用 WeakReferance 类实现, 用于描述一些非必需的对象, 只要 GC 发现了弱引用的对象就会被回收

Phantom Referance

虚引用, JDK 中使用 PhantomReferance 类实现, 也称之为幽灵引用和幻影引用, 和软引用和弱引用不同, 他不能通过 PhantomReferance 的 get 方法获取到实例, 为一个对象设置虚引用仅是为了在这个对象回收前收到一个系统消息 (告诉虚拟机虚拟机该对象可以被 GC 回收了) 且必须和引用队列 (ReferenceQueue) 联合使用

finalize() 方法

finalize() 方法是 Object 中定义的一个方法, 在对象被 GC 回收器回收前会被调用, 且只会被调用一次, 例如对象在 finalize() 方法中实现对象的自我拯救, 又被引用, 下一次该对象再次被回收时就不会再调用 finalize() 方法 了, 虽然 finalize() 方法会在对象回收前调用, 但是并不建议使用此方法进行如回收资源的操作, 主要原因如下

  • GC 并不是可靠的, System.gc() 也只是通知 JVM 可以开始进行 GC 操作, 真正的 GC 操作并不是立即执行的, 导致资源不能及时释放
  • GC 时会执行对象的 finalize() 方法, 如果有耗时的操作, 会引起严重的 GC 性能问题, 更为严重的如果里面出现死循环, 会导致 JVM 的整个 GC 崩溃
本文结束,感谢您的阅读!