理解JVM架构

为了更好的使用JAVA生态系统的组件,每个JAVA开发者都应该学习理解JVM结构以及JAVA后台是怎么工作的。
这篇文章将会介绍夯实的JVM的内部架构和技术。

写作背景


java是一个拥有上千万开发者的多范式(以类为基础的面向对象、结构化、指令型、广泛型、反射型、并发型)程序语言。
在所有的知名排名网站、java已经位居榜首连续15年,数以万计的企业级应用程序使用java语言编写。是编写企业级应用程序
的首选语言

我会翻译三篇文章
理解JVM架构
理解JAVA内存模型
JAVA垃圾回收内部工作原理

JAVA环境

对任何程序语言来说,都需要构造一个环境。
为了能够开发、编译、排查错误、和执行应用程序
这个环境会包含所有需要的组件、应用程序接口、lib库等
JAVA提供两种环境
每个开发者在开发应用程序之前都需要设置好对应的环境

  • JRE(Java Runtime Environment):最小的java程序运行环境(不支持开发)
    JRE包含JVM(Java Virtual Machine)和部署相关的组件。
  • JDK(Java Development Kit):用来开发和运行Java应用程序的完整的开发环境。
    包含JRE和一些开发工具以及debug组件。

JRE用来运行开发同事写好的java程序,JDK是程序员用来写JAVA代码的环境

JAVA运行原理

我们可以用任何终端编辑器(VIM、nano)或者图形界面编辑器(gedit、sublime)写一个简单的Java程序.
但是为了实现复杂的Java应用程序,我们需要一个IDE(Intergrated(集成) Development Environment)
比如 IntelliJ IDEA、Eclipse、Netbeans.
一个经典的Java应用程序需要包含正确的Java语言语法和正确的.java格式
建议使用OOP(Object Oriented Programming)之类的编程概念和适当的体系结构模式,方便后续的维护。

Java语言优势是它的设计理念WORA(write once,run anywhere).
Java源代码使用javac(此工具来自JDK)编译器编译成一个叫做bytecode的中间态(.classs 文件)
这些bytecode是带有操作行数、十六进制(hexadecimal)格式的文件。

JVM可以将这些指令解释成操作系统和底层硬件平台可以理解的本机语言(不需要进一步编译)。
因此,bytecode 扮演着一个独立于操作系统平台的中间态,可以在任意JVM之间移植,不需要关心
底层操作系统和硬件架构如何。
JVM可以运行应用程序、帮助代码和底层操作系统以及硬件通讯
因此我们的操作系统支持(Windows、Linux、Mac),cpu(x86、x64)。

大多数人知道上面的JAVA相关故事,上面故事中的JVM对我们来说就像一个黑盒子,JVM神奇的解释bytecode,
并且执行一些java程序运行时的其他操作比如JIT(just-in-time)compilation &GC(Garbage Collection),
后面我们将会揭秘JVM的运行原理。

JVM架构

JVM只是一个规范,如何实现这个规范因厂商不同而不同,下面我们来看一下规范中定义的通用(commonly-accept)JVM架构。

Class Loader Subsystem

JVM运行在RAM(实际的内存,后续写RAM指的就是内存)之上,在程序运行期间,使用Class Loader Subsystem(JVM中的类装入系统)
将.class文件加载到RAM中,这个就叫做动态类加载(dynamic class loading). (灯泡亮)这个Class Loader Subsystem将会
程序运行时第一次引用类的时候(并不是编译的时候)loads、links、initializes  .class文件。

Loading

Class Loader的主要工作是加载已编译的类(.class文件,十六进制bytecode)到内存中。
Class Loade通常从main class(class with static main() method declaration)开始加载
后续的类加载都是根据已经运行的类中的类引用完成的,比如:

  • 当bytecode对一个类进行静态引用时
  • 当bytecode创建一个类对象时

有三种类型的类装入器,它们遵守以下四个准则

Visibility Principle(能见度准则)

子Class loader可以访问父Class loader加载的类,父Class loader不能访问子Class loader加载的类

Uniqueness Principle(唯一性准则)

被父Class loader 加载过的类不可以再被子Class loader加载,这样可以保证类不会被重复加载

Delegation Hierarchy Principle(委托顺序准则)

为了满足(satisfy)第一和第二准则,JVM遵循一个委托(类加载)顺序准则,当收到一个类加载请求
的时候,从最低级的类加载器(Application Class loader)开始,Application Class Loader将类加载请求
委托给Extension Class Loader,Extension Class Loader再将类加载请求委托给Bootstrap Class Loader.
如果请求加载的类在Bootstrap Class Loader的Class Path中找到,则Bootstrap Class Loader对该类进行加载,
如果在Bootstrap Class Loader的Class Path中没有找到该类则将类加载请求传回Extension Class Loader,
Extension Class Loader也确认Class Path中是否有请求加载的类,有的话加载改类,没有的话将请求
传回到Application Class loader,然后继续确认Class Path中是否有请求加载的类,有则进行类加载,
如果没有则会抛出Exception–java.lang.ClassNotFoundException

No Unloading Principle

