JVM的基本结构

JAVA虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上 不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。 比如kotlin、scala等。 另外JVM有很多种,不只是Hotspot,还有JRockit、J9等等。

JAVA的优势主要在于它的设计理念:write once, run anywhere。

虽然C++之类的语言会编译其源代码但只能匹配特定的操作系统和CPU硬件,但是JAVA是使用JAVA编译器(javac),将JAVA源代码编译为字节码(.class)文件。该字节码为16进制格式,带有操作数、操作码。JVM可以根据本机的运行环境将这些操作指令解释为本机的操作系统和CPU可以理解的机器语言(无需进一步的编译)。

因此字节码充当了独立平台的存在,可以在任何JVM之间移植,进而与操作系统和硬件隔离开,但是JVM也是基于操作系统和基础硬件开发的,JVM底层需要与操作系统和底层硬件进行交互,所以我们需要根据操作系统和CPU结果选择合适的JVM版本。(Win32,Win64,MacOS,Linux)

JVM由三个主要的子系统组成

  • 类加载子系统
  • 运行时数据区
  • 执行引擎


    JRE结构.png

类加载子系统(Class Loader SubSystem)

JVM运行在内存中。JAVA程序在执行期间,会使用类加载子系统将需要的类加载到内存中。这种行为被成为动态类加载功能。在程序运行时(非编译)它会加载、链接、初始化class文件(.class)。

compile.png

类的生命周期


classLife.png

1.加载(Loading)

将编译后的class文件加载到内存是Class Loader的主要任务。

通常类加载过程从加载主类(即程序入口类)开始,后续的类加载都是在已运行的类中的类引用完成的,比如以下情况

  • 当调用一个静态方法的时候(System.out)
  • 当创建一个对象的时候(Person p = new Person)

类加载器主要有三种类型,它们遵循以下四个原则

  • 可见性原则(Visibility Principle): 子类加载器可以看到父类加载器加载的类,但是父类加载器找不到子类加载器加载的类。
  • 唯一性原则(Uniqueness Principle): 父类加载器加载的类不应再由子类加载器加载,并且确保不会发生重复的加载
  • 层次委托原则(Delegation Hierarchy Principle): 为了满足上述两个原则,JVM遵循委托的层次结构来为每个类装入请求选择类装入器。
从最低的子级别开始,应用程序类加载器(Application ClassLoader)将接收到的类加载请求委托给扩展类加载器(Extension ClassLoader),然后扩展类加载器将请求委托给启动类加载器(Bootstrap ClassLoader)。如果在Bootstrap路径中找到了所请求的类,则将加载该类。否则,请求将再次转移回扩展类加载器级别,以从扩展路径或自定义指定的路径中查找类。如果它也失败,则请求返回到应用程序类加载器,以从System类路径中查找该类,并且如果应用程序类加载器也未能加载所请求的类,则我们将获得运行时异常 — java.lang.ClassNotFoundException。
  • 无卸载原则(No Unloading Principle): 类加载器无法卸载已经加载的类。除了不能卸载之外,可以删除当前的类加载器,并创建一个新的类加载器。

类加载机制

classloader.png

启动类加载器(Boostrap ClassLoader):负责加载JRE的核心类库,$JAVA_HOME/jre/lib

扩展类加载器(Extension ClassLoader):负责加载JRE扩展目录$JAVA_HOME/jre/lib/ext或者java.ext指定的任何其他目录中加载类。

应用程序加载器(Applicaiton ClassLoader):从类路径(claasspath)加载应用程序特定的类。

除了上述的三种类加载器之外,还可以自定义类加载器。类加载委托模型保证了应用程序的独立性。这种自定义的类加载器常用于Tomcat等web应用程序中。

2.连接(Linking)

连接涉及验证和准备加载的类、接口、父类、父接口以及元素类型,同时具有以下属性

  • 在连接一个类或者接口之前,必须将其完全加载
  • 在初始化类或接口之前,必须对其进行完全验证和准备
  • 如果在链接过程中发生错误,则会在程序中的某个位置引发错误,该错误将由程序执行,而这些操作可能直接或间接地需要链接到错误所涉及的类或接口

连接分为以下三个阶段

2.1 验证(Verification)

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

# 例如检查如下
- 一致且格式正确的符号表
- 不覆盖最终方法/类
- 方法遵循访问控制关键字
- 方法具有正确的数量和参数类型
- 字节码不会错误地操作堆栈
- 变量在读取前已初始化
- 变量的值为正确的类型

2.2 准备(Preparation)

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

2.3 解析(Resolution)

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

3.初始化(Initialization)

​ 在这里将执行每个加载的类或者接口的初始化逻辑(比如调用类的构造函数),由于JVM是多线程的,因此在适当同步的情况下非常仔细地进行类或接口的初始化,以避免其他线程尝试初始化同一个类或接口(即时线程是安全的)。

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

运行时数据区(Runtime Data Area)

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

  • 加载的类及其直接父类的全限定类名
  • .class的关联类型 Class / Interface / Enum / @Interface
  • 修饰符、静态变量、方法信息等

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

1.方法区(Method Area)

​ 类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有类定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+ 运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和Java的堆区分开。

  • 方法区是JVM中的一种规范,具体在JVM内部的实现中各有不同

