-
每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。
-
在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是将这些以符号引用所表示的方法转换为对实际方法的直接引用。
4)方法返回:
- 方法执行后,有两种方式退出该方法:正常调用完成,执行返回指令。异常调用完成,遇到未捕获异常,不会有方法返回值给调用者。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是相似的,当线程调用Java方法时,会创建一个栈帧并压入虚拟机栈;而调用本地方法时,虚拟机会保持栈不变,不会压入新的栈帧,虚拟机只是简单的动态链接并直接调用指定的本地方法,使用的是某种本地方法栈。比如某个虚拟机实现的本地方法接口是使用C连接模型,那么它的本地方法栈就是C栈。
本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,它可以做任何他想做的事情,本地方法不受虚拟机控制。
程序计数器
每一个运行的线程都会有它的程序计数器(PC寄存器),与线程的生命周期一样。执行某个方法时,PC寄存器的内容总是下一条将被执行的地址,这个地址可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是 undefined。
程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。多线程环境下,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
代码在JVM内存中的体现
当我们通过Object o=new Object()
创建一个对象时,在JVM中会分配一块内存用来存储该对象的信息,实现原理如下图所示。
在main方法中,创建了一个局部变量o
,当main方法运行时,首先会把main方法压入到栈帧中,接着执行该方法的Object o =new Object()
创建对象。
-
在局部变量表中创建一个局部变量
o
。 -
在堆内存中分配一块内存地址,用来存储
object
对象。 -
变量
o
指向堆内存中的内存地址。
我们再来看一个例子,声明一个Person对象,在该对象中存在一个常量name
、以及一个成员变量age
,当运行该类中的main
方法时,此时JVM内存中的运行情况如下。
在这个例子中,看到了常量池的出现,看来,还有必要了解一下常量池的知识
JVM中的常量池
在JVM中,常量池主要分为:Class文件常量池、运行时常量池,当然还有全局字符串常量池,以及基本类型包装类对象常量池。
常量池主要存放两大类常量:字面量和符号引用。
-
字面量:字面量主要是文本字符串、final 常量值、类名和方法名的常量等。
-
符号引用:符号引用对java动态连接起着非常重要的作用。主要的符号引用有:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
Class文件常量池
class文件是一组以8位字节为单位的二进制数据流,在java代码的编译期间,我们编写的.java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
为了更好的说明,我们通过下面这段代码为例进行讲解。
class ConstantExample{
private int value = 1;
public String s = “abc”;
public final static int f = 0x101;
public void setValue(int v){
final int temp = 3;
this.value = temp + v;
}
public int getValue(){
return value;
}
}
这段代码被编译后,通过javap -v
命令查看编译后的字节码。
从下面这个字节码信息中可以看到,执行这个命令之后我们得到了该class文件的版本号、常量池、已经编译后的字节码指令(处于篇幅原因这里省略),下面我们会对照这个class文件来讲解:
example/target/classes/HelloExample.class
Last modified 2021-10-25; size 734 bytes
MD5 checksum fd06c1426f4fdef12aa109ee7f010a45
Compiled from “HelloExample.java”
public class HelloExample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#32 // java/lang/Object.“”😦)V
#2 = Fieldref #5.#33 // HelloExample.value:I
#3 = String #34 // abc
#4 = Fieldref #5.#35 // HelloExample.s:Ljava/lang/String;
#5 = Class #36 // HelloExample
#6 = Class #37 // java/lang/Object
#7 = Utf8 value
#8 = Utf8 I
#9 = Utf8 s
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
#14 = Utf8
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 LHelloExample;
#21 = Utf8 getValue
#22 = Utf8 ()I
#23 = Utf8 setValue
#24 = Utf8 (I)V
#25 = Utf8 MethodParameters
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 SourceFile
#31 = Utf8 HelloExample.java
#32 = NameAndType #14:#15 // “”😦)V
#33 = NameAndType #7:#8 // value:I
#34 = Utf8 abc
#35 = NameAndType #9:#10 // s:Ljava/lang/String;
#36 = Utf8 HelloExample
#37 = Utf8 java/lang/Object
字面量
字面量接近于java语言层面的常量概念,主要包括:
- 文本字符串,也就是我们经常声明的:
public String s = "abc";
中的"abc"
#3 = String #34 // abc
- 用final修饰的成员变量,包括静态变量、实例变量和局部变量
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
这里需要说明的一点,上面说的存在于常量池的字面量,指的是数据的值,也就是abc
和0x101(257)
,通过上面对常量池的观察可知这两个字面量是确实存在于常量池的。而对于基本类型数据(甚至是方法中的局部变量),也就是上面的private int value = 1
;常量池中只保留了他的的字段描述符I
和字段的名称value
,他们的字面量不会存在于常量池:
符号引用
符号引用主要涉及编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名,也就是
Ljava/lang/String;
这样,将类名中原来的".“替换为”/"得到的,主要用于在运行时解析得到类的直接引用.
#5 = Class #36 // HelloExample
#6 = Class #37 // java/lang/Object
- 字段的名称和描述符,字段也就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量
#2 = Fieldref #5.#33 // HelloExample.value:I
#7 = Utf8 value
#8 = Utf8 I
运行时常量
运行时常量池是方法区的一部分,所以也是全局共享的。我们知道,jvm在执行某个类的时候,必须经过加载、连接(验证,准备,解析)、初始化,在第一步的加载阶段,虚拟机需要完成下面3件事情:
-
通过一个类的**“全限定名”来获取此类的二进制字节流**
-
将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构
-
在内存中生成一个类代表这类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口
这里需要说明的一点是,类对象和普通的实例对象是不同的,类对象是在类加载的时候生成的,普通的实例对象一般是在调用new之后创建。
上面第二条,将class字节流代表的静态储存结构转化为方法区的运行时数据结构,其中就包含了class文件常量池进入运行时常量池的过程。这里需要强调一下,不同的类共用一个运行时常量池,同时在进入运行时常量池的过程中,多个class文件中常量池中相同的字符串只会存在一份在运行时常量池中,这也是一种优化。
运行时常量池的作用是存储 Java class文件常量池中的符号信息。运行时常量池 中保存着一些 class 文件中描述的符号引用,同时在类加载的**“解析阶段”还会将这些符号引用所翻译出来的直接引用(直接指向实例对象的指针)**存储在 运行时常量池 中。
运行时常量池相对于 class 常量池一大特征就是其具有动态性,Java 规范并不要求常量只能在运行时才产生,也就是说运行时常量池中的内容并不全部来自 class 常量池,class 常量池并非运行时常量池的唯一数据输入口;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的较多的是String.intern()(这个方法下面将会详细讲)。
问题解答
====
理解了上述JVM的背景知识之后,再回到最开始的问题.下面这段代码会创建几个对象?
String str=new String(“abc”);