写点什么

了解 Java 架构

用户头像
陈皮
关注
发布于: 2020 年 06 月 21 日
了解 Java 架构



Java 基本架构如下:





1. 类加载子系统

Java 程序执行时,使用类加载系统加载 .class 文件到内存中。这被称为 Java 的动态类加载功能。在运行期,类加载器在第一次引用类时会对该文件加载、链接、初始化。

1.1 加载

类加载器的主要功能是加载编译后的字段码类(.class 文件)到内存中。通常先加载包含 main 方法的主类,所有后续的类加载尝试都是根据已运行的类中的类引用完成的,如以下情况所述:

  • 静态引用(e.g. System.out)

  • 创建对象(e.g. Person person = new Person())

有 3 种类加载器(根据类的继承关系划分),遵循 4 个主要的原则。

1.1.1 可见性原则

子类类加载器可以看到基类加载器加载的类,但是基类加载器不能查找到子类加载器加载的类。

1.1.2 唯一性原则

  • 基类加载的类不能再次被子类加载

  • 保证类不会被重复加载

1.1.3 委托原则 - Delegation Hierarchy Principle

为了以上两个原则,对每一个加载请求 JVM 都遵循一个选择类加载器的委托层级。从最底层的子类开始,应用程序类加载器将接收到的类加载请求委托给扩展类加载器, 然后扩展类加载器把加载请求委托给启动类加载器。如果请求加载的类在启动路径下,则类加载成功。否则请求返还给该扩展类加载器,在扩展路径或者自定义路径下查找类。如果再次失败,请求退回应用程序类加载器从系统类路径下查找。如果系统类路径下也没有找到则抛出异常:java.lang.ClassNotFoundException。





1.1.4 不可卸载原则

类加载器能够加载类,但不能卸载已经被加载的类。类加载器可以被删除并重建。

  • 启动类加载器(BootSrap Class Loader)从 rt.jar 加载标准 JDK 类,比如定义在启动目录下的核心的 Java API 类 - $JAVA_HOME/jre/lib (e.g. java.lang.* 包下的类)。是 Java 中所有类加载器的基类。

  • 扩展类加载器(Extension Class Loader)将请求委托给他的基类启动类加载器,如果不成功从扩展目录下加载(如: 扩展目录下的安全扩展功能)- JAVA_HOME/jre/lib/ext 或者其他由 java.ext.dirs 指定的系统属性。由 sun.misc.LauncherJAVAHOME/jre/lib/ext或者其他由java.ext.dirs指定的系统属性。由sun.misc.LauncherExtClassLoader 类实现。

  • 系统/应用类加载器(System/Application Class Loader)从系统类目录下加载应用指定的类,可以通过 cp 或者 -classpath 命令参数指定。它在内部使用映射到 java.class.path 的环境变量。由 sun.misc.Launcher$AppClassLoader 类实现。

注:除了以上 3 种主要的类加载器,程序员可以自定义类加载器。这通过类加载器委托模型保证了应用程序的独立性。这种方法用于Tomcat之类的Web应用程序服务器中,以使Web应用程序和企业解决方案独立运行。

1.2 链接(Linking)

这通过类加载器委托模型保证了应用程序的独立性。这种方法用于 Tomcat 之类的 Web 应用程序服务器中,以使 Web 应用程序和企业解决方案独立运行。是实现 Java 的动态性的重要一步。

  • 在链接一个类或接口之前,必须将其完全加载。

  • 在初始化类或接口之前,必须对其进行完全验证和准备(在下一步中).

  • 如果在链接过程中发生错误,则会在程序中的某个位置引发错误,该错误将由程序执行,而这些操作可能直接或间接地需要链接到错误所涉及的类或接口。

链接在一下 3 个阶段发生。

1.2.1 验证(Verification)

价差 .class 文件的正确性(是否符合 Java 语言规范?它是由有效的编译器根据JVM规范生成?)。这是类加载过程中最复杂的测试过程,并且耗时最长。即使链接减慢了类加载过程的速度,它也避免了在执行字节码时多次执行这些检查的需要,从而使整体执行高效而有效。如果验证失败,则会抛出运行时错误(java.lang.VerifyError)。

  • consistent and correctly formatted symbol table

  • final methods / classes not overridden

  • methods respect access control keywords

  • methods have correct number and type of parameters

  • bytecode doesn’t manipulate stack incorrectly

  • variables are initialized before being read

  • variables are a value of the correct type

1.2.2 准备(Preparation)

为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。静态字段已创建并初始化为其默认值,但是在此阶段不执行任何初始化程序或代码,因为这是初始化的一部分。

1.2.3 解析(Resolution)

用直接引用替换类型中的符号引用。通过搜索方法区域以找到引用的实体来完成此操作。

1.3 初始化(Initialization)

在这里,将执行每个加载的类或接口的初始化逻辑(例如,调用类的构造函数)。

这是类加载的最后阶段,所有静态变量都分配有代码中​​定义的原始值,并且将执行静态块(如果有)。在类中从上到下,从类层次结构中的父级到子级逐行执行。

