JVM学习笔记(4)-运行时数据区详解之程序计数器与虚拟机栈
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了JVM学习笔记(4)-运行时数据区详解之程序计数器与虚拟机栈,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含9538字,纯文字阅读大概需要14分钟。
内容图文
运行时数据区详解<1>程序计数器与虚拟机栈
- 一.程序计数器(PC寄存器)
- 二.虚拟机栈
一.程序计数器(PC寄存器)
PC Register介绍
官方虚拟机规范关于PC寄存器的介绍 https://docs.oracle.com/javase/specs/jvms/se8/html/
作用:
在运行时数据区中,PC计数器不会发生内存溢出和GC, 虚拟机栈可能会发生溢出,不会进行GC, 方法区和堆可能会发生内存溢出和GC
举例说明
两个常见问题
1:使用PC寄存器存储字节码指令地址有什么用呢
2: PC寄存器为什么会被设定为线程私有
cpu时间片
二.虚拟机栈
虚拟机栈概述
虚拟机栈出现的背景
内存中的栈与堆
虚拟机栈基本内容
栈的特点(优点)
栈中可能会发生的异常
设置栈内存的大小
官方文档参考 : https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
可以搜索-Xss找到对应的解释和命令 : https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
示例:
设置JVM参数的地方
栈的存储单位
栈中存储什么:
栈运行原理
在idea中打断点调试时可以一步步的查看栈的入栈出栈情况
方法的两种结束方式
可以使用javap -v xxx.class反编译之后查看,反编译之后的代码每个方法后面都会有一个return, 如果返回的是int,则是ireturn,
如果是double 返回的是dreturn, 如果是String类型的返回值,则是areturn;
栈帧的内部结构
局部变量表
局部变量表的大小在编译期间就已经确定了,不会再进行改变
jclass插件中字节码文件的查看方法:
查看main方法的字节码文件: 中间的参数代表一个String类型的引用数组
java代码:
静态方法,字节码解析:
length为有效长度
关于Slot(变量槽)的理解
在构造方法和普通非静态方法中都可以使用this, 而this也是需要一个地方进行存放的
ps: 为什么静态方法中不能使用this
Slot的重复利用
举例: 静态变量与局部变量的对比
变量的分类: 按照类型分: 1> 基本数据类型 2> 引用数据类型
按照子啊类中声明的位置分: 1> 成员变量: 在使用前,都经历过默认初始化赋值
类变量(静态变量) : linking的prepare阶段: 给类变量默认赋值
---> initial阶段: 给类变量显式赋值即静态代码块赋值
实例变量: 随着对象的创建, 会在堆空间中分配示例变量空间,并进行默认赋值
2> 局部变量: 在使用前,必须要进行显式赋值!否则编译不通过.
补充说明:
操作数栈(Operand Statck)数据
这里的操作数栈其实是使用数组来实现的, 存储了,由执行引擎根据字节码指令来执行操作,比如add操作,执行引擎会将这些字节码指令翻译成机器指令来执行,将数据放入取出操作数栈.
在这段代码中,i,j,k 首先是存放在了局部变量表中,然后在执行引擎执行字节码指令的过程中,会将需要使用的数据和生成的数据都放到栈中进行临时保存,知道运算完毕.
操作数栈和局部变量表一样,都是在编译器都已经确定好了深度, 同样都是32byte占一个深度,64位占两个深度
javap 解析时可见,这两个用的都是数组结构,两者长度并不相关
代码追踪
其中的bipush表示这个数为byte类型的,但是其实在操作数栈中 byte,short, char, boolean都是以int来存储的,后面的iload也以看得出来.
首先刚刚开始的时候局部变量表和操作数栈都是空的,PC寄存器也是指向了0, 这些指令开头的i都代表了int类型
读取操作指令, bipush: 将15放到操作数栈里面
再读取: istore_1 : 将15这个变量从栈中取出存放到局部变量表为1的位置(这里局部变量表的0的位置存放的是this变量)
读取bipush : 将8放入栈中
读取 istore_2 : 将8取出放到局部变量表的2的位置
PC寄存器的地址也会跟着一直进行调整
然后执行 iload_1,iload_2 将局部变量表中的数据读取出来依次放到栈中
然后执行iadd : 执行青青会将15和8取出进行运算,得到结果13,再将13存入栈中,
再执行 istore_3 : 将栈中的23取出来存放到局部变量表的3的位置
最后执行return结束
从以上的代码可以看出,从始至终,操作数栈所需要的深度只需要2
局部变量表只需要4个深度即可
栈顶缓存技术(Top-of-StatckCashing)
寄存器: 指令更少,执行速度快
动态链接(Dynamic Linking) 或 指向运行时常量池的方法应用
使用常量池的作用主要是为了提供一些符号和常量,便于指令的识别,方便复用
方法的调用:解析与分派, 链接和绑定
链接和绑定
示例
class Animal{
public void eat() {
System.out.println("动物进食");
}
}
interface Huntable{
void hund();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
public void hund() {
System.out.println("狗拿耗子, 多管闲事..");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
super(); // 表现为: 早期绑定
}
public Cat(String name) {
this(); // 表现为: 早期绑定
}
@Override
public void eat() {
super.eat(); // 表现为: 早期绑定
System.out.println("猫吃鱼..");
}
public void hund() {
System.out.println("猫吃耗子,天经地义");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat(); // 表现为: 晚期绑定
}
public void showHunt(Huntable huntable) {
huntable.hund();// 表现为: 晚期绑定
}
}
虚方法与非虚方法
非虚方法
非虚方法都不涉及多态, 即非虚方法都不能被重写, 都是在编译器就可以确定调用的对象
方法调用指令
代码示例
class Father {
public Father() {
System.out.println("father的构造器");
}
public static void showStatic(String str) {
System.out.println("father" + str);
}
public final void showFinal() {
System.out.println("father show final");
}
public void showCommin() {
System.out.println("father 普通方法");
}
}
public class Son extends Father {
public Son() {
super();
}
private Son(int age) {
this();
}
public static void showStatic(String str) {
System.out.println("son" + str);
}
public static void showPrivate(String str) {
System.out.println("son private" + str);
}
public void show() {
// invokestatic
showStatic("huqi.com");
// invokestatic
super.showStatic("good!");
// invokestatic
showPrivate("hello");
// invokespecial
super.showCommin();
// invokevirtual
// 因为此方法声明有final, 不能被子类重写, 所以也认为此方法是虚方法
showFinal();
// invokevirtual
showCommin();
// invokevirtual
info();
MethodInterface mi = null;
// invokeinterface
mi.methodA();
}
public void info() {
}
public void display(Father father) {
father.showCommin();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
interface MethodInterface{
void methodA();
}
关于invokedynamic(动态调用)指令
动态类型语言和静态类型语言
java : String info = "胡琦"; 不能直接info = "胡琦", 即是在编译期间就进行了参数类型的检查.
JS: var name = "胡琦"; var name = 10; var只是形式上的代表变量类型,并没有真正意义上的代表变量的类型
Python : info = 135.6; 只有在运行期间才知道变量的类型
示例:
@FunctionalInterface
interface Func{
boolean func(String str);
}
public class Lambda {
public void lambda(Func func) {
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = s -> {
return true;
};
lambda.lambda(func);
lambda.lambda(s -> {
return true;
});
}
}
方法重写的本质
虚方法表
在类加载子系统的链接阶段中的解析环节,会将符号引用转换为直接引用,其中的符号引用即为下图所示:
例子1:
例子2:
interface Friendly {
void sayhello();
void sayGoodbye();
}
class Dog {
public void sayhello() {
}
@Override
public String toString() {
return "Dog";
}
}
class Cat implements Friendly {
public void eat() {
}
@Override
public void sayhello() {
}
@Override
public void sayGoodbye() {
}
@Override
public String toString() {
return "cat";
}
@Override
protected void finalize(){
}
}
class CockerSpaniel extends Dog implements Friendly {
@Override
public void sayhello() {
super.sayhello();
}
@Override
public void sayGoodbye() {
}
}
public class VirtualmethodTable {
}
Dog虚方法表
CockerSpaniel虚方法表
Cat虚方法表
方法返回地址(Retuen Address)
主要针对正常退出 起作用的
比如在栈中有两个栈帧,即下面A的栈帧调用了上面的B,那么在方法结束的时候, 需要继续执行A的方法,会将A调用者的pc寄存器的地址保存在方法返回地址中,交给执行引擎,确保执行引擎继续执行A方法
lreturn为long类型的返回, freturn为float, dreturn为double, areturn为引用类型的返回
如下
c
x
一些附加信息
栈的相关面试题
举例栈溢出的情况(StatckOverflowError)
通过-Xss设置栈的大小; OOM
调整栈大小,就能保证不出现溢出吗
不能.
只能让溢出出现的晚一些,比如如果死循环递归调用就不行
分配的栈内存越大越好吗
不是,需要合理分配,会挤占其他的资源空间
垃圾回收是否会涉及到虚拟机栈
不会的
方法中定义的局部变量是否线程安全
具体问题具体分析
/**
* 何为线程安全?
* 如果只有一个线程才可以操作此数据, 则必是线程安全的
* 如果有读个线程操作此数据,则此数据是共享数据,如果不考虑同步机制的话,会存在线程安全的问题
*/
public class StringBuilderTest {
int num = 10;
// stringBuilder的声明方式是线程安全的
public static void method1() {
// StringBuilder: 线程不安全
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
}
// 是线程不安全的,有可能被多个线程进行调用
public static void method2(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
// 是线程不安全的,村子啊返回值,坑被多个线程强,都进行操作, 可能会发生逃逸
public static StringBuilder method3() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// 是线程安全的, stringBuilder其实在内部已经消亡了
public static String method4() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}).start();
method2(stringBuilder);
}
}
内容总结
以上是互联网集市为您收集整理的JVM学习笔记(4)-运行时数据区详解之程序计数器与虚拟机栈全部内容,希望文章能够帮你解决JVM学习笔记(4)-运行时数据区详解之程序计数器与虚拟机栈所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。