JVM的运行时数据区分为五个部分:
方法区(HotSpot JVM独有),堆区(垃圾回收的重点区域),本地栈(存放jvm运行时所需的c/c++库),程序计数器,元空间(常量池,元信息存储位置)。
此外,有些资料将JIT编译产物归属为第六类,有些人认为它属于元空间内容,都是一样的。
示意图:
文章目录
一、虚拟机线程
虚拟机线程与操作系统的线程一一对应。
每个虚拟机线程拥有一套独立的程序计数器、虚拟机栈和本地方法栈,会在每个线程初始化时为他创建。这个部分会自动销毁。
所有线程共用堆区和元空间(严谨点说是堆外内存)。而这两个部分也是垃圾回收机制需要回收的区域。
当虚拟机线程创建时,会建立向操作系统线程的映射。 操作系统会为将其分配到一个能够调度的CPU上,建立一个操作系统线程,称为 `本地线程` ,之后由操作系统调用JVM线程的run();
当本地线程执行完毕之后,直接销毁;
jvm会根据是否只剩下守护线程选择销毁。
守护线程:由jvm负责创建、维护系统工作的线程,从始至终在后台工作,如GC线程就是守护线程。
JVM后台线程分类:
二、程序计数器
2.1 作用及工作原理
程序计数器,英文为Program Counter Register,全称应该是程序计数寄存器,因此也有简称为程序寄存器的【以下简称PC】。
有一种很形象的称呼:代码钩子, 比喻借鉴的是北京烤鸭的烤鸭钩子,可以勾出指定的那个鸭。
PC就是这样的东西,只不过他是用来勾出指定的代码的。
PC是JVM中存取速度最快的区域
。
工作功能描述:
PC会记录一条指令的位置,代表当前方法栈的对应方法的将要执行的下一条语句的编号。
执行引擎会根据这个编号去寻找这个位置,对其进行翻译。
只会指向java方法, 若指向本地方法栈的native方法内容会变成undefined。
PS:
执行引擎是解释与编译并行的,而翻译过程用的字节码解释器
的向导就是PC。
作用示意图:
可以看到,执行引擎和PC指向同一个位置。
每个线程拥有一个PC,PC与线程生命周期一致。
PC没有垃圾回收机制,且没有OOM
【OutOfMemoryError】,他也是唯一一个两者都没有的数据区单元。
OOM, 指的是内存溢出错误。
讨论任何数据区单元都需要讨论有没有PC和OOM。
如,堆和元空间拥有GC,而方法栈只有OOM.
Pc在一段程序中的作用位置示意图:
PC工作实例【通过一段简单代码演示PC的作用,其实是学一下常见的栈指令。。。。】
/**
* PC寄存器的作用
*/
public class Demo1 {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
String s = "ad";
System.out.println(c);
System.out.println(s);
}
}
下面最左边的标号就是PC所存储的内容,执行引擎会根据这段信息去特定的位置查找代码。
可以看到#7位置对应的常量池内容是String,可以推断ldc的意思是利用String常量池的内容为String引用赋值。
这也验证了不通过new关键字得到的String对象是从常量池中找的。
- ps:两个小知识:
CPU时间片:
cpu是基于调度的工作区域,会为每个线程分配时间片,即工作时间,一旦工作时间结束,线程就会被阻塞,轮换到其他线程执行,直到下次轮到他执行才可以上机。
并行并发:
并行是指几个线程同时执行:如多个GC线程会并行执行。
并发是指基于时间片的运行,每个线程只能轮流地执行一段时间片,就会切换到其他时间片,在同一时刻不能同时运行,但可以说在同一段时间
内,并发的线程是同时执行的。
串行:并行的反义词,指的是一段线程执行完之后,才会轮到下一段线程执行。GC与用户进程之间的运行就是串行的。
2.2 两道面试题
1.如图:
2. 为什么PC是线程私有的?【即每个线程有一份】
由于JVM是并发执行的,因此若PC是共享的,切换线程时新的线程就会把原来的线程的PC值覆盖,这样再切换回老线程就没法继续执行了。
三、虚拟机栈
JVM的指令集是基于栈结构的,
多平台,设计简单,指令数量多,功能少。
效率低,指令条数多。
每个线程都独立拥有一个虚拟机栈。
虚拟机栈是除了PC的JVM中速度最快的区域。
3.1 认识
栈主要是用于程序运行使用,但是也可以存储一些数据,如局部变量,对象引用等。
堆主要用来存储数据。
注意:
栈的空间在真实的内存中开辟,而且其大小为用户定义, 因此栈的大小可能大于堆的大小
除了栈以外,其他区域的空间都属于JVM的虚拟内存。
虚拟机栈不存在GC,存在OOM
PS:
独立选定, 动态拓展。
区分StackOverflowError与OutOfMemoryError,都是虚拟机栈的常见异常。
Stack是虚拟机栈溢出异常,方法栈大小是执行之前确定的,超过这个确定值就会溢出。
OutOfMemory也是虚拟机栈内存超过限制,不过此时的栈大小是自动分配的,他说的不足是因为真实的内存空间确实不足了。
两者区别在于在前分配还是动态分配。
调优1:设置虚拟机栈的大小
通过设置参数-Xss size
除了Windows之外的系统栈大小默认是1024k,Windows是根据当时状态确定的,可以按照k或者m来进行设置。如,-Xss 256k
示例:演示如何体现出栈大小的改变:
通过main()调用自己可以实现栈溢出
public class Demo1 {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args); //最简单的实现栈溢出的代码样例
}
}
设置栈空间大小之前,打印了15320次:
设置256k之后:
减少了约五分之四。
IDEA设置栈空间:
-
虚拟机栈永远指向当前栈帧。当该方法执行完毕时,栈的这一栈帧会丢弃。
-
虚拟机栈不允许一个线程调用另一个线程中的栈帧。
-
一个方法只有两种结束方式:
正常return或者异常返回。
无论怎样的结束方式,都会丢弃当前栈帧
栈帧内部结构: 下面会逐一说明这几个结构。
3.2 局部变量表
局部变量表本质上是一个数组,他的数据单元是Slot。
基本单位:Slot【变量槽
】
可存储局部变量,对象引用,returnAddress等信息。
局部变量表大小在编译时就已经确定。
各数据类型所占的空间分为1Slot与2Slot,只有double与long占两个Slot
slot按照源代码文本中的顺序分配索引。
占双slot的类型【double,long】的索引是他的第一个slot位置。
局部变量表越大,栈帧越大,一个栈包含的栈帧就越少。【这是局部变量表对栈性能影响的体现】
局部变量表只对当前栈帧有效,一旦栈帧销毁,局部变量表随之销毁。
皮之不存毛将焉附。
栈帧各个位置的信息:
方法总体描述如下:
可以根据他的描述组合出完整的方法签名。
行号表:
体现原文方法代码行号LineNumber与方法栈的行号StartPC的对应关系。 注意,源代码行号的位置start pc是从**有效语句**开始的。【即大括号内的第一行语句】
局部变量表总体信息:
code length是翻译为字节码后代码的行数,这个会在后面具体信息有体现。
最大允许的局部变量数:这个在编译时静态的设定。
局部变量表具体信息:
方法中每有一个局部变量,局部变量表就多出一行。
内容有:
- start pc:该变量开始起作用的位置【说人话就是声明的位置。】
- Length:该局部变量起作用的行数。【在方法内部也会存在局部区域,如if{}中就是局部区域。因此起作用不一定是到最后一行,后面会有一个使用内部局部变量覆盖的情形】
我们发现,三行局部变量的StartPC和Length加起来都是16,这个值就是全局信息中CodeLength的数值,故表示这三个变量都是定义在方法的非局部区域的。
- 还有一个局部变量的描述信息,主要是描述这个局部变量的类型的。
若是用户自定义的引用类型,会使用L开头表示。
PS:
`this`也是一个变量,也会存储在局部变量表中,并且只有非静态方法有这个this的slot。
与java的联系:
java中不允许在静态方法中使用this。现在明白了,因为this不在static方法的局部变量表。
局部套局部: 局部变量表的`重复利用`。 方法内部也会存在局部区域,而这个局部内的变量一旦生命周期结束,他的Slot索引【槽位】会被局部外部的变量顶替掉
与java的联系(2): java中存在这几种类型的变量: + 成员变量: 1. 类变量,静态变量 他会在类加载的**prepare阶段**进行定义,并赋值默认值; 2. 实例变量【即在类中定义的非静态变量】 **随着对象在堆中的初始化,会为他进行创建,并赋值默认值**
- 局部变量:
- 不会进行默认初始化,若想要在局部中使用,必须进行初始化(即使初始化为默认值),否则编译就不会通过。
局部变量表小结
局部变量表是JVM虚拟机栈性能调优最重要的部分,也是对栈性能影响最大的部分。
局部变量表也是最重要的垃圾回收算法根搜索算法
的一个根节点【有无引用决定回不回收】。
综上,都是说局部变量表非常重要就是。
3.3 操作数栈
操作数栈是临时存储方法流程中的数据的地方,是由数组实现的栈结构,因此不能通过索引获取值,而是通过出栈和入栈进行存取。
操作数栈的深度由临时存取的数据多少有关。【就好像算法中的递归栈,同一个方法区下一个递归函数调用之前前面的递归函数必须先返回,因此他的栈深度会不断增减,必然达不到一个区域所有的数字大小的深度。如,下面的简单程序,操作数的深度只要为2即可。】
单纯这么一说,好像和临时变量表差别不大?其实有本质的区别。
打个比方:
大家应该都玩过吃鸡吧?
吃鸡里每个人都可以带很多类型的装备,有枪,平底锅,药品,手雷等等,这些东西都能存在背包或者挂在身上,就好比是临时变量表的作用。
但是,任何时候我们能使用的装备就只有一个,你不可能一边磕药一边打枪,不然不就锁血了。。。,拿在手上的装备就好比是操作数栈,因此操作数栈中的数据就是当前操作需要的数据。
而“操作”本身就好像是吃鸡的嗑药,打枪,扔手雷等等,操作数栈中能用的有xadd, xinc等等。
不同点在于,每个操作数栈的元素只能使用一次,一旦使用完毕就被覆盖。
这样设计的显而易见的好处是:
操作数栈无需指定索引,当前操作需要多少数据就去操作数栈顶pop几个元素即可【根据指令大小决定pop范围】。
相比之下,临时变量表必须要索引,因此可以看到他的指令都是xxx_i
这样,若没有索引,就没法每次都能取到任何一个数据了。
而这也是为什么说JVM的指令都是零地址指令
的原因。
这其实和他们的名称是相关的:
虽然两者的实现都是数组,但是前者叫栈,后者叫表,存取方式可见一斑。
对应的字节码指令:
- xpush : 存储x类型的数值进操作数栈;
- xload_i : 从临时变量表取得第i个索引位置的x类型元素并存到操作数栈中。
- xadd: 将操作数栈顶的两个x类型元素进行相加,结果保存到操作数栈顶。
- xstore_i: 将操作数栈顶的x类型元素保存临时变量表的第i个位置。
之所以非得要类型,是因为取数值的需要范围,不同类型数据的范围是不一致的。
操作数栈对待不同的数据类型分配不同大小的空间,其中:
double和long【即64bit数据】占据两个栈空间,其他【即32bit即以下数据】占据一个栈空间。
操作数栈的空间大小在编译时定义。一个方法(栈帧)对应一个操作数栈。
当方法返回时,需要将返回值压入调用方法的操作数栈。
小案例:
下面是一段程序与其对应的操作数的处理。
上面一段程序对应的操作数栈工作流程:
- PC存储当前方法第一个指令地址0;
- 执行引擎执行第一条指令
- 原指令:byte i = 15;即声名一个byte类型的声名并赋值为15;【虽然写作一行,但是实际上是两种操作】
- 字节码指令:bipush 15; istore_1; 将15存储到操作数栈中byte大小的位置【一个栈空间】;将操作数栈的顶层元素放到局部变量表索引为1的位置【注意,这里byte指令使用的是istore,是因为32bit以下可以转化为整数的数据都被当做int来处理,如short,char,byte】。
之所以是存到索引为1而不是0,这是因为0的位置存储着非静态方法都有的变量:this。
- PC索引更新到2,执行第三条指令;和上面一毛一样
【但是一样就很奇怪了,为什么int类型使用的也是bipush?查了一下,当int取值在-15之间时,使用`iconst`;当int取值为-128127之间时,使用
bipush
;当取值-2147483648~2147483647之间时,使用sipush
。是不是很眼熟?这说明-128~127之间的整形数据是从整形常量池中取的
】
4. PC更新到6,执行第6条指令。
- 原指令:int k = i + j; 看上去只有一条,实际上有,取i,取j,i + j, 存到k,保存变量表这些操作;
字节码指令:
- iload_1, 从临时变量表下标1的位置取得变量i,放到操作数栈顶;
- iload_2, …j…
- iadd 取出操作数栈顶的两个元素相加,并放到操作数栈顶;
- istore_3, 将操作数栈顶元素取出,放到临时变量表索引为3的位置;
- return,方法正常返回。
面试题:
关于i++
与 ++i
的问题:
i++ 与 ++i的各项字节码操作:
public static void main(String[] args) {
int i1 = 0;
int i2 = i1++;
i1 = 0;
int i3 = ++i1;
int i4 = 0;
i4 = i4++;
i4 = 0;
i4 = ++i4;
int i5 = 0;
int i6 = i5++ + ++i5;
}
0 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_2
7 iconst_0
8 istore_1
9 iinc 1 by 1
12 iload_1
13 istore_3
14 iconst_0
15 istore 4
17 iload 4
19 iinc 4 by 1
22 istore 4
24 iconst_0
25 istore 4
27 iinc 4 by 1
30 iload 4
32 istore 4
34 iconst_0
35 istore 5
37 iload 5
39 iinc 5 by 1
42 iinc 5 by 1
45 iload 5
47 iadd
48 istore 6
50 return
从字节码指令可以看出,i++和++i的指令都是一样的,都是:iinc i by 1
,其中i表示这个数字在临时变量表的下标。
区别在于iload i的位置:
iload 5
iinc 5 by 1
iinc 5 by 1
iload 5
i++在于先执行iload,然后在iinc;++i反之。
前面说过,iload是从临时变量表取值存储到操作数栈顶,而iinc操作需要下标,想必是直接对临时变量表的操作,因此,若先行吧这个数加载到操作数栈顶,他的++就不能体现到下次的操作数栈的运算中去了。
反之,若先对临时变量表对应元素进行加一,之后+1过后的数就会读取到操作数栈顶,因此也就可以体现出数组的改变了。
前沿技术
近来,HotSpot虚拟机设计者提出了ToS技术,这是一门提升操作数栈的读取速度的技术。
既然操作数栈读取元素不考虑下标,而且使用频率较高,就可以直接将他存储在cpu的寄存器中,学过组成原理都知道,寄存器是存储系统中存取速度最快的区域,并且就在cpu内核旁边,这样可以大大加快原本只能设定在内存的操作数栈的存取速度。
但是这一技术还在测试之中。
3.4 动态链接
有的资料将动态链接,方法返回地址,附加信息放在一起统称为帧数据区。
动态链接是指帮助栈帧的符号引用转化为实际的指向方法区的方法引用的东西。
当前方法的栈帧会保存一些方法所需的另外一些方法的引用,这些引用来自于常量池,而常量池只提供这些方法的符号引用,即一个代号,动态链接就是用来这种符号的引用转化为真实的方法引用。
真实名称叫做指向运行时常量池的方法引用
private int count;
public void method() {
System.out.println("methodA");
method2();
}
public void method2() {
System.out.println("methodB");
this.count++;
}
对应的字节码
method1:
0 getstatic #7 <java/lang/System.out>
3 ldc #13 <methodA>
5 invokevirtual #15 <java/io/PrintStream.println>
8 aload_0
9 invokevirtual #21 <com/peng/chapter4/part4/Demo1.method2>
12 return
method2:
0 getstatic #7 <java/lang/System.out>
3 ldc #26 <methodB>
5 invokevirtual #15 <java/io/PrintStream.println>
8 aload_0
9 dup
10 getfield #28 <com/peng/chapter4/part4/Demo1.count>
13 iconst_1
14 iadd
15 putfield #28 <com/peng/chapter4/part4/Demo1.count>
18 return
对应的常量池:
可以看到method1()的第0行有一个getStatic #7
,意思就是去常量池符号为#7的位置查找,展开常量池的#7,发现为:
即对应的是System类的下的PrintStream类型,名字叫做out的变量(field)。
第二句加载#13位置,展开看:
这是一个字符串的常量,提示我们去常量池(cp_info, 即constantPool_info)的第#14查看。
打开#14, 可以看到完整的字符串信息。(之后,任何指向值为methodA的字符串都会指向#14的位置)
5 invokevirtual #15
这句的意思是调用一个空返回(virtual)的方法,他的位置在#15
他会加载PrintStream类来执行println()这个方法,此后,pringln()的方法和类都会被放置到常量池,即符号位#16和#17的常量。
跳过几句重复的代码,查看调用method2()的过程。
9 invokevirtual #21 <com/peng/chapter4/part4/Demo1.method2>
invokevirturl 表明调用的是一个具有多态性质的方法。
提示去#21查找这个方法
#21处存储了这个virtual方法的类和方法帧存放的位置,之后我们就不一一看了。
ps:在c++中,virtual关键字修饰多态方法,即空的待实现的方法。
指针是为了重用
字节码不应该太多,保存过多数据,因此才有符号引用代替
3.5 方法调用
(1)方法的链接机制【即实现符号引用和实际方法绑定的机制】
-
静态链接:
编译期间就能确定方法的指向,并且在运行过程中一直保持不变,叫静态链接。 -
动态链接
在方法执行时会动态根据需要改变引用的绑定方法叫做动态链接。
(2)这两种链接方式解决的是两种方法的绑定机制:
3. 早期绑定:
在编译期间就能确定下对应方法位置的绑定。
4. 晚期绑定
随着方法运行才能动态确定实际方法指向。
晚期绑定在多态中体现最多
使用一个case来说明。
/**
* 静态绑定与动态绑定
*/
public class Demo {
interface Huntable{
void hunt();
}
static class Animal{
public void eat() {
}
}
static class Dog extends Animal implements Huntable{
@Override
public void eat() {
System.out.println("狗吃肉");
}
@Override
public void hunt() {
System.out.println("狗拿耗子");
}
}
static class Test{
public void show(Animal animal) {
animal.eat();
}
public void show2(Huntable huntable) {
huntable.hunt();
}
public static void main(String[] args) {
Test test = new Test();
test.show(new Dog());
test.show2(new Dog());
}
}
}
查看Test类的字节码:
0 aload_1
1 invokevirtual #7 <com/peng/chapter4/part5/Demo$Animal.eat>
4 return
0 aload_1
1 invokeinterface #12 <com/peng/chapter4/part5/Demo$Huntable.hunt> count 1
6 return
可以看到,他们使用invokevirtual
与invokeinterface
关联对应的类是Animal类。
实际上,他们最终会追溯到对应的子类重写方法,这就是动态绑定。【特别是接口,他连实体都没有,自然指向接口是不合理的,而这只能在方法运行时JVM才能感受到,由动态链接参与绑定。】
3.6 虚方法与非虚方法
编译时确定调用的版本,且运行时不存在切换的方法叫做非虚方法。
与之相对应的就是虚方法。
非虚方法只有五种,除此之外都是虚方法:
- final修饰的方法
- 静态方法
- super.方法,即父类方法
- 构造方法
- 私有方法
对应起来,大体上都是不能重写的方法。
与之相关的,有虚拟机栈的一堆指令:
- invokestatic:调用静态方法
- invokespecial:调用private,构造器方法【<init>】,super方法
虚方法:
- invokevirtual
- invokeinterface
其中尤其注意final方法,他虽然是非虚方法,但是调用时使用invokevirtual
老规矩,一个例子:
PS:我发现,在子类中,可以重写父类方法的同时,将重写的父类方法标记为final
源代码:
class Father {
public Father() {
System.out.println("father constructor");
}
public void fatherShow() {
System.out.println("fatherShow");
}
public final void fatherFinal() {
System.out.println("father final");
}
public static void fatherStatic() {
System.out.println("father static");
}
}
class Son extends Father {
public Son(){
super();
}
public Son(String name) {
this();
}
@Override
public final void fatherShow() {
System.out.println("Son Show");
}
public void sonShow() {
super.fatherShow();
fatherShow();
}
public void showFaterFinal() {
super.fatherFinal();
fatherFinal();
}
public static void sonStatic() {
fatherStatic();
}
}
逐一对子类方法的字节码进行分析:
①子类构造1:
0 aload_0
1 invokespecial #1 <com/peng/chapter4/part7/Father.<init>>
4 return
使用invokespecial,指向是父类的<init>
②子类构造2:
0 aload_0
1 invokespecial #7 <com/peng/chapter4/part7/Son.<init>>
4 return
同样是invokespecial,这次指向之类的构造。
可以看出,构造方法使用invokespecial。是一个非虚方法。
③重写的父类方法:
不涉及父类调用,只有一个println的虚调用
0 getstatic #10 <java/lang/System.out>
3 ldc #16 <Son Show>
5 invokevirtual #18 <java/io/PrintStream.println>
8 return
④SonShow()
在这个方法,我通过super.与直接调用的方式调用了两次同名方法,
0 aload_0
1 invokespecial #24 <com/peng/chapter4/part7/Father.fatherShow>
4 aload_0
5 invokevirtual #27 <com/peng/chapter4/part7/Son.fatherShow>
8 return
可见,super.的方式被视为special,而自己子类重写并标记为fianl的方法被视为virtual。
因此遇到final要尤其小心。
⑤showFatherFinal()方法
这个方法调用很特殊,我首先使用super.调用父类的final方法,并且使用不加super.的方式调用了一次。
发现,super.被视为special,而直接调用的final被视为virtual。
可见,super关键字的优先级高于final的virtual特性。
0 aload_0
1 invokespecial #24 <com/peng/chapter4/part7/Father.fatherShow>
4 aload_0
5 invokevirtual #27 <com/peng/chapter4/part7/Son.fatherShow>
8 return
⑥sonStatic()
调用父类的static方法,
0 invokestatic #32 <com/peng/chapter4/part7/Son.fatherStatic>
3 return
static方法使用invokestatic调用。
动态类型语言与静态类型语言以及invokedynamic
编程语言按照变量类型的检查方式分为静态类型语言与动态类型语言两种。
静态类型语言会在编译时检查变量是否符合声名的类型,即变量要符合变量自身的类型
动态编程语言的变量本身没有类型,当编译器检查到该变量的值时,这个变量自身的类型才被确定了,即变量值决定变量类型。
由上述分析,可见java使用静态类型语言。
如,声名变量需要指定类型的语言,c,java等都是静态;
而像python,js,kotlin等使用var或者没有变量提示符的语言就是动态语言。
动态语言可以说是语言发展的潮流,为了跟上这种潮流,也为了让JVM能够跑多种动态类型的语言,jvm在java7时引入了invokedynamic这条虚拟机指令。
但是在java7中不能通过直接的代码得到。
到了java8,java推出了lambda表达式,就是一种典型的动态结构。
lambda表达式不具有确定的结构(其实是一种对象)。
lambda结构方法的分配就使用invokedynamic。
case:
public class Demo2 {
public void method() {
TestLambda lambda = () -> {
System.out.println("This is a lambda expression!");
};
System.out.println(lambda);
}
@Test
public void test() {
method();
}
}
@FunctionalInterface
interface TestLambda{
void method();
}
字节码指令:
0 invokedynamic #7 <method, BootstrapMethods #0>
5 astore_1
6 getstatic #11 <java/lang/System.out>
9 aload_1
10 invokevirtual #17 <java/io/PrintStream.println>
13 return
可以看到第一条语句中使用了invokedynamic调用#7的一个BootStrapMethod。
方法重写的实质:
由于第三步搜索各父类太过耗时,JVM使用虚方法表来记录各个虚方法的信息。
虚方法表、常量池符号引用都在链接阶段生成。
在这幅图中,蓝色的方法没有经过重写,因此会在虚方法表中直接指向父类的Object类。
白色的方法经过了重写,因此指向他们自己类的方法区。
还有一个例子:
Speicher继承Dog类。
Dog重写了接口的两个方法和Object的toString()方法。
Speicher同样重写了这两个方法,但是没有重写Dog的toString();
虚方法表指向就是这样的;
toString()会指向父类的toString()
而重写的两个方法指向自己的。
3.7 方法返回地址
当执行方法调用时,PC值会从当前方法更新到新的方法的首行。
但是,当新方法执行完毕时,也必须要返回上一个方法的调用位置。
而执行保存返回地址位置的单元就是方法返回地址。
方法返回地址有这几种:
- return : 空返回或者构造方法返回
- ireturn:char,byte,int,short返回
- lreturn: long返回
- dreturn :double返回
- freturn:float返回
- areturn:引用类返回。
String返回值被视为引用类型返回。
但是方法调用也会出现异常,异常时候不会有方法返回,而是将异常信息写异常信息表
中。
异常信息表也会记录程序中对待异常处理的方式。
case:
public void useRead() {
try {
readFile();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readFile() throws IOException {
FileReader fr = new FileReader("ad.txt");
int len = 0;
char[] bytes = new char[1024];
while ((len = fr.read(bytes)) != -1) {
System.out.println(len);
}
}
useRead()的异常信息表。
从字节码的0到4行为异常处理阶段,采用捕获的方式。
从映射表可以看出实际代码的位置:
《这里java代码没有带行号》
同样,对readFile()的异常处理:
抛出异常会直接开辟一个Exceptions的区域,位于code的同等级位置,描述异常类型与符号引用。
3.8 其他附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
这些信息不是必须的,因此可能没有。
3.9 与虚拟机栈相关的面试题
- 栈溢出的情况:
- 当栈大小是事先分配的(通过
-Xss
运行参数),若程序运行时候所需的方法空间大于这个事先分配的空间,会发生StackOverFlowError的异常; - 当栈大小是动态分配的,若是其所需空间大于计算机当前实际能够使用的内存大小,就不能分配给他,会发生OutOfMemoryError。
-
调整栈大小,是否就能避免栈溢出?
不是,若程序出现死循环,栈空间无论多大都有消耗殆尽的一天 -
分配的栈空间是越大越好吗?
不是。当栈分配的过多时,其他数据区的单元的内存就会变少,整体性能不升反降。但是,若是在合理的范围内提升肯定是越大越好。 -
垃圾回收是否会回收虚拟机栈?
不会,只对堆和方法区有影响。 -
方法中的局部变量是否一定线程安全?
不一定,需要具体问题具体分析。
当方法返回值或者形参有引用类变量时,是线程不安全的。
- 返回值: 当返回引用类变量时,其他调用这个方法的进程会同时争抢操作这个返回值的权利,就会引发线程不安全。
- 形参:当传递给当前方法的形参是由多个线程提供的时,会引发线程安全问题。
当方法在内部使用变量时,是安全的,这种情况叫做内部消亡
public class StringBuilderTest {
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
注意第四个方法:
在内部新开辟一个线程,去共同使用本方法内部的变量,也是线程不安全的。