存储以下数据

  • 类加载器的引用
  • 运行时常量池 - 数字常量 / 字段引用 / 方法引用 / 属性;以及每个类和接口的常量,它包含方法和字段的所有引用。引用方法或字段时,JVM使用运行时常量池在内存中搜索该方法或字段的实际地址。
  • 字段数据 - 名称,类型,修饰符,属性
  • 方法数据 - 名称,返回类型,参数类型(按顺序),修饰符,属性
  • 方法代码 - 字节码,操作数堆栈大小,局部变量大小,局部变量表,异常表;异常表中的每个异常处理程序:起点,终点,处理程序代码的PC偏移,捕获的异常类的常量池索引

2.堆(Heap)

​ 虚拟机启动时自动分配创建,用于存放对象的实例,几乎所有对象(包括常量池)都在堆上分配内存,当对象无法在该空间申请到内存是将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。


heap.png

2.1 新生代(Young Generation)

新生代是类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集, 结束生命。新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在Enden区被new出来的。幸存区又分为From和To区。当Eden区的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对新生代区域进行垃圾回收(Minor GC),将新生代区中不再被其它对象引用的对象进行销毁。然后将Eden区中剩余的对象移到From Survivor区。若From Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区。

动态年龄判断:虚拟机并不是永远的要求对象的年龄必须达到 MaxTenuringThreshold(默认15)才能晋升到老年代。如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进人老年代,无须等到MaxTenuringThreshoId中要求的年龄。

2.2 老年代(Old Generation)

新生代经过多次GC仍然存活的对象移动到老年代区域。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出

OOM(OutOfMemoryError)异常。

2.3 元空间(Meta Space)

在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

为什么移除了永久代

参考官方解释http://openjdk.java.net/jeps/122

大概意思是移除永久代是为融合HotSpot与 JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。


hostspot5.png

3.栈(Stack)

Java线程执行方法的内存模型一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息),并将这个栈帧推入到栈顶。栈不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一 致。


stackFrame.png

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

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

帧数据 : 与该方法有关的所有符号都存储在这里。作为异常,捕获的异常信息也将保留在帧数据中。
栈可以是动态或固定大小,如果线程需要比允许的栈大的栈,则会引发StackOverflowError。如果一个线程需要一个新的帧,并且没有足够的内存来分配它,则抛出OutOfMemoryError。

4.本地方法栈(Native Method Stack)

​ 和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库。

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

​ 一旦创建并初始化了本机线程,它将调用Java线程中的run()方法。当run()方法返回时,将处理未捕获的异常(如果有),然后本机线程确认是否由于线程终止(例如,它是最后一个非守护线程)而需要终止JVM。线程终止时,将释放本机线程和Java线程的所有资源。Java线程终止后,将回收本机线程。因此,操作系统负责调度所有线程并将其分配给任何可用的CPU。

5.程序计数器(Program Counter Register)

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

执行引擎(Execution Engine)

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

1.解释(Interpreter)

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

2.即时编译(Just-In-Time Compiler)

​ 如果只有解释器可用,当一个方法被多次调用时,每次都会进行解释,如果解释处理有效,这将是多余的操作。使用JIT编译器已经可以做到这一点。首先,它将整个字节码编译为本机代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。本机代码存储在缓存中,因此可以更快地执行编译后的代码。
​ 但是,即使对于JIT编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个方法。自适应编译的想法已在Oracle Hotspot VM中使用。
​ 当JVM供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能。

  • 中间代码生成器生成中间代码
  • 代码优化器负责优化上面生成的中间代码
  • 目标代码生成器负责生成本机代码(即机器代码)
  • Profiler是一个特殊的组件,负责查找性能瓶颈(也称为热点)(例如,多次调用一种方法的实例)

Oracle Hotspot虚拟机的优化方法
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编译器称为“高级动态优化编译器”,它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如,java服务器MyApp)来调用Java HotSpot Server VM。

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

IBM AOT(提前)编译

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

3.垃圾收集(Garbage Collector, GC)

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

Java本地接口(JAVA Native Interface,JNI)

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

本地方法库(Native Method Libraries)

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

JVM线程(JVM Thread)

​ 实际上,为了执行我们前面讨论的每个任务,JVM同时运行多个线程。这些线程中的一些带有编程逻辑,是由程序创建的(应用程序线程),而其余的则是由JVM本身创建的,以承担系统中的后台任务(系统线程)。

主应用程序线程是作为调用公共静态void main(String [])的一部分而创建的主线程,而所有其他应用程序线程都是由该主线程创建的。应用程序线程执行诸如执行以main()方法开头的指令,在Heap区域中创建对象(如果它在任何方法逻辑中找到新关键字)等任务。

主要系统线程

  • 编译器线程:在运行时,这些线程将字节码编译为本地代码。
  • GC线程:所有与GC相关的活动均由这些线程执行。
  • 定期任务线程:用于调度定期操作执行的计时器事件(即中断)由该线程执行。
  • 信号调度程序线程:此线程接收发送到JVM进程的信号,并通过调用适当的JVM方法在JVM内处理它们。
  • VM线程:作为前提条件,某些操作需要JVM到达安全点,在该点不再进行对Heap区域的修改。这种情况的示例是“世界停止”垃圾回收,线程堆栈转储,线程挂起和有偏向的锁吊销。这些操作可以在称为VM线程的特殊线程上执行。

一些了解的点(Some Pointers for Understanding)

  • Java被认为是解释语言和编译语言。
  • 根据设计,由于动态链接和运行时解释,Java速度很慢。
  • JIT编译器通过保留本机代码而不是字节码来补偿解释器重复操作的缺点。
  • 最新的Java版本解决了其原始体系结构中的性能瓶颈。
  • JVM只是一个规范。供应商可以在实施过程中自由定制,创新和改善其性能。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350