大家好,又见面了,我是全栈君。
原文地址:http://blog.jamesdbloom.com/JVMInternals.html(转载请注明出处和本文地址英文原文)
本文简要解析JVM的内部结构。下图显示了一个典型的一块JVM(符合JVM Specification Java SE 7 Edition)所具备的关键内部组件。
上图展示的全部这些组件都将在以下两个章节中被解析。第一章包括将会在每一个线程上创建的组件;第二章包括那些不依赖于线程就可以创建的组件(线程间可共享的组件)。
- 线程内创建
- JVM系统线程
- 单个线程
- 程序计数器(PC)
- 栈
- 原生栈
- 栈的限制
- Frame
- 局部变量数组
- 操作数栈
- 动态链接
- 线程之间共享
- 堆
- 内存管理
- 非堆内存区
- JIT编译
- 方法区
- 类文件结构
- 类载入器
- 更快的类载入
- 方法区位置
- 类载入器引用
- 运行时常量池
- 异常表
- 符号表
- (内部字符串)字符串表
线程内组件
一个线程是对程序的一次运行。
JVM同意一个应用创建多个线程并行地运行。在HotspotJVM中,对Java线程和原生操作系统线程之间有一个直接的映射。在全部用于创建Java线程的状态(诸如:thread-local存储、分配缓冲区、同步对象、栈以及程序计数器)准备好之后,原生线程才会被创建。而一旦Java线程终止。原生线程将会被回收。因此,操作系统将会调度全部线程并分配给它们不论什么可用的CPU时间片。一旦原生线程被实例化完毕,它将调用Java线程上的run()方法。
当run()方法返回。未捕获的异常被处理,原生线程会确认JVM是否须要随着线程的终止而终止(比方它是最后一个非deamon线程)。一旦线程被终止,不管是原生线程还是Java线程。它们所占用的资源都会被释放。
JVM系统线程
假设你用jconsole或者不论什么其它的debug工具查看,可能会看到有很多线程在后台运行。这些运行着的后台线程不包括主线程,主线程是基于运行publicstatic void main(String[]) 的须要而被创建的。而这些后台线程都是被主线程所创建。
在HotspotJVM中基本的后台系统线程,见下表:
VM 线程 | 该线程用于等待运行一系列能够使得JVM到达一个“safe-point”的操作。
而这些操作不得不发生在一个独立的线程上的原因是:它们都要求JVM处于一个——无法改动堆的safepoint。 |
周期性的任务线程 | 该线程用于响应timer事件(比如。中断),这些事件用于调度运行周期性的操作 |
GC 线程 | 这些线程支持在JVM中不同类型的垃圾回收 |
编译器线程 | 它们用于在运行时将字节码编译为本地机器码 |
信号分发线程 | 该线程接收发送给JVM的信号。并通过调用JVM合适的方法进行处理 |
单个线程
每一个线程的一次运行都包括例如以下的组件
程序计数器(PC)
除非当前指令或者操作码是原生的。否则当前指令或操作码的地址都须要依赖于PC来寻址。假设当前方法是原生的,那么该PC即为undefined。全部的CPU都有一个PC,通常PC在每一个指令运行后被增加以指向即将运行的下一条指令的地址。JVM使用PC来跟踪正在运行的指令的位置。其实。PC被用来指向methodarea的一个内存地址。
栈
每一个线程都有属于它自己的栈,用于存储在线程上运行的每一个方法的frame。
栈是一个后进先出的数据结构,这能够使得当前正在运行的方法位于栈的顶部。
对于每一个方法的运行,都会有一个新的frame被创建并被入栈到栈的顶部。当方法正常的返回或在方法运行的过程中遇到未捕获的异常时frame会被出栈。栈不会被直接进行操作。除了push/ pop frame 对象。因此能够看出,frame对象可能会被分配在堆上。而且内存也不是必需是连续的地址空间(请注意区分frame的指针跟frame对象)。
原生栈
不是全部的JVM都支持原生方法。但那些支持该特性的JVM一般会对每一个线程创建一个原生方法栈。假设对JVM的JNI(JavaNative Invocation)採用c链接模型的实现,那么原生栈也将是一个C实现的栈。在这个样例中,原生栈中參数的顺序 、返回值都将跟通常的C程序同样。
一个原生方法一般会对JVM产生一个回调(这依赖于JVM的实现)并运行一个Java方法。这样一个原生到Java的调用发生在栈上(通常在Java栈),与此同一时候线程也将离开原生栈。通常在Java栈上创建一个新的frame。
栈的限制
一个栈能够是动态的或者是有合适大小的。假设一个线程要求更大的栈,那么将抛出StackOverflowError异常;假设一个线程要求新创建一个frame,又没有足够的内存空间来分配。将会抛出OutOfMemoryError异常。
Frame
对于每一个方法的运行。一个新frame会被创建并被入栈到栈顶。
当方法正常返回或在方法运行的过程中遇到未捕获的异常。frame会被出栈。
每一个frame都包括例如以下部分:
- 局部变量数组
- 返回值
- 操作数栈
- 对当前方法所属的类的常量池的引用
局部变量数组
局部变量数组包括了在方法运行期间所用到的全部的变量。包括一个对this的引用,全部的方法參数,以及其它局部定义的变量。
对于类方法(比方静态方法),方法參数的存储索引从0開始。而对于实例方法,索引为0的槽都为存储this指针而保留。
一个局部变量。能够是例如以下类型:
- boolean
- byte
- char
- long
- short
- int
- float
- double
- reference
- returnAddress
操作数栈
比如,一个简单的变量初始化,就导致产生两个与操作数栈交互的字节码。
int i;
编译后获得例如以下的字节码:
0: iconst_0 // Push 0 to top of the operand stack1: istore_1 // Pop value from top of operand stack and store as local variable 1
对很多其它细节,请阅读兴许内容。
动态链接
该引用指向将要被运行的方法所属的类的常量池。该引用也用于辅助动态链接。
但JVM必须保证:解析发生在每一个引用被首次使用前,同一时候在该时间点,假设遇到分析错误能够抛出异常。绑定是一个处理过程,它将被符号引用标识的字段、方法或类替换为一个直接引用。
这个处理过程仅仅发生一次。由于符号引用须要被全然替换。假设一个符号引用关联着一个类,而该类还没有被解析,那么该类也会被马上载入。
每一个直接引用都被以偏移的方式存储。该存储结构关联着变量或方法的运行时位置。
线程之间共享
堆
数组和对象永远不能被分配到栈上。由于frame被设计为在其创建后不可更改大小。Frame仅仅存储用于指向堆中的数组和对象的指针。
不像原始类型以及存储在局部变量数组中的引用(以上这些都指存储在每一个frame里的),对象总是存储在堆上。所以当一个方法运行结束,它们不会被马上移除(对象仅仅能被垃圾回收器回收)。
- 青年代
- 通常又被切割为Eden跟Survivor两个部分
- 老年代(也称之为终身代)
- 永久代
内存管理
对象和数组永远都不会被显式释放,因此仅仅能依靠垃圾回收器来自己主动地回收它们。
通常。以例如以下的步骤进行:
- 新对象和数组被创建在年轻代
- 次垃圾回收器将在年轻代上运行。那些仍然存活着的对象。将被从eden区移动到survivor区
- 主垃圾回收器将会把对象在代与代之间进行移动,主垃圾回收器一般会导致应用程序的线程暂停。那些仍然存活着的对象将被从年轻代移动到老年代
- 永久代会在每次老年代被回收的时候同一时候进行,它们在两者中其一满了之后都会被回收
非堆式内存
- 永久代中包括:
- 方法区
- 内部字符串
- 代码缓存:用于编译以及存储方法,这些方法已经被JIT编译成本地代码
JIT编译
为了提高性能,OracleHotspot VM会寻找那些有规律地运行的字节码,并把他们编译为本地原生代码。而原生代码将会被存储在代码缓存的非“堆”内存区。这样,HotspotVM会尝试去选择最合适的方式在它编译代码以及它运行被解释过代码的额外时间之间作出权衡。
方法区
- 类载入器的引用
- 运行时常量池
- 数值常量
- 字段引用
- 方法引用
- 属性
- 字段数据
- 对每一个字段
- 名称
- 类型
- 改动器
- 属性
- 方法数据
- 对每一个方法
- 名称
- 返回值
- 參数类型(按顺序)
- 改动器
- 属性
- 方法体
- 对每一个方法
- 字节码
- 操作数栈大小
- 局部变量大小
- 局部变量表
- 异常表
- 对每一个异常处理器
- 起始点
- 终止点
- 对处理器代码的PC偏移量
- 被捕获的异常类在常量池中的索引
全部的线程共享同样的方法区。所以,对于方法区数据的訪问以及对动态链接的处理必须是线程安全的。
假设两个线程企图訪问一个还没有被载入的类(该类必须仅仅能被载入一次)的字段或者方法,直到该类被载入完毕,这两个线程才干继续运行。
类的文件结构
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info contant_pool[constant_pool_count – 1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
magic, minor_version, major_version |
指定一些信息: 当前类的版本号、编译当前类的JDK版本号 |
constant_pool | 跟符号表类似,但它包括很多其它的数据 |
access_flags | 为该类提供一组改动器 |
this_class | 为该类提供全然限定名在常量池中的索引,比如:org/jamesdbloom/foo/Bar |
super_class | 提供对其父类的符号引用在常量池中的索引,比如:java/lang/Object |
interface | 常量池中的数组索引,该数组提供对全部被实现的接口的符号引用 |
fields | 常量池中的数组索引。该数组提供对每一个字段的完整描写叙述 |
methods | 常量池中的数组索引,该数组提供对每一个方法签名的完整描写叙述,假设该方法不是抽象的或者native的, 那么也会包括字节码 |
attributes | 不同值的数组,提供关于类的额外信息,包括注解:RetentionPolicy.CLASS以及RetentionPolicy.RUNTIME |
package org.jvminternals;public class SimpleClass { public void sayHello() { System.out.println("Hello"); }}
假设你运行以下的命令。那么你将看到以下的输出:
public class org.jvminternals.SimpleClass SourceFile: "SimpleClass.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #6.#17 // java/lang/Object."<init>":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // "Hello" #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // org/jvminternals/SimpleClass #6 = Class #24 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lorg/jvminternals/SimpleClass; #14 = Utf8 sayHello #15 = Utf8 SourceFile #16 = Utf8 SimpleClass.java #17 = NameAndType #7:#8 // "<init>":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Hello #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 org/jvminternals/SimpleClass #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V{ public org.jvminternals.SimpleClass(); Signature: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/jvminternals/SimpleClass; public void sayHello(); Signature: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String "Hello" 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 6: 0 line 7: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lorg/jvminternals/SimpleClass;}
该类文件显示了三个主要部门:常量池、构造器以及sayHello方法
- 常量池:它提供了跟通用符号表提供的同样的信息
- 方法:(每一个都包括四个区域)
- 签名和訪问标记
- 字节码
- line number表 – 它给调试器提供相关信息。哪一行关联到哪条字节码指令。
比如。在sayHello方法中,Java代码的第6行关联着字节码指令行数为0。第7行Java代码关联着字节码指令行数为8
- 局部变量表 – 列出了在frame中提供的全部局部变量,在这个样例中。唯一的局部变量是:this
aload_0 | 该操作码是形如aload_<n>格式的一组操作码中当中的一个。
它们都是用来载入一个对象引用到操作数栈。 |
ldc | 该操作码用来从运行时常量池取出一个常量压入操作数栈 |
getstatic | 该操作码用来从运行时常量池的静态字段列表入栈一个静态值到操作数栈 |
invokespecial invokevirtual |
这些操作码是一组用来运行方法的操作码 (总共同拥有:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual这几种)。 当中,本例中出现的invokevirtual用来运行类的实例方法; 而invokespecial用于运行实例的初始化方法,同一时候也用于运行私有方法以及属于超类但被当前类继承的方法 (超类方法动态绑定到子类)。 |
return | 该操作码是一组操作码(ireturn,lreturn,freturn,dreturn,areturn以及return)中的当中一个。 每一个操作码,都是类型相关的返回语句。 当中i代表int,l表示long。f表示float,d表示double而a表示一个对象的引用。 没有标识符作为首字母的return语句。仅会返回void |
第一个操作数getstatic,用来入栈一个指向System类的静态字段out的引用到操作数栈。
接下来的操作数ldc。入栈一个字符串字面量“Hello”到操作数栈。最后。invokevirtual操作数,运行System.out的println方法,这将使得“Hello”作为一个參数从操作数栈出栈,并为当前线程创建一个新的frame。
类载入器
main方法的运行,将顺序经历载入。链接,以及对额外必要的类跟接口的初始化。
载入: 载入是这样一个过程:查找表示该类或接口类型的类文件,并把它读到一个字节数组中。接着,这些字节会被解析以确认它们是否表示一个Class对象以及是否有正确的主、次版本号号。不论什么被当做直接superclass的类或接口也一同被载入。一旦这些工作完毕,一个类或接口对象将会从二进制表示中创建。
链接: 链接包括了对该类或接口的验证,准备类型以及该类的直接父类跟父接口。简而言之。链接包括三个步骤:验证、准备以及解析(optional)
验证: 该阶段会确认类以及接口的表示形式在结构上的正确性,同一时候满足Java编程语言以及JVM语义上的要求。比方,接下来的检查动作将会被运行:
- 一致以及正确被格式化的符号表
- final方法或类没有被override
- 方法必须带有訪问控制keyword
- 方法有正确的參数值跟类型
- 字节码不会对栈进行不对得篡改
- 变量在被读取之前已经进行了初始化
- 变量具有正确的类型
在验证阶段运行这些检查意味着在运行时能够免去在链接阶段进行这些动作。虽然拖慢了类的载入速度,然而它避免了在运行字节码的时候运行这些检查。准备: 包括了对静态存储的内存分配以及JVM所使用的不论什么数据结构(比方方法表)。静态字段都被创建以及实例化为它们的默认值。然而,没有不论什么实例化器或代码在这个阶段被运行,由于这些任务将会发生在实例化阶段。解析: 是一个可选的阶段。该阶段通过载入引用的类或接口来检查符号引用是否正确。假设在这个点这些检查没发生。那么对符号引用的解析会被推迟到直到它们被字节码指令使用之前。
实例化 类或接口。包括运行类或接口的实例化方法:<clinit>
在JVM中存在多个不同职责的类载入器。每一个类载入器都代理其已被载入的父载入器(除了bootstrap类载入器,由于它是根载入器)。
Bootstrap类载入器:通常使用原生代码实现。由于它在JVM启动后非常快就会被初始化。
Bootstrap类载入器用于载入最基本的JavaAPI,比方rt.jar。它仅载入那些位于boot类路径中的信任级别非常高的类。因此,它也跳过了非常多验证过程。
Extension 类载入器:从标准的Java扩展API中载入类。比如。安全的扩展功能集。
System 类载入器:这是应用程序默认的类载入器。它从classpath中载入应用程序类。
用户定义的类载入器:能够额外得定义类载入器来载入应用程序类。用户定义的类载入器可用于一些特殊的场景。比方:在运行时又一次载入类或将一些特殊的类隔离为多个不同的分组(通常webserver中都会有这种需求,比方Tomcat)。
更快的类载入
一个称之为类数据共享(CDS)的特性自HotspotJVM 5.0開始被引进。在安装JVM期间。安装器载入一系列的Java核心类(如rt.jar)到一个经过映射过的内存区进行共享存档。
CDS降低了载入这些类的时间从而提升了JVM的启动速度,同一时候同意这些类在不同的JVM实例之间共享。这大大降低了内存碎片。
方法区的位置
JVM Specification Java SE 7 Edition清楚地声明:虽然方法区是堆的一个逻辑组成部分,但最简单的实现可能是既不对它进行垃圾回收也不压缩它。然而矛盾的是利用jconsole查看Oracle的JVM的方法区(以及CodeCache)是非堆形式的。
OpenJDK代码显示CodeCache相对ObjectHeap而言是VM中一个独立的域。
类载入器引用
全部的类都包括一个指向载入它们的类载入器的引用。反过来类载入器也包括它载入的全部类的引用。
运行时常量池
JVM对每一个类型维护着一个常量池,它是一个跟符号表类似的运行时数据结构。但它包括了很多其它的数据。Java的字节码须要一些数据,通常这些数据会由于太大而难以直接存储在字节码中。取而代之的一种做法是将其存储在常量池中,字节码包括一个对常量池的引用。
运行时常量池主要用来进行动态链接。
几种类型的数据会存储在常量池中。它们是:
- 数值字面量
- 字符串字面量
- 类的引用
- 字段的引用
- 方法的引用
Object foo = new Object();
编译为字节码将会像例如以下这样:
0: new #2 // Class java/lang/Object 1: dup 2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
“new”操作码后面跟着#2操作数。该操作数是一个指向常量池的索引,因此它引用常量池中得第二条记录。
而第二条记录是一个类的引用,该记录反过来引用还有一个位于常量池里的记录(它是一个用UTF8编码的类名://Class java/lang/Object)。该符号链接稍后会用于查找java.lang.Object类。new操作码创建类的一个实例同一时候实例化它的变量。
这个指向类的新实例的引用会被增加到操作数栈。dup操作码接着创建一个额外的对操作数栈的栈顶引用的拷贝。
同一时候将this引用增加栈顶。终于。一个实例的初始化方法会被调用(上图第二行通过调用invokespecial)。this操作数同样也包括一个对常量池的引用。
实例化方法消费栈顶引用(把其视为传递给该方法的一个參数)。终于。将会产生一个对新对象的引用(这个引用是既被创建完毕也被初始化完毕的)。
package org.jvminternals; public class SimpleClass { public void sayHello() { System.out.println("Hello"); } }
生成的类文件的常量池,看起来会像下图所看到的:
Constant pool: #1 = Methodref #6.#17 // java/lang/Object."<init>":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // "Hello" #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // org/jvminternals/SimpleClass #6 = Class #24 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lorg/jvminternals/SimpleClass; #14 = Utf8 sayHello #15 = Utf8 SourceFile #16 = Utf8 SimpleClass.java #17 = NameAndType #7:#8 // "<init>":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Hello #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 org/jvminternals/SimpleClass #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V
常量池中包括了以下的这些类型:
Integer | 一个4字节的int常量 |
Long | 一个8字节的long常量 |
Float | 一个4字节的float常量 |
Double | 一个8字节的double常量 |
String | 一个String字面值常量指向常量池中还有一个包括终于字节的UTF8记录 |
Utf8 | 一个字节流表示一个Utf8编码的字串序列 |
Class | 一个Class字面值常量指向常量池中的还有一个Utf8记录,它包括JVM内部格式的全然限定名 (它用于动态链接) |
NameAndType | 用一个冒号区分一对值,每一个值都指向常量池中得其它记录。
冒号前的第一个值指向一个utf8字符串字面量表示方法名或者字段名。 |
Fieldref, Methodref, InterfaceMethodref |
用点来分隔的一对值,每一个值指向常量池中的还有一个记录。 点前的第一个值指向一个Class记录。第二个值指向一个NameAndType记录 |
异常表
- 起始点
- 终止点
- 处理代码的PC偏移量
- 被捕获的异常类的常量池索引
在没有异常抛出的样例中,finally块仍然会在方法的最后被运行。
一旦return语句被运行就会马上跳转到finally代码块继续运行。
符号表
比如,当某个类被卸载后。全部它的运行时常量池中的符号的引用计数都会被减一。当符号表中的一个符号的引用计数到达0时,符号表就觉得该符号将不会再被引用,而随后也会被从符号表中卸载。不管是符号表还是字符串表(见以下)。全部的记录都存储在一个标准化的表单中以此来提升性能同一时候能够确认每条记录仅仅出现一次。
内部字符串(字符串表)
另外。假设String.intern()在一个字符串实例上被调用,那么必须返回一个引用,该引用指代的实例必须跟该字符串的字面量同样。以下的代码将返回true。
"j" + "v" + "m").intern() == "jvm"
在JVM中,内部字符串被存储在字符串表中。字符串表是一个hashtable映射对象指针到符号(比方:Hashtable<oop,Symbol>),它被存储在永久代里。
string类的例子还可以通过调用来获得String.intern()要明确地内部化。什么时候String.intern()被称为。假设符号表中已包含在字符串中,然后指着引用字符串将被退回。假设串中不包含的字符表,它添加到字符串表它将被返回到它的基准的同时。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/116660.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...