在之前的文章 一步步解析 java 执行内幕 中, 比较详细分析了 java 代码是如何一步一步在 jvm 中执行的, 然而涉及到的 jvm 核心技术点, 并未做深入分析, 因为觉得那时候分享, 还不是时候, 庆幸的是, 最近刚优化线上商城并发系统, 相关优化记录在上篇博文 记一次线上商城系统高并发的优化 , 分享这篇文章后, 觉得是时候与大家分享交流 jvm 底层一些核心技术的时机了.
本篇文章将重点分析 jvm, 涉及到的内容包括 jvm 内存模型, 类加载器, GC 回收算法, GC 回收器, 整体偏向于理论.
本篇文章不适合初学者, 适合具有 3 年以上开发经验的技术人员, 欢迎大家一起交流分享, 文章若有不足之处, 欢迎读者朋友们指出, 先感谢.
一 明确 jdk,jre 和 jvm 之间关系
下图为官网关于 jdk,jre 和 jvm 的架构图, 从该架构图, 很容易看出三者之间关系:
(1)jdk 包含 jre, 而 jre 又包含 jvm
(2)jdk 主要用于开发环境, jre 主要用于发布环境, 当然, 发布环境用 jdk 也没问题, 仅仅是性能可能会有点影响, jdk 与 jre 关系有点类似程序 debug 版本和 release 版本之间关系
(3)从文件大小来说, jdk 比 jre 大. 从图中可以看出, jdk 比 jre 多了一层工具包, 如常用的 javac,java 命令等
二 类加载器
关于 jvm 类加载器, 可概括为如下图:
1. 为什么要有类加载器?
(1)将字节码文件加载到运行时数据区..java 源码通过 Javac 命令编译后形成的字节码文件(.class), 通过类加载器加载进入 jvm 中的.
(2)确定字节码文件在运行时数据区的唯一性. 相同的字节码文件, 通过不同的类加载器, 就形成不同的文件, 因此字节码文件在运行时数据区的唯一性是由字节码文件和加载它的类加载器共同决定的
2. 类加载器的种类
从种类上来划分, 类加载器主要划分为四大类
(1)启动类加载器 (根类加载器 Bootstrap ClassLoader): 该类加载器位于类加载器的最顶层, 主要加载 jre 核心相关 jar 包, 如 /jre/lib/rt.jar
(2)扩展类加载器(Extension ClassLoader): 该类加载器位于类加载器层次的第二层, 主要加载 jre 扩展相关 jar 包, 如 / jre/lib/ext/*.jar
(3)应用程序类加载器 (Application ClassLoader) App: 该类加载器位于类加载器的第三层, 主要加载类路径(classpaht) 下的相关 jar 包
(4)用户自定义类加载器(User ClassLoader): 该类加载器为用户自定义类加载器, 主要加载用户指定的路径下的相关 jar 包
3. 类加载器的机制(双亲委派)
对于字节码的加载, 类加载机制为双亲委派, 什么叫双亲委派呢?
类加载器获取字节码文件后, 不是直接加载, 而是将该字节码文件传递给其直接父级类加载器, 其直接父加载器又继续传递给其直接父加载器的直接父加载器, 依次类推到根父加载器, 若根父加载器
能加载, 则加载, 否则交给其直接孩子加载器加载, 直接孩子加载器能加载就加载, 若不能, 依次类推其直接孩子类加载器, 若都不能加载, 最后才由用户自定义类加载器加载.
4.jdk 1.8 如何实现类加载器?
如下为 jdk 1.8 类加载器的实现, 采用递归方式
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- long t1 = System.nanoTime();
- c = findClass(name);
- // this is the defining class loader; record the stats
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
5. 破坏双亲委派模型
在某些情况下, 由于受加载范围限制, 父类加载器无法加载到需要的文件, 因此父类加载器需要委托其子类加载器去加载相应的字节码文件.
如在 jdk 中定义的数据库驱动接口 Driver, 但该接口的实现却由不同的数据库厂商来实现, 这就产生这样一个问题: 由启动类(Bootstrap ClassLoader)
执行的 DriverManager 要加载实现了 Driver 接口的相关实现类, 从而实现统一管理, 但 Bootstrap ClassLoader 只能加载 jre/lib 下的相应文件, 不能加载
由各个厂商实现的 Dirver 接口相关实现类(Dirver 实现类是由 Application ClassLoader 加载), 这时就需要 Bootstrap ClassLoader 委托其子类加载器加载 Driver
来实现, 从而破坏了双亲委派模型.
三 类的生命周期
java 中的类, 在 jvm 中的生命周期, 大概分为五个阶段:
1. 加载阶段: 获取字节码二进制流, 并将静态存储结构转化成方法区的运行时数据结构, 且在方法区生成相应的类对象(java.lang.Class 对象), 作为该类的数据访问入口.
2. 连接阶段: 该阶段包括三个小阶段, 即验证, 准备和解析三阶段
(1)验证: 确保字节码文件符合虚拟机规范要求, 如元数据验证, 文件格式验证, 字节码验证和符号验证等
(2)准备: 为内的静态表里分配内存, 并且设置 jvm 默认值, 对于非静态变量, 此阶段, 不需分配内存.
(3)解析: 将常量池内的符号引用转化为直接引用
3. 初始化阶段: 类对象使用前的一些必要初始化工作
如下引用自一位博友的观点, 个人认为解释得很好.
在 Java 代码中, 如果要初始化一个静态字段, 我们可以在声明时直接赋值, 也可以在静态代码块中对其赋值.
除了 final static 修饰的常量, 直接赋值操作以及所有静态代码块中的代码, 则会被 Java 编译器置于同一方法中, 并把它命名为 <clinit> . 初始化的目的是是为标记为
常量值的字段赋值, 以及执行 < clinit> 方法的过程. Java 虚拟机会通过加锁来确保类的 <clinit> 方法仅被执行一次.
哪些条件会发生类初始化呢?
(1)当虚拟机启动时, 初始化用户指定的主类(main 函数);
(2)当遇到用于新建目标类实例的 new 指令时, 初始化 new 指令的目标类;
(3)当遇到调用静态方法的指令时, 初始化该静态方法所在的类;
(4)子类的初始化会触发父类的初始化;
(5)如果一个接口定义了 default 方法, 那么直接实现或者间接实现该接口的类的初始化, 会触发该接口的初始化;
(6)使用反射 API 对某个类进行反射调用时, 初始化这个类;
(7)当初次调用 MethodHandle 实例时, 初始化该 MethodHandle 指向的方法所在的类.
4. 使用阶段: jvm 中使用对象
5. 卸载阶段: 将对象从 jvm 中卸载(unload), 哪些条件会使 jvm 发生类卸载呢?
(1)加载该类的类加载器被回收
(2)该类的所有实例已经被回收
(3)该类对应的 java.lang.Class 对象没有任何地方被引用
四 jvm 内存模型
1.JVM 内存模型是怎样的?
如下为 JVM 内存模型架构图, 由于在之前的文章中论述过, 这里就不再一 一论述, 主要讲解堆区.
在 jdk 1.8 前, 堆区主要分为新生代, 老年代和永久代. jdk 1.8 后, 去掉了永久代, 增加了 MetaSpace 区. 这里, 主要分享 jdk 1.8.
根据 jdk1.8, 堆区逻辑抽象为三个部分:
(1)新生代: 包括 Eden 区, S0 区(也叫 from 区),S21(也叫 TO 区)
(2)老年代
(3)Metaspace 区
2. 新生代和老年代的内存大小是怎样的?
根据官方建议, 新生代占三分之一(Eden:S0:S1=8:1:1), 老年代占三分之二, 因此内存分配图如下:
3.GC 回收是怎样进行的?
对象先在 Eden 区运行, 当 Eden 内存用占用满时, Eden 会进行两个操作: 回收不用的对象和将未回收对象放入 s0 区, 此时 s0 区和 s1 区互唤名称, 即 s0->s1,s1->s0,Eden 区经过一次对象回收后, 释放了空间, 当 Eden 下次再满时, 执行相同步骤, 依次循环执行, 当 Eden 区回收后, 剩下的对象超过 s0 容量, 则将出发一次 Minor GC, 此时将未回收的对象放入老年区, 依次循环执行, 当 Eden 区触发 Minor GC 时, 剩余的对象容量大于 old 区剩余容量时, 则 old 区将触发一次 Major GC, 此时便会触发一次 Full GC. 需要注意的是, 一般发生 Major GC, 基本都都会伴随一次 Full GC 回收, Full GC 非常损耗性能, 在 JVM 调优时, 要注意.
下图我在生产环境截的一张 GC 图, 监控工具 VisualVM
4. 垃圾回收算法有哪些?
(1)标记 - 清除算法
该算法分为 2 个阶段, 即标记阶段和清楚阶段, 首先标记所有要回收的对象, 然后回收被标记的对象. 该算法效率低, 且容易产生内存碎片.
a. 效率低: 需要遍历两次内存, 第一次标记, 第二次回收被标记对象
b. 由于是非连续内存片段, 容易产生碎片, 当对象过大时, 容易发生 Full GC
下图为标记 - 清除算法 回收前和回收后对比示意图
(2)标记 - 复制算法
该算法解决了 "标记 - 清除" 算法效率低和大部分内存碎片问题, 它将内存分为大小相等的两块, 每次只使用其中一块, 当其中一块需要回收时, 只需将该快区域还存活的对象复制到另一块, 然后再把该块内存一次性清理掉, 循环往复.
下图为标记 - 复制算法回收前和回收收简要示意图
然而, 由于年轻代大部分对象驻留时间都非常短, 98% 的对象都很快被回收, 存活的对象非常少, 不需要按照内存 1:1 来划分, 而是按照 8:1:1 来划分,
将 2% 存活的对象放在 s0(from 区)即可.
如下为按照 Eden:s0:s1 =8:1:1 划分示意图
(3)标记 - 整理算法
该算法分为两阶段, 即标记和整理, 首先标记所有存活对象, 将这些对象向一端移动, 然后直接清理掉端边界以外的内存. 由于老年代的对象存活时间比较长, 因此适合用该算法.
标记过程仍与 "标记 - 清除" 过程一致, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活对象向一端移动, 然后直接清理掉端边界以外的内存.
如下为 "标记 - 整理算法" 回收期和回收后示意图
(4)分代收集算法
该算法未目前 jvm 算法, 采用分代思想, 模型如下:
5. 常见 GC 回收器有哪些?
(1)SerialGC
SerialGC 又叫串行回收器, 也是最基础的 GC 回收器, 主要适用于单核 CPU, 新生代采用复制算法, 老年代采用标记 - 压缩算法, 在运行的过程中需要暂停应用程序,
因此会造成 STW 问题, 在 JVM 标注参数为:-XX:+UseSerialGC .
(2)ParallelGC
ParallelGC 基于 SerialGC, 主要解决 SerialGC 串行问题, 改为并行问题, 解决多线程问题, 但同样会产生 STW 问题, jvm 关键参数:
a.-XX:+UseParNewGC, 表示新生代并行(复制算法) 老年代串行(标记 - 压缩)
b.XX:+UseParallelOldGC, 老年代也是并行
(3)CMS GC
CMSGC 属于老年代回收器, 采用 "标记 - 清除算法", 不会发生 STW 问题, 在 jvm 中参数设置:
-XX:+UseConcMarkSweepGC, 表示老年代使用 CMS 收集器
(4)Garbage First
Garbage First 面向 jvm 垃圾收集器 , 它满足短时间停顿的同时达到一个高的吞吐量, 适用于多核 CPU 和大内存的服务端, 也是 jdk9 的默认垃圾回收器.
五 总结
本篇文章在之前文章 一步步解析 java 执行内幕 基础上, 深入分析了 JVM 内存模型, 其中重点分析了 jdk,jre 和 jvm 关系, jvm 类加载器, jvm 堆内存划分, GC 回收器和 GC 回收算法等, 整体偏向于理论, 由于篇幅有限, 本篇文章未分析这些技术在 JVM 实际调优中是如何运用的, 将在接下来的文章中与大家分享.
来源: https://www.cnblogs.com/wangjiming/p/13237378.html