JVM系统学习之路系列演示代码地址: https://github.com/mtcarpenter/JavaTutorial
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
有不少 Java 开发人员一提到Java内存结构,就会非常粗粒度地将JVM中的内存区理解为仅有 Java 堆(heap)和 Java 栈(stack)?为什么?
首先栈是运行时的单位,而堆是存储的单位
生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈中可能出现的异常
/**
* 演示栈中的异常:StackOverflowError
* 默认情况下:count : 9788
* 设置栈的大小: -Xss256k : count : 2209
*/
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
}
在我的 Win 10 + i7 运行此段代码,当栈深度达到 9788 的时候,就出现栈内存空间不足。
设置栈内存大小
我们可以使用参数 -Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
栈帧(Stack Frame)的格式存在
。栈帧(Stack Frame)
的格式存在。在这个线程上正在执行的每个方法都各自对应一个 栈帧(Stack Frame)
。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。OOP的基本概念:类和对象类中基本结构:field(属性、字段、域)、method
压栈
和 出栈
,遵循“先进后出”/“后进先出”原则。当前栈帧(Current Frame
,与当前栈帧相对应的方法就是 当前方法(Current Method)
,定义这个方法的类就是 当前类(Current Class)
。通过一段代码简单的测试
/**
* @author shkstart
* @create 2020 下午 4:11
*
* 方法的结束方式分为两种:
* ① 正常结束,以return为代表
* ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
*
*/
public class StackFrameTest {
public static void main(String[] args) {
try {
StackFrameTest test = new StackFrameTest();
test.method1();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("main()正常结束");
}
public void method1(){
System.out.println("method1()开始执行...");
method2();
System.out.println("method1()执行结束...");
// System.out.println(10 / 0);
// return ;//可以省略
}
public int method2() {
System.out.println("method2()开始执行...");
int i = 10;
int m = (int) method3();
System.out.println("method2()即将结束...");
return i + m;
}
public double method3() {
System.out.println("method3()开始执行...");
double j = 20.0;
System.out.println("method3()即将结束...");
return j;
}
}
其输出结果为:
method1()开始执行...
method2()开始执行...
method3()开始执行...
method3()即将结束...
method2()即将结束...
method1()执行结束...
main()正常结束
满足栈先进后出的概念,这里通过 idea 的 DEBUG,能够看到栈信息。
栈帧的内部结构
每个栈帧中存储着:
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的。
Slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public class SlotTest {
public void localVar1(){
int a = 0;
System.out.println(a);
int b= 0;
}
public void localVar2(){
{
int a = 0;
System.out.println(a);
}
// 此时的 b 就会复用 a 的槽位
int b= 0;
}
}
变量的分类:
案例演示
将 testAddOperation()
编译成字节码,如下
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
使用 javap 命令反编译class文件: javap -v 类名.class
public void testAddOperation();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 15
2: istore_1
3: bipush 8
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
byte、short、char、boolean 内部都是使用int型来进行保存的 从上面的代码我们可以知道,我们都是通过 bipush 对操作数 15 和 8进行入栈操作 同时使用的是 iadd方法进行相加操作,i -> 代表的就是 int,也就是int类型的加法操作
首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为 0,然后使用 bipush 让操作数 15 入栈。
执行完后,让 PC 寄存器 + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表 1 的位置,我们可以看到局部变量表的已经增加了一个元素
为什么局部变量表不是从 0 开始的呢? 其实局部变量表也是从 0 开始的,但是因为0号位置存储的是 this 指针,所以说就直接省略了~
然后PC 寄存器 +1,指向的是下一行。让操作数 8 也入栈,同时执行 store 操作,存入局部变量表中
然后从局部变量表中,依次将数据放在操作数栈中
然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表 3 的位置
最后PC寄存器的位置指向 10,也就是 return 方法,则直接退出方法
栈顶缓存技术: Top Of Stack Cashing
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了 栈顶缓存
(Tos,Top-of-Stack Cashing)
技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
寄存器:指令更少,执行速度快
动态链接:Dynamic Linking
动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
对应的方法的绑定机制为: 早期绑定(Early Binding)
和 晚期绑定(Late Binding)
。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java 中任何一个普通的方法其实都具备虚函数的特征,它们相当于 C++ 语言中的虚函数(C++中则需要使用关键字 virtual 来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
类的继承关系 方法的重写
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(final 修饰的除外)称为虚方法。
invokednamic指令
动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查) JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
如上图所示:如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object 的方法中。
/**
* 面试题:
* 方法中定义的局部变量是否线程安全?具体情况具体分析
*
* 何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的。
* 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
*/
public class StringBuilderTest {
int num = 10;
//s1的声明方式是线程安全的
public static void method1(){
//StringBuilder:线程不安全
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
//...
}
//sBuilder的操作过程:是线程不安全的
public static void method2(StringBuilder sBuilder){
sBuilder.append("a");
sBuilder.append("b");
//...
}
//s1的操作:是线程不安全的
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是线程安全的
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(() -> {
s.append("a");
s.append("b");
}).start();
method2(s);
}
}
总结一句话就是:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
运行时数据区,是否存在Error和GC?
运行时数据区 | 是否存在Error | 是否存在GC |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是 | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是 | 是 |
简单的回顾下本章节,什么是 Java 虚拟机栈?每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧
,对应着一次次的 Java 方法调用。栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。栈帧的 压栈
和 出栈
,遵循“先进后出”/“后进先出”原则。不同线程中所包含的栈帧是不允许存在相互引用。JVM 栈顶缓存
技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。方法返回地址,一个方法的结束,有两种方式:正常执行完成和出现未处理的异常,非正常退出。
欢迎关注公众号 山间木匠 , 我是小春哥,从事 Java 后端开发,会一点前端、通过持续输出系列技术文章以文会友,如果本文能为您提供帮助,欢迎大家关注、 点赞、分享支持,我们下期再见!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。