2 运行时数据区(Runtime Data Area)





运行时数据区是 JVM 程序在 OS 上运行时分配的内存存储区。除了读取 .class 文件之外,类加载器子系统还会生成相应的二进制数据,并将以下信息分别保存在每个类的方法区 中:

  • 加载的类及其直接父类的全类名

  • .class 文件是否与其他 Class/Interface/Enum 相关

  • 修饰符,静态变量和方法信息等

然后,对于每个已加载的 .class 文件,它都会按照 java.lang 包中的定义,恰好创建一个Class对象来表示堆内存中的文件。此Class对象可用于稍后在我们的代码中读取类级别信息(类名称,父名称,方法,变量信息,静态变量等)。

2.1 方法区(线程间共享)

这是一个共享资源(每个 JVM 仅 1 个方法区域)。所有 JVM 线程共享相同的 Method 区域,因此对 Method 数据的访问和动态链接的过程必须是线程安全的。

方法区域存储类级别的数据(包括静态变量),例如:

  • 类加载器引用

  • 运行时常量池 - 数字常数,字段引用,方法引用,属性;以及每个类和接口的常量,它包含方法和字段的所有引用。引用方法或字段时,JVM 使用运行时常量池在内存中搜索该方法或字段的实际地址。

  • 字段数据 - 每个字段:名称,类型,修饰符,属性。

  • 方法数据 - 每个方法:名称,返回类型,参数类型(按顺序),修饰符,属性

  • 方法代码 - 每个方法:字节码,操作数堆栈大小,局部变量大小,局部变量表,异常表;异常表中的每个异常处理程序:起点,终点,处理程序代码的PC偏移,捕获的异常类的常量池索引

2.2 堆(线程间共享)

这也是一个共享资源(每个JVM仅1个堆区域)。所有对象及其对应的实例变量数组的信息都存储在堆区域中。由于“方法”和“堆”区域共享多个线程的内存,因此“方法和堆”区域中存储的数据不是线程安全的。堆是 GC 的主要目标

2.3 栈

这不是共享资源。对于每个 JVM 线程,当线程启动时,都会创建一个单独的运行时栈存储方法调用。对于每个此类方法调用,将创建一个条目并将其添加(推入)到运行时栈的顶部,并且该条目称为栈帧(Stack Frame)。

每个堆栈帧都有局部变量数组,操作数栈和正在执行的方法所属的类的运行时常量池的引用。局部变量数组和操作数栈的大小在编译时确定。根据该方法固定栈帧的大小。

当方法正常返回或在方法调用期间抛出未捕获的异常时,将删除栈帧。 如果发生任何异常,栈轨迹的每一行(显示为诸如 printStackTrace()之类的方法)表示一个栈帧。栈区不是共享资源,因此是线程安全的

2.3.1 栈帧

分为三个子实体:

2.3.1.1 局部变量数组

它的索引从0开始。对于特定的方法,涉及多少个局部变量,相应的值存储在此处。0是该方法所属的类实例的引用。从1开始,保存发送到方法的参数。在方法参数之后,将保存方法的局部变量。

2.3.1.2 操作数栈

如有需要,它可以充当运行时工作空间来执行任何中间操作。每个方法在操作数栈和局部变量数组之间交换数据,并推送或弹出其他方法调用结果。可以在编译期间确定操作数栈空间的必要大小。

2.3.1.3 帧数据(Frame Data)

与该方法有关的所有符号都存储在这里。另外,catch 块信息也将保留在帧数据中。

因为这是运行时栈帧,当线程终止后,该栈帧会被 JVM 销毁。

栈可以固定或动态分配大小。如果线程需要的栈大于允许的栈空间,则会抛出 java.lang.StackOverflowError 异常。如果一个线程需要一个新的帧,但是没有足够的内存来分配它,则抛出 java.lang.OutOfMemoryError。

2.4 程序计数器 - PC(Program Counter) Registers

对于每个JVM线程,当线程启动时,将创建一个单独的 PC(程序计数器)寄存器,以保存当前正在执行的指令的地址(方法区中的内存地址)。如果当前方法是本地方法,则PC是未定义的。执行完成后,PC寄存器将更新为下一条指令的地址。

2.5 原生方法栈

Java 线程和本机操作系统线程之间存在一个映射。在为 Java 线程准备了所有状态之后,还将创建一个单独的本机堆栈,以存储通过 JNI(Java本机接口)调用的本机方法信息(通常用C / C ++编写)。

创建和初始化本机线程后,它将回调 Java 线程中的 run() 方法。当run() 方法返回时,将处理未捕获的异常(如果有),然后本机线程确认是否需要由于线程终止而终止 JVM(即它是最后一个非守护线程)。 线程终止时,将释放本机线程和 Java 线程的所有资源(本机线程先释放,Java 线程对象交由 GC 回收)。

Java 线程终止后,将回收本机线程。操作系统负责调度所有线程并将其分配给任何可用的 CPU。

3 执行引擎

