地址空间划分
我们知道JVM是内存中的虚拟机,主要使用内存进行存储,所有类、类型、方法,都是在内存中,这决定着我们的程序运行是否健壮、高效。
在Java虚拟机栈中,一个栈帧对应一个方法,,方法执行时会在虚拟机栈中创建一个栈帧,而且当前虚拟机栈只能有一个活跃的栈帧,并且处于栈顶,当前方法结束后,可能会将返回值返回给调用它的方法,而自己将会被弹出栈(即销毁),下一个栈顶将会被执行。
举例说明:
ByteCodeSample.java
package com.mtli.jvm.model;
/**
* @Description:测试JVM内存模型
* @Author: Mt.Li
* @Create: 2020-04-26 17:47
*/
public class ByteCodeSample {
public static int add(int a , int b) {
int c= 0;
c = a + b;
return c;
}
}
对其进行编译生成.class文件
javac com/mtli/jvm/model/ByteCodeSample.java
然后用javap -verbose 进行反编译
javap -verbose com/mtli/jvm/model/ByteCodeSample.class
生成如下:
Classfile /E:/JavaTest/javabasic/java_basic/src/com/mtli/jvm/model/
ByteCodeSample.class
Last modified 2020-4-26; size 289 bytes
MD5 checksum 2421660bb241239f1a67171bb771521f
Compiled from "ByteCodeSample.java"
public class com.mtli.jvm.model.ByteCodeSample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 描述类信息
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."
t>":()V
#2 = Class #13 // com/mtli/jvm/model/Byt
eCodeSample
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 add
#9 = Utf8 (II)I
#10 = Utf8 SourceFile
#11 = Utf8 ByteCodeSample.java
#12 = NameAndType #4:#5 // "":()V
#13 = Utf8 com/mtli/jvm/model/ByteCodeSample
#14 = Utf8 java/lang/Object
// 以上是常量池(线程共享)
{
public com.mtli.jvm.model.ByteCodeSample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/O
bject."" :()V
4: return
LineNumberTable:
line 8: 0
// 以上是初始化过程
public static int add(int, int);
descriptor: (II)I // 接收两个int类型变量
flags: ACC_PUBLIC, ACC_STATIC // 描述方法权限和类型
Code:
stack=2, locals=3, args_size=2 // 操作数栈深度 、 容量 、参数数量
0: iconst_0
1: istore_2
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: iload_2
7: ireturn
LineNumberTable:
line 10: 0 // 这里的第0行对应我们代码中的第10行
line 12: 2
line 13: 6
}
SourceFile: "ByteCodeSample.java"
执行add(1,2)
以下是程序在JVM虚拟机栈中的执行过程
图不是很清楚,我来说一下过程,最下边的是程序计数器(前边提到的),最上边是操作指令,中间是局部变量表和操作数栈(位置从0开始)
执行完之后,当前线程虚拟机栈的栈帧会弹出,对应的其他方法与当前栈帧的连接释放、引用释放,它的下一个栈帧成为栈顶。
我们知道,一个栈帧对应一个方法,存放栈帧的线程虚拟栈是有深度限制的,我们调用递归方法,每递归一次,就会创建一个新的栈帧压入虚拟栈,当超出限度后,就会报此错误。
举例说明:
package com.mtli.jvm.model;
/**
* @Description:斐波那契
* F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
* F(2) = F(1) + F(0) = 1,F(3) = F(2) + F(1) = 1+1 = 2
* 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
* @Author: Mt.Li
* @Create: 2020-04-26 18:33
*/
public class Fibonacci {
public static int fibonacci(int n) {
if(n>=0){
if(n == 0) {return 0;}
if(n == 1) {return 1;}
return fibonacci(n-1) +fibonacci(n-2);
}
return n;
}
public static void main(String[] args) {
System.out.println(fibonacci(0));
System.out.println(fibonacci(1));
System.out.println(fibonacci(2));
System.out.println(fibonacci(3));
System.out.println(fibonacci(1000000));
// java.lang.StackOverflowError
}
}
结果:
解决方法是限制递归次数,或者直接用循环解决。
还有就是,由JVM管理的虚拟机栈数量也是有限的,也就是线程数量也是有限定。
由于栈帧在方法返回后会自动释放,所有栈是不需要GC来回收的。
元空间(MetaSpace)在jdk1.7之前是属于永久代(PermGen)的,两者的作用就是记录class的信息,jdk1.7中,永久代被移入堆中解决了前面版本的永久代分配内存不足时报出的OutOfMemoryError,jdk1.8之后元空间替代了永久代。
说到这里我们不得不提一下String.intern()方法在jdk版本变更中的不同
String s = new String("a");
s.intern();
JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用
我们看一个例子:
jdk1.8
public class InternDifference {
public static void main(String[] args) {
String s = new String("a"); // 会先根据字面量在池中创建一个"a"字符串,
// 然后new的时候,在堆中创建一个"a",s——>堆中"a"
s.intern(); // intern的时候,因为该对象已经存在于堆中,于是,想要将堆中对此对象的引用添加到池中
// 但是池中此时已经存在"a",且不是堆中这个对象的引用,是独立存在的,intern无法创建引用进去
String s2 = "a"; // 池中有"a",便直接引用池中的"a",s2——>池中"a"
System.out.println(s == s2); // 两者的指向不同,故结果为false
String s3 = new String("a") + new String("a"); // 两个括号中的字面量原本都是要在池中创建"a"的,但是上面的
// 例子中,已经在池中创建了"a",故这里只会在堆中创建aa对象,s3指向堆中的对象
s3.intern(); // intern时,发现池中并没有aa,于是将堆中对此对象的引用添加到字符串常量池中,然后池中就会有堆中"aa"对象的引用
String s4 = "aa"; // 这里字面量创建的时候,发现池中有aa,则s4直接指向池中的"aa"
System.out.println(s3 == s4); // 但是池中的"aa"实际上是对堆中对象的引用,所以两者实际上都是指向堆中对象,结果为true
}
}
// 结果
false
true
jdk1.6:
public class InternDifference {
public static void main(String[] args) {
String s = new String("a"); // 和上边一样,这里用引号声明了"a"字符串,会在常量池中先创建"a"
// 然后new String()会在堆中创建对象
s.intern(); // intern原本是想要将"a"直接复制一份到常量池中,但是池中已经有了,池中的也是独立存在的,故不能放副本
String s2 = "a"; // s2字面量创建的时候发现池中有"a",故直接引用常量池中的"a"
System.out.println(s == s2); // 根据上边的分析,结果肯定为false
String s3 = new String("a") + new String("a"); // 和之前一样,先是想要创建声明的"a",但是池中已经有了上边的"a"
// 故不会创建,只能直接new String创建生成"aa",s3指向堆中对象
s3.intern(); // intern时发现池中没有"aa",故会放一个副本到常量池中(注意不是对堆中对象的引用)
String s4 = "aa"; // 这里创建的时候发现池中已经有"aa",于是直接引用池中"aa"
System.out.println(s3 == s4); // s3引用堆中的,s4引用池中的,故为false
}
}
// 结果
false
false
如有不对,请大家下方评论区指正,谢谢