文章目录
程序计数器
概述
程序计数器(Program Counter Register)也称之为PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条 要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
特点分析
1)计算机在任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
2)如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此 该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。
常见问题分析
1) 使用程序计数器(PC寄存器)存储字节码指令地址有什么用?
我们知道,CPU运行时,需要在线程间来回切换,当切换回当前线程时,为了知道从哪里开始执行,就需要一个计数器记录CPU在当前线程的执行位置。
2) JVM规范中为什么将PC寄存器设置为线程私有?
我们知道,一个线程在特定时间段内只能执行某个线程的一个方法。但是CPU在运行过程中会不停的做任务切换,这样必然会导致线程经常中断或恢复执行。为了能够准确记录每个线程正在执行的当前字节码指令地址,最好的办法就是为每个线程都分配一个PC寄存器,这样的的好处是,每个线程都可以独立计算,从而不会出现相互干扰的情况。
PC寄存器实践分析
第一步:定义java类,代码如下:
package com.java.jvm;
public class PCRegisterTests {
public static void main(String[] args) {
int a=10;
int b=20;
int c=10+20;
}
}
第二步:对字节码文件进行解析
在PCRegisterTests.class文件对应的目录,基于javap指令进行解析,例如:
Javap -v PCRegisterTests.class
解析结果为
Classfile /E:/TCGBIV/DEVCODES/CGB2112CODES/01-java/target/classes/com/java/jvm/PCRegisterTes
ts.class
Last modified 2022-5-5; size 481 bytes
MD5 checksum 5e04f67d41d8da019d39c068301cbf74
Compiled from "PCRegisterTests.java"
public class com.java.jvm.PCRegisterTests
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // com/java/jvm/PCRegisterTests
#3 = Class #23 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/java/jvm/PCRegisterTests;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 a
#16 = Utf8 I
#17 = Utf8 b
#18 = Utf8 c
#19 = Utf8 SourceFile
#20 = Utf8 PCRegisterTests.java
#21 = NameAndType #4:#5 // "<init>":()V
#22 = Utf8 com/java/jvm/PCRegisterTests
#23 = Utf8 java/lang/Object
{
public com.java.jvm.PCRegisterTests();
descriptor: ()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 Lcom/java/jvm/PCRegisterTests;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: bipush 30
8: istore_3
9: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 a I
6 4 2 b I
9 1 3 c I
}
SourceFile: "PCRegisterTests.java"
其中,如上代码中的0~9部分为指令偏移地址,这部分值会存储在pc寄存器中,例如:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: bipush 30
8: istore_3
9: return
JVM执行指令时,执行引擎会从当前线程的PC寄存器中,基于地址找到具体指令,然后进行执行这些指令。
小节面试分析
1)什么是PC寄存器?是一块较小的内存空间,用来存储指向下一条指令的地址?
2)每个线程都有一个PC寄存器吗?是的。
3)JVM规范中为什么将PC寄存器设置为线程私有?
栈结构分析
概述
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,解决的是程序运行时数据的操作问题,即程序如何执行,或者说如何处理数据。而堆解决的是数据存储的问题,即数据怎么放,放哪里。
特点分析
由于跨平台性的设计,Java的指令都是根据栈来设计的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
Java 虚拟机栈是线程私有的,每个方法在被线程调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈的生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了。
如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。
栈桢构成分析
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。每个栈帧中存储着:
1)局部变量表(Local Variables)
2)操作数栈(Operand Stack)(或表达式栈)
3)动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
4)方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
5)一些附加信息
总之,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
局部变量表分析
局部变量表也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息。在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。
局部变量表以变量槽为最小单位,每个变量槽可以存放一个32位以内的数据类型,故每个变量槽都应该能存放 boolean、byte、char、short、int、float、refrence或returnAddress类型的数据,对于long、double两种,会占用两个变量槽。例如:
操作数栈分析
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈(Operand Stack)。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。例如:
动态链接分析
每个方法在运行时,都会创建一个栈帧,其内部包含一个指向运行时常量池的引用,这个引用的目的是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
Java源文件编译后的字节码文件中,所有类中变量和方法的引用都会保存在class文件的常量池中。动态链接的作用就是为了将这些符号引用转换为调用方法时的直接引用。
常用参数配置分析
我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。例如:
-Xss1m
-Xss1024k
-Xss1048576
案例演示:
public class StackErrorTests {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
程序运行的最终结果为StackOverflowError,此时,可以基于idea配置栈内存大小,并检查不同栈大小的配置对输出结果有什么影响。
本地(native)方法栈分析
本地方法栈(Native Method Stack)与虚拟机栈的作用类似,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的(查看一下Object类中的本地方法)。在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 Sun HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。
小节面试分析
1)JAVA方法栈有什么特点?
2)JAVA方法栈中都可以存储什么?
堆内存分析
概述
Java堆(Java Heap)是JVM 中内存最大的一块,被所有线程共享,在虚拟机启动 时创建,主要用于存放对象实例,大部分对象实例也都是在这里分配。但是,随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。小对象未逃逸还可以在直接在栈上分配。
特点分析
Java 虚拟机规范规定, Java堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过 -Xmx 和 -Xms 参数定义堆内存大小。如果在堆中没有内存完成实例分配, 并且堆已不可以再进行扩展时,系统底层运行时将会抛出 OutOfMemoryError。
堆构成分析
JAVA堆内存在JVM中可分为年轻代和老年代。年轻代又分为Eden和两个Survivor区。如图所示:
其中,堆内存又是垃圾收集器(GC)管理的主要区域。
对象内存分配
基于此内存架构,对象内存分配过程如下:
- 编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。
- 如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
- 如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
- 如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。
- 如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
说明:在对象创建时可能会触发Yong GC,此GC过程的简易原理图分析如下:
其中: - 新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象。
- GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代。
- GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区。
- GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推。
- 当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。
常用参数配置分析
- -Xms设置堆的最小空间大小(默认为物理内存的1/64)。
- -Xmx设置堆的最大空间大小(默认为物理内存的1/4)。
- -XX:NewRatio 新生代和老年代的比值,值为4则表示新生代:比老年代1:4
- -XX:SurivorRatio 表示Survivor和eden的比值,值为8表示两个survivor:eden=2:8。
- -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄。
- -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值。
- -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)。
- -XX:+PrintGCDetails:输出详细的GC处理日志。
- -XX:+PrintGC: 打印gc简要信息。
小节面试问题分析
问题1:如何查看当前正在运行的进程中某个JVM参数的值?
第一步:jps:查看当前运行中的进程
第二步:jinfo -flag 参数名 进程id (例如 jinfo -flag SurvivorRatio 10091)
问题2:假如年轻代幸存区设置的比较小会有什么问题?
伊甸园区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
问题3:假如年轻代伊甸园区设置的比例比较小会有什么问题?
伊甸园设置的比较小,会增加GC的频率,可能会导致STW的时间边长,影响系统性能。
问题4:如何理解堆内存中的空间分配担保机制?
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于,则此次Minor GC是安全的。
如果小于,则虚拟机会查看-XX:HandlePromotionFailure参数设置值是否允担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
如果小于,则进行一次Full GC。
如果HandlePromotionFailure=false,则进行一次Full GC。
问题五:jvm内存分区,为什么要有新生代和老年代?
为了更好的实现垃圾回收。
方法区分析
概述
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。不同jdk,方法区的实现不同,HotSpot 虚拟机在 JDK 8 中使 用 Native Memory 来实现方法区。
在JDK8的HostSpot虚拟机中方法区的落地实现是元数据区(Metaspace),但是Metaspace并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间(Metaspace)的大小仅受本地内存限制,但可以通过一些参数来指定元空间的大小。
特点分析
1.方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。
2.方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
3.方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
4.方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace
方法区构成分析
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。
1)类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。
2)常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
编写如下代码,然后使用javap查看类的字节码信息,例如:
package com.java.jvm;
//javap -p -v MethodAreaStructureTests.class > class.txt
public class MethodAreaStructureTests {
private int size;
private static int count=10;
public void doChangeSize(){
size++;
}
public static void doChangeCount(){
count++;
}
public static void main(String[] args) {
System.out.println(count);
}
}
类编译后,在类当前路径查看类信息,例如:
javap -p -v MethodAreaStructureTests.class > class.txt
此时打开class.txt文件,查看类的字节码信息,例如:
Classfile /E:/TCGBIV/DEVCODES/CGB2112CODES/01-java/target/classes/com/java/jvm/MethodAreaStructureTests.class
Last modified 2022-5-8; size 831 bytes
MD5 checksum ffe8357e90f1ccc9cb28ef1142c68863
Compiled from "MethodAreaStructureTests.java"
public class com.java.jvm.MethodAreaStructureTests
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #6.#28 // com/java/jvm/MethodAreaStructureTests.size:I
#3 = Fieldref #6.#29 // com/java/jvm/MethodAreaStructureTests.count:I
#4 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #32.#33 // java/io/PrintStream.println:(I)V
#6 = Class #34 // com/java/jvm/MethodAreaStructureTests
#7 = Class #35 // java/lang/Object
#8 = Utf8 size
#9 = Utf8 I
#10 = Utf8 count
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/java/jvm/MethodAreaStructureTests;
#18 = Utf8 doChangeSize
#19 = Utf8 doChangeCount
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 <clinit>
#25 = Utf8 SourceFile
#26 = Utf8 MethodAreaStructureTests.java
#27 = NameAndType #11:#12 // "<init>":()V
#28 = NameAndType #8:#9 // size:I
#29 = NameAndType #10:#9 // count:I
#30 = Class #36 // java/lang/System
#31 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#32 = Class #39 // java/io/PrintStream
#33 = NameAndType #40:#41 // println:(I)V
#34 = Utf8 com/java/jvm/MethodAreaStructureTests
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (I)V
{
private int size;
descriptor: I
flags: ACC_PRIVATE
private static int count;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC
public com.java.jvm.MethodAreaStructureTests();
descriptor: ()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 Lcom/java/jvm/MethodAreaStructureTests;
public void doChangeSize();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field size:I
5: iconst_1
6: iadd
7: putfield #2 // Field size:I
10: return
LineNumberTable:
line 7: 0
line 8: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/java/jvm/MethodAreaStructureTests;
public static void doChangeCount();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field count:I
3: iconst_1
4: iadd
5: putstatic #3 // Field count:I
8: return
LineNumberTable:
line 10: 0
line 11: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #3 // Field count:I
6: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 13: 0
line 14: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field count:I
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "MethodAreaStructureTests.java"
常用参数配置分析
JDK8中的元空间大小的参数分析及配置如下:
1)元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定。例如-XX:MetaspaceSize=100M -XX:MaxMetaspaceSize=100m
2)Windows下,-XX:MetaspaceSize 默认值约为21M,-XX:MaxMetaspaceSize的默认值是-1,即没有限制。(可通过 jps获取进程号,然后通过jinfo -flag MetaspaceSize 进程id 的方式进行查看)
3)如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
4)-XX:MetaspaceSize:设置初始的元空间大小。对于一个 64位 的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
5)如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
小节面试分析
问题1:方法区主要存储哪些内容?
类型信息、运行时常量池、静态变量、JIT代码缓存、属性信息、方法信息等。
问题2:如何理解直接内存?
直接内存是在Java堆外的、直接向系统申请的内存区间。访问直接内存的速度会优于Java堆。因此读写频繁的场合可能会考虑使用直接内存。
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");