内存中的栈与堆
首先栈是运行时的单位,而堆是存储的单位
虚拟机栈的基本内容
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,栈是线程私有的
虚拟机栈的生命周期
生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了
虚拟机栈的作用
主管Java程序的运行,它保存方法的局部变量(8 种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个:
对于栈来说不存在垃圾回收问题(栈存在溢出的情况)
栈中可能出现的异常
面试题:栈中可能出现的异常
栈异常演示
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
设置栈内存的大小
-Xss1024m // 栈内存为 1024MBS
-Xss1024k // 栈内存为 1024KB
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
栈存储什么?
栈的运行原理
代码示例:
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test.method1();
}
public void method1() {
System.out.println("method1()开始执行...");
method2();
System.out.println("method1()执行结束...");
}
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()执行结束...
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String method1()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #8 // Method method2:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #9 // String method1()执行结束...
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
1234567891011121314151617181920212223
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String method2()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: bipush 10
10: istore_1
11: aload_0
12: invokevirtual #11 // Method method3:()D
15: d2i
16: istore_2
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #12 // String method2()即将结束...
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: iload_1
26: iload_2
27: iadd
28: ireturn
LineNumberTable:
line 22: 0
line 23: 8
line 24: 11
line 25: 17
line 26: 25
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/atguigu/java1/StackFrameTest;
11 18 1 i I
17 12 2 m I
public double method3();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String method3()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: ldc2_w #14 // double 20.0d
11: dstore_1
12: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #16 // String method3()即将结束...
17: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: dload_1
21: dreturn
LineNumberTable:
line 30: 0
line 31: 8
line 32: 12
line 33: 20
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
12 10 1 j D
栈帧内部结构
每个栈帧中存储着:
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的
认识局部变量表
局部变量表所需的容量大小是在编译期确定下来的
public class LocalVariablesTest {
private int count = 0;
public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}
public void test1() {
Date date = new Date();
String name1 = "atguigu.com";
test2(date, name1);
System.out.println(date + name1);
}
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;//占据两个slot
char gender = '男';
return dateP + name2;
}
}
思考:
public static void main(String[] args) {
if(args == null){
LocalVariablesTest test = new LocalVariablesTest();
}
int num = 10;
}
字节码中方法内部结构的剖析
关于 Slot 的理解
Slot 代码示例
this 存放在 index = 0 的位置:
public void test3() { this.count++;}
64位的类型(1ong和double)占用两个slot
public String test2(Date dateP, String name2) { dateP = null; name2 = "songhongkang"; double weight = 130.5;//占据两个slot char gender = '男'; return dateP + name2;}
static 无法调用 this
//练习:public static void testStatic(){ LocalVariablesTest test = new LocalVariablesTest(); Date date = new Date(); int count = 10; System.out.println(count); //因为 this 变量不存在于当前方法的局部变量表中!! //System.out.println(this.count);}
Slot 的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public void test4() { int a = 0; { int b = 0; b = a + 1; } //变量c使用之前已经销毁的变量b占据的slot的位置 int c = a + 1;}
静态变量与局部变量的对比
变量的分类:
代码示例
补充说明
操作数栈的特点
操作数栈:Operand Stack
代码举例
操作数栈的作用
操作数栈的深度
通过反编译生成的字节码指令查看操作数栈的深度
操作数栈代码追踪
public void testAddOperation() { //byte、short、char、boolean:都以int型来保存 byte i = 15; int j = 8; int k = i + j;}
0 bipush 15 2 istore_1 3 bipush 8 5 istore_2 6 iload_1 7 iload_2 8 iadd 9 istore_310 return
程序执行流程如下
关于 int j = 8; 的说明
关于调用方法,返回值入操作数栈的说明
public int getSum(){ int m = 10; int n = 20; int k = m + n; return k;}public void testGetSum(){ //获取上一个栈桢返回的结果,并保存在操作数栈中 int i = getSum(); int j = 10;}
i 与 i 的区别
// 程序员面试过程中, 常见的i++和++i 的区别,放到字节码篇章时再介绍。public void add(){ //第1类问题: int i1 = 10; i1++; int i2 = 10; ++i2; //第2类问题: int i3 = 10; int i4 = i3++; int i5 = 10; int i6 = ++i5; //第3类问题: int i7 = 10; i7 = i7++; int i8 = 10; i8 = ++i8; //第4类问题: int i9 = 10; int i10 = i9++ + ++i9;}
0 bipush 10 2 istore_1 3 iinc 1 by 1 6 bipush 10 8 istore_2 9 iinc 2 by 112 bipush 1014 istore_315 iload_316 iinc 3 by 119 istore 421 bipush 1023 istore 525 iinc 5 by 128 iload 530 istore 632 bipush 1034 istore 736 iload 738 iinc 7 by 141 istore 743 bipush 1045 istore 847 iinc 8 by 150 iload 852 istore 854 bipush 1056 istore 958 iload 960 iinc 9 by 163 iinc 9 by 166 iload 968 iadd69 istore 1071 return
i++
//第2类问题:int i3 = 10;int i4 = i3++;
12 bipush 1014 istore_315 iload_316 iinc 3 by 119 istore 4
++i
int i5 = 10;int i6 = ++i5;
21 bipush 1023 istore 525 iinc 5 by 128 iload 530 istore 6
总结:
注:以下是网友总结 i 与 i 的区别,我觉得不错,就加进来啦~~~
i++
++i
栈顶缓存技术:Top Of Stack Cashing
动态链接(或指向运行时常量池的方法引用)
动态链接:Dynamic Linking
代码示例
public class DynamicLinkingTest { int num = 10; public void methodA(){ System.out.println("methodA()...."); } public void methodB(){ System.out.println("methodB()...."); methodA(); num++; }}
public void methodB(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String methodB().... 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: invokevirtual #7 // Method methodA:()V 12: aload_0 13: dup 14: getfield #2 // Field num:I 17: iconst_1 18: iadd 19: putfield #2 // Field num:I 22: return LineNumberTable: line 16: 0 line 18: 8 line 20: 12 line 21: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest;
Constant pool: #1 = Methodref #9.#23 // java/lang/Object."<init>":()V #2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I #3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream; #4 = String #27 // methodA().... #5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V #6 = String #30 // methodB().... #7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V #8 = Class #32 // com/atguigu/java1/DynamicLinkingTest #9 = Class #33 // java/lang/Object #10 = Utf8 num #11 = Utf8 I #12 = Utf8 <init> #13 = Utf8 ()V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 LocalVariableTable #17 = Utf8 this #18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest; #19 = Utf8 methodA #20 = Utf8 methodB #21 = Utf8 SourceFile #22 = Utf8 DynamicLinkingTest.java #23 = NameAndType #12:#13 // "<init>":()V #24 = NameAndType #10:#11 // num:I #25 = Class #34 // java/lang/System #26 = NameAndType #35:#36 // out:Ljava/io/PrintStream; #27 = Utf8 methodA().... #28 = Class #37 // java/io/PrintStream #29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V #30 = Utf8 methodB().... #31 = NameAndType #19:#13 // methodA:()V #32 = Utf8 com/atguigu/java1/DynamicLinkingTest #33 = Utf8 java/lang/Object #34 = Utf8 java/lang/System #35 = Utf8 out #36 = Utf8 Ljava/io/PrintStream; #37 = Utf8 java/io/PrintStream #38 = Utf8 println #39 = Utf8 (Ljava/lang/String;)V
为什么要用常量池呢?
静态链接机制与动态链接机制
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
方法的绑定机制
静态链接和动态链接对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
代码示例
/** * 说明早期绑定和晚期绑定的例子 * * @author shkstart * @create 2020 上午 11:59 */class Animal { public void eat() { System.out.println("动物进食"); }}interface Huntable { void hunt();}class Dog extends Animal implements Huntable { @Override public void eat() { System.out.println("狗吃骨头"); } @Override public void hunt() { 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("猫吃鱼"); } @Override public void hunt() { System.out.println("捕食耗子,天经地义"); }}public class AnimalTest { public void showAnimal(Animal animal) { animal.eat();//表现为:晚期绑定 } public void showHunt(Huntable h) { h.hunt();//表现为:晚期绑定 }}
多态性与方法绑定机制
虚方法与非虚方法
虚方法与非虚方法的区别
子类对象的多态的使用前提:
虚拟机中调用方法的指令
四条普通指令:
<init>
方法、私有及父类方法,解析阶段确定唯一方法版本一条动态调用指令
invokedynamic:动态解析出需要调用的方法,然后执行
区别
代码示例:
/** * 解析调用中非虚方法、虚方法的测试 * * invokestatic指令和invokespecial指令调用的方法称为非虚方法 * @author shkstart * @create 2020 下午 12:07 */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 showCommon() { System.out.println("father 普通方法"); }}public class Son extends Father { public Son() { //invokespecial super(); } public Son(int age) { //invokespecial this(); } //不是重写的父类的静态方法,因为静态方法不能被重写! public static void showStatic(String str) { System.out.println("son " + str); } private void showPrivate(String str) { System.out.println("son private" + str); } public void show() { //invokestatic showStatic("atguigu.com"); //invokestatic super.showStatic("good!"); //invokespecial showPrivate("hello!"); //invokevirtual //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。 showFinal(); //invokespecial super.showCommon(); //invokevirtual //有可能子类会重写父类的showCommon()方法 showCommon(); info(); MethodInterface in = null; //invokeinterface in.methodA(); } public void info() { } public void display(Father f) { f.showCommon(); } public static void main(String[] args) { Son so = new Son(); so.show(); }}interface MethodInterface { void methodA();}
关于 invokedynamic 指令
代码示例
@FunctionalInterfaceinterface Func { public 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; }); }}
动态语言和静态语言
Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查)JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
方法重写的本质
Java 语言中方法重写的本质:
IllegalAccessError介绍
回看解析阶段
虚方法表
方法返回地址(return address)
方法退出的两种方式
当一个方法开始执行后,只有两种方式可以退出这个方法,
正常退出:
异常退出:
代码举例
public class ReturnAddressTest { public boolean methodBoolean() { return false; } public byte methodByte() { return 0; } public short methodShort() { return 0; } public char methodChar() { return 'a'; } public int methodInt() { return 0; } public long methodLong() { return 0L; } public float methodFloat() { return 0.0f; } public double methodDouble() { return 0.0; } public String methodString() { return null; } public Date methodDate() { return null; } public void methodVoid() { } static { int i = 10; } public void method2() { methodVoid(); try { method1(); } catch (IOException e) { e.printStackTrace(); } } public void method1() throws IOException { FileReader fis = new FileReader("atguigu.txt"); char[] cBuffer = new char[1024]; int len; while ((len = fis.read(cBuffer)) != -1) { String str = new String(cBuffer, 0, len); System.out.println(str); } fis.close(); }}
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
举例栈溢出的情况?(StackOverflowError)
通过 -Xss 设置栈的大小
调整栈大小,就能保证不出现溢出么?
不能保证不溢出
分配的栈内存越大越好么?
不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个虚拟机的内存空间是有限的
垃圾回收是否涉及到虚拟机栈?
不会
方法中定义的局部变量是否线程安全?
何为线程安全?
具体问题具体分析:
/** * 面试题: * 方法中定义的局部变量是否线程安全?具体情况具体分析 * * 何为线程安全? * 如果只有一个线程才可以操作此数据,则必是线程安全的。 * 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。 * @author shkstart * @create 2020 下午 7:48 */public class StringBuilderTest { //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之后,将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 |
---|---|---|
程序计数器 | 否 | 否 |
虚拟机栈 | 是(SOF) | 否 |
本地方法栈 | 是 | 否 |
方法区 | 是(OOM) | 是 |
堆 | 是(OOM) | 是 |