字节码的实际执行在这里进行。执行引擎通过读取分配给上述运行时数据区的数据逐行执行字节码中的指令。

3.1 解释器(Interpreter)

解释器解释字节码并一对一执行指令。它可以快速解释一个字节码行,但是执行解释后的结果是一项较慢的任务。缺点是,当多次调用一个方法时,每次都需要新的解释和较慢的执行。

3.2 Just-In-Time(JIT) 编译器

如果只有解释器可用,则当多次调用一个方法时,每次也会进行解释,如果有效处理,这将是多余的操作。使用JIT编译器已经可以做到这一点。 首先,它将整个字节码编译为本机代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。

但是,即使对于 JIT 编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT 编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个方法。自适应编译的想法已在 Oracle Hotspot VM 中使用。

当 JVM 供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能。

  • 中间代码生成器生成中间代码。

  • 代码优化器负责优化上面生成的中间代码。

  • 目标代码生成器负责生成本机代码(即机器代码)。

  • Profiler 是一个特殊的组件,负责查找性能热点(例如,多次调用一种方法的实例)。

3.2.1 供应商的优化方法

3.2.1.1 Oracle Hotspot VMs

Oracle 有两种流行的 JIT 编译器模型 Hotspot Compiler,用于实现其标准 Java VM 的两种实现。通过分析,它可以确定最需要 JIT 编译的热点,然后将代码的那些性能关键部分编译为本机代码。随着时间的流逝,如果不再频繁调用这种已编译的方法,它将把该方法标识为不再是热点,并迅速从缓存中删除本机代码,并开始以解释器模式运行。这种方法可以提高性能,同时避免不必要地编译很少使用的代码。此外,Hotspot Compiler 可以即时确定使用 lining 等技术来优化已编译代码的最佳方式。编译器执行的运行时分析使它可以消除在确定哪些优化将产生最大性能收益方面的猜测。

这些虚拟机使用相同的运行时(解释器,内存,线程),但是将自定义构建 JIT 编译器的实现,如下所述。

  • Oracle Java Hotspot Client VM 是 Oracle JDK 和 JRE 的默认 VM 技术。它通过减少应用程序启动时间和内存占用量而在客户端环境中运行应用程序时进行了优化,以实现最佳性能。

  • Oracle Java Hotspot Server VM 旨在为在服务器环境中运行的应用程序提供最高的程序执行速度。这里使用的JIT编译器称为“高级动态优化编译器”(Advanced Dynamic Optimizing Compiler),它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如 Java server MyApp)来调用 Java HotSpot Server VM。

Oracle 的 Java Hotspot 技术以其快速的内存分配,快速高效的 GC 以及易于在大型共享内存多处理器服务器中扩展的线程处理能力而闻名。

3.2.1.2 IBM AOT (Ahead-Of-Time) Compiling

这里的特色是这些 JVM 共享通过共享缓存编译的本机代码,因此已经通过 AOT 编译器编译的代码可以由另一个 JVM 使用,而无需编译。此外,IBM JVM通过使用AOT编译器将代码预编译为JXE(Java可执行文件)文件格式,提供了一种快速的执行方式。

3.2 垃圾回收器

只要引用了一个对象,JVM 就会认为它是活动的。一旦不再引用对象,因此应用程序代码无法访问该对象,则垃圾收集器将其删除并回收未使用的内存。通常,垃圾回收是在后台进行的,但是我们可以通过调用 System.gc() 方法来触发垃圾回收(同样无法保证执行。因此,请调用 Thread.sleep(1000)并等待 GC 完成)。



JVM 通过可达性分析算法进行垃圾对象的识别:从线程栈帧中的局部变量,或者是方法区静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记过的对象都是被使用的对象,而那些没有地被标记的对象就是可回收的垃圾对象。



回收三种放法:



  • 清理

  • 压缩

  • 复制



3.2.1 分代垃圾回收



web 应用中大部分对象的生命周期很短,在一个线程中处理完一次请求就不被引用了。 JVM 堆被划分为 Eden(新创建对象)、S0、S1、老年代(长时间被引用对象)四个区。对象创建后会住 Eden 区,当发生垃圾回收时,Eden 区中不被引用的对象将被销毁,存活对象会复制到 S0(或 S1)中。在多次 GC 后仍存活的对象会被复制到老年代。



3.2.2 垃圾回收算法



后台 GC 线程处理垃圾回收逻辑。





4 Java Native Interface(JNI)

该接口用于与执行所需的本机方法库进行交互,并提供此类本机库的功能(通常用 C/C++ 编写)。这使 JVM 可以调用 C/C++ 库,并可以由特定于硬件的 C/C++ 库调用。

5 原生方法库

这是执行引擎所需的 C/C++ 本机库的集合,可以通过提供的本机接口进行访问。

6 Reference

understanding-jvm-architecture

understanding-java-memory

Java Garbase Collection Basics



发布于: 2020 年 06 月 21 日阅读数: 87
用户头像

陈皮

关注

还未添加个人签名 2018.04.26 加入

还未添加个人简介

评论

发布
暂无评论
了解 Java 架构