Class Loader只可以加载一个类,但是并不能将一个已加载的类unload,取而代之的是将整个Class Loader删除
重新创建一个Class loader。(就像表不能删除数据,直接drop掉然后新建一张空表)

  • Bootstrap Class Loader:加载JDK自带rt.jar中的Class,比如核心Java API 类(java.lang.* package classes)在bootstrap path

$JAVA_HOME/jre/lib 目录下,由C/C++等本地语言实现,充当所有Class loader的父类。

  • Extension Class Loader:向父Class loader(Bootstrap Class Loader)委托类加载请求,如果没有成功则在

扩展目录($JAVA_HOME/jre/lib/ext 或者其他java.ext.dirs声明的目录)中寻找需要加载的类,Extension Class loader

是由 sun.misc.Launcher$ExtClassLoader class使用java语言实现的

  • System/Application Class Loader:从系统的Class path中加载应用声明的类,可以使用-cp 或者 -classpath命令行选项

在调试应用时设置,-cp和-classpath内部使用映射到java.class.path的环境变量。System/Application Class Loade是由 

sun.misc.Launcher$ExtClassLoader class使用java语言实现的。

除了以上介绍的三个Class laoder,程序员可以在代码中直接创建一个用户定义的Class loader

每个Class loader都有自己的namespace存放已加载的类的名称,当收到类加载的请求时,首先检查FQCN(Fully Qualified Class Name)
是否已经在namespace中存在,如果已存在说明已经加载过了,如果类的名称一致但是在不通的namespace中。则表明示是不同的类。

Linking

Linking用来认证以及准备已加载的类或者接口,是直接的超级类或者超级接口,Linking有以下属性。

  • 类或者接口在Link之前必须已经加载。
  • 类或者接口在initialized之前必须已经认证并且准备好。
  • 如果在link过程中发生错误,那一定是程序执行某些动作时需要进行link错误中涉及的类或者接口。

Link的过程分以下三个步骤。

  • Verification(认证): 确定.class文件的正确性(代码是否按照Java语言的规范编写,.class文件是否是有效编译器(遵循JVM编译规范)编译过的等等)。
    整个认证过程有非常多的验证项,需要较长的时间,所以link操作拉低了整个类加载的速度,但是避免了在bytecode运行时进行重复的检查。
    因此link加快了程序的整体运行速度。如果Verify过程中出错,则会抛出以下错误(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
  • Preparation(准备):为静态存储或者JVM用到的结构化数据(类似method table)分配内存,静态field(列)被创建赋值为默认值。但是并没有发生代码执行和初始化操作。
  • Initialization(初始化):所有已加载的类和接口的初始化逻辑在这个阶段执行(e.g. 调用类的接口函数),因为JVM是多线程的,所以
    初始化类和接口时候需要将当前线程在初始化这个类的信息广播到所有的线程,避免类的重复初始化(i.e. make it thread safe). 线程安全)(警告)
    初始化是类加载的最后一步,所有的静态变量都会被赋予代码中定义的初始化值,静态块将会被执行(如果有的话),
    从父加载器开始到子加载器的类将会从第一行到最后一行依次执行。

Runtime Data Area

Runtime Data Area 是JVM在操作系统运行的内存占用区域,Class loader subsystem除了读取.class文件,还为每个独立的类生成对应的
二进制数据文件并保存在方法区中(Method Area)

  • 已加载的类的完整名字,和它的直接父类
  • .class文件是否和其他的类、接口、枚举有关联
  • 修饰符、静态变量、和其他Method信息等

每个已加载的类都会在heap中创建一个类对象,这个类对象用来在后续的代码中读取类的以下信息
(类名称、类的父级名称、methods、变量信息、静态变量等)

Method Area(线程之间共享使用)

这是一块共享的内存资源(每个JVM 只有一个Method Area),所有的JVM线程共享这一块内存区域,
所以读取method Area的数据或者程序的动态链接(Dynamic Linking)必须是线程安全(thread safe)的。(和之前的线程安全解释一致,需要同步线程间信息)

Method Area存放类级别的数据(例如静态变量),包括以下信息:

  • 运行时常量池–数字常量、字段引用、方法引用、属性以及每个类或者接口的常量,包含所有方法和字段的引用,
    当一个方法或者字段被引用,JVM会在运行时常量池中搜索这个方法或者字段的实际内存地址
  • 字段数据(field data)–-每个字段:名称、类型、修饰符、属性
  • 方法数据(method data)–每个方法:名称、返回类型、参数类型、修饰符、属性
  • 方法代码(method code)–每个方法:字节码(bytecode)、操作堆栈大小、本地变量大小、本地变量表、异常类型

Heap Area(线程之间共享使用)

这也是一个线程共用的内存区域(每个JVM只能有一个heap区),所有的对象和对象对应的实例变量以及数组都存储在
heap区中,因为Method Area和heap区域数据线程的共享内存区域,没有存放持久化的数据,所以很适合GC(garbage collection)。

Stack Area(线程独立使用)

非共享内存区域,当一个线程开始运行,一个独立的运行时栈在内存中创建,用来存储每一次的方法调用(method calls)
每一次的方法调用,一行调用信息就会被写入到运行时栈的最上面,多行方法调用堆积到一起就是我们报错或者thread dump中
显示的Stack Frame。

Leave a Reply