阿里面试官问我:JVM内幕,我懵了~
Posted 程序员熊大大
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阿里面试官问我:JVM内幕,我懵了~相关的知识,希望对你有一定的参考价值。
JVM_Internal_Architecture
上图显示的组件分两个章节解释。第一章讨论针对每个线程创建的组件,第二章节讨论了线程无关组件。
-
线程 -
JVM 系统线程 -
每个线程相关的 -
程序计数器 -
栈 -
本地栈 -
栈限制 -
栈帧 -
局部变量数组 -
操作数栈 -
动态链接 -
线程共享 -
堆 -
内存管理 -
非堆内存 -
即时编译 -
方法区 -
类文件结构 -
类加载器 -
更快的类加载 -
方法区在哪里 -
类加载器参考 -
运行时常量池 -
异常表 -
符号表 -
Interned 字符串
线程
run()
方法。
run()
返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。
JVM 系统线程
线程相关组件
-
局部变量数组 -
返回值 -
操作数栈 -
类当前方法的运行时常量池引用
局部变量数组
-
boolean -
byte -
char -
long -
short -
int -
float -
double -
reference -
returnAddress
int i;
被编译成下面的字节码:
0: iconst_0 // Push 0 to top of the operand stack
1: istore_1 // Pop value from top of operand stack and store as local variable 1
更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息,可以在类文件结构部分找到。
线程间共享
堆
堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。
为了支持垃圾回收机制,堆被分为了下面三个区域:
新生代
经常被分为 Eden 和 Survivor
老年代
永久代
内存管理
新的对象和数组被创建并放入老年代。
Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。
非堆内存
非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。
非堆内存包括:
永久代,包括:
方法区
驻留字符串(interned strings)
代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。
-
Classloader 引用 -
运行时常量池 -
数值型常量 -
字段引用 -
方法引用 -
属性 -
字段数据 -
字段名 -
类型 -
修饰符 -
属性(Attribute) -
针对每个字段的信息 -
方法数据 -
方法名 -
返回值类型 -
参数类型(按顺序) -
修饰符 -
属性 -
每个方法 -
方法代码 -
字节码 -
操作数栈大小 -
局部变量大小 -
局部变量表 -
异常表 -
每个异常处理器 -
开始点 -
结束点 -
异常处理代码的程序计数器(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];
}
package org.jvminternals;public class SimpleClass {
public void sayHello() {
System.out.println("Hello");
}
}
运行下面的命令,就可以得到下面的结果输出:
javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class
。
public class org.jvminternals.SimpleClass
SourceFile: "SimpleClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
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{ 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;
}
这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello
方法。
常量池:提供了通常由符号表提供的相同信息,详细描述见下文。
方法:每一个方法包含四个区域,
签名和访问标签
字节码
LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。
LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。
构造器函数包含两个指令。首先,this 变量被压栈到操作数栈,然后父类的构造器函数被调用,而这个构造器会消费 this,之后 this 被弹出操作数栈。
bytecode_explanation_SimpleClass
sayHello()
方法更加复杂,正如之前解释的那样,因为它需要用运行时常量池中的指向符号引用的真实引用。
第一个操作码
getstatic
从
System
类中将
out
静态变量压到操作数栈。
下一个操作码
ldc
把字符串
“Hello”
压栈到操作数栈。
最后
invokevirtual
操作符会调用
System.out
变量的
println
方法,从操作数栈作弹出
”Hello”
变量作为
println
的一个参数,并在当前线程开辟一个新栈帧。
bytecode_explanation_sayHello_smaller
类加载器
public static void main(String[])
调用之前完成链接和初始化。
执行这个方法会执行加载、链接、初始化需要的额外类和接口。
-
格式一致且格式化正确的符号表 -
final
方法和类没有被重载 -
方法遵循访问控制关键词 -
方法参数的数量、类型正确 -
字节码没有不当的操作栈数据 -
变量在读取之前被初始化过 -
变量值的类型正确
<clinit>
的执行组成。
Class_Loading_Linking_Initializing
Bootstrap 加载器一般由本地代码实现,因为它在 JVM 加载以后的早期阶段就被初始化了。bootstrap 加载器负责载入基础的 Java API,比如包含 rt.jar。它只加载拥有较高信任级别的启动路径下找到的类,因此跳过了很多普通类需要做的校验工作。
Extension 加载器加载了标准 Java 扩展 API 中的类,比如 security 的扩展函数。
System 加载器是应用的默认类加载器,比如从 classpath 中加载应用类。
用户自定义类加载器也可以用来加载应用类。使用自定义的类加载器有很多特殊的原因:运行时重新加载类或者把加载的类分隔为不同的组,典型的用法比如 web 服务器 Tomcat。
class_loader_hierarchy
加速类加载
共享类数据(CDS)是Hotspot JVM 5.0 的时候引入的新特性。在 JVM 安装过程中,安装进程会加载一系列核心 JVM 类(比如 rt.jar)到一个共享的内存映射区域。CDS 减少了加载这些类需要的时间,提高了 JVM 启动的速度,允许这些类被不同的 JVM 实例共享,同时也减少了内存消耗。
方法区在哪里
运行时常量池
-
数字型 -
字符串型 -
类引用型 -
域引用型 -
方法引用
Object foo = new Object();
写成字节码将是下面这样:
0: new #2 // Class java/lang/Object
1: dup2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
new
操作码的后面紧跟着操作数
#2
。
这个操作数是常量池的一个索引,表示它指向常量池的第二个实体。
第二个实体是一个类的引用,这个实体反过来引用了另一个在常量池中包含 UTF8 编码的字符串类名的实体(
// Class java/lang/Object
)。
然后,这个符号引用被用来寻找
java.lang.Object
类。
new
操作码创建一个类实例并初始化变量。
新类实例的引用则被添加到操作数栈。
dup
操作码创建一个操作数栈顶元素引用的额外拷贝。
最后用
invokespecial
来调用第 2 行的实例初始化方法。
操作码也包含一个指向常量池的引用。
初始化方法把操作数栈出栈的顶部引用当做此方法的一个参数。
最后这个新对象只有一个引用,这个对象已经完成了创建及初始化。
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
这个常量池包含了下面的类型:
异常表
-
起始点(Start point) -
结束点(End point) -
异常处理代码的 PC 偏移量 -
被捕获异常的常量池索引
try-catch
或者
try-finally
异常处理器,那么就会创建一个异常表。
它为每个异常处理器和 finally 代码块存储必要的信息,包括处理器覆盖的代码块区域和处理异常的类型。
Hashtable<Symbol*, Symbol>
),它拥有指向所有符号(包括在每个类运行时常量池中的符号)的指针。
字符串表
String.intern()
方法的返回引用必须与字符串是字面量时的一样。
因此,下面的代码返回 true:
("j" + "v" + "m").intern() == "jvm"
Hashtable<oop, Symbol>
),它被保存到永久代中。
符号表和字符串表的实体都以规范的格式保存,保证每个实体都只出现一次。
String
类的实例可以调用
String.intern()
显式地
intern
。
当调用
String.intern()
方法时,如果符号表已经包含了这个字符串,那么就会返回符号表里的这个引用,如果不是,那么这个字符串就被加入到字符串表中同时返回这个引用。
长按下方的二维码即可快速进群!!!
群里会不定期发送各种技术和视频资料
以上是关于阿里面试官问我:JVM内幕,我懵了~的主要内容,如果未能解决你的问题,请参考以下文章
面试官问:为什么 Java 线程没有Running状态?我懵了