接下来我们继续深入第二个环节,也就是JVM的内存结构,很多人想到BAT等大厂去面试,但是现在互联网大厂面试几乎都会考核JVM相关知识的积累,所在在了解完了JVM的类加载机制之后,我们有必要一起来学习下JVM的内存区域划分。
其实我们通过类的加载过程也能知道,在准备阶段我们的类以及静态变量都会进行空间的分配,JVM在运行我们的代码时,是必须要使用多块内存空间的,不同空间里面存放不同的数据,然后配合我们的代码流程,完整系统的运行起来。
一.程序计数器
首先我们来看第一个内存区域:程序计数器
Program Counter Register 程序计数器(PC寄存器)
- 作用,是记住下一条jvm指令的执行地址
- 特点
- 是线程私有的
- 不会存在内存溢出
首先我们来看一段非常简单的代码:
public class Demo1 {
public static void main(String[] args) {
int num1 = 1;
System.out.println(num1);
int num2 = 2;
System.out.println(num2);
}
}
这个代码大家都能看懂,但是JVM能看懂吗?答案是:NO!
JVM是不识别我们写的代码的,我们的java代码会被编译为.class字节码文件,而字节码文件中的代码才是JVM能识别和执行的,这些代码我们也叫【字节码指令】,它对应了一条一条的机器指令,JVM通过将这些指令再解释翻译为机器指令,来操作我们的计算器进行执行。
上述的代码对应的字节码指令如下:
0 iconst_1
1 istore_1
2 getstatic #2 <java/lang/System.out>
5 iload_1
6 invokevirtual #3 <java/io/PrintStream.println>
9 iconst_2
10 istore_2
11 getstatic #2 <java/lang/System.out>
14 iload_2
15 invokevirtual #3 <java/io/PrintStream.println>
18 return
具体的指令含义后续再做讲解,我们需要知道的是【程序计数器】就是记录下一条JVM所要执行的指令地址
通过之前的加载图进行表示:
二.虚拟机栈
定义:
Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
解释:
1.每个线程运行时所需要的内存,称为虚拟机栈 —> 每个线程都有自己的Java虚拟机栈
Java代码的执行一定是由线程来执行某个方法中的代码,哪怕就是我们的main()方法也是有一个主线程来执行的,在main线程执行main()方法的代码指令的时候,就会通过main线程对应的程序计数器来记录自己执行的指令位置。
main()方法本质上是一个方法,在main()中也可以调用其他的方法,而每个方法中也有自己的局部变量数据,因此JVM提供了一块内存区域用来保存每个方法内的局部变量等数据,这个区域就是Java虚拟机栈。
2.每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
当我们在线程中调用了一个方法,就会对该方法创建一个对应的栈帧,比如我们如下的代码:
public class Demo1 {
public static void main(String[] args) {
int num1 = 1;
System.out.println(num1);
int num2 = 2;
System.out.println(num2);
}
}
这时在虚拟机栈内存中就会先创建对应main方法的栈帧,同时记录保存对应的局部变量:
而如果我们在main()方法中调用一个其他的方法:
public class Demo1 {
public static void main(String[] args) {
int num1 = 1;
System.out.println(num1);
int num2 = 2;
System.out.println(num2);
method1();
}
public static void method1(){
int num3 = 20;
System.out.println("哈哈哈哈");
}
}
对应的虚拟机栈:
并且当method1方法执行完毕后会弹出该栈队列,最后弹出main()方法栈帧,代表整个main方法代码执行完毕。这也对应了栈的特点:先进后出。
流程图小结:
栈内存相关面试案例剖析
1.垃圾回收是否涉及栈内存?
栈帧每次执行结束自动弹栈,所以不会涉及到垃圾的产生,也就不会对栈内存进行垃圾回收
2.栈内存分配越大越好吗?
并不是,假设分配的物理内存是100MB,每个线程栈大小是1MB,那么可以分配100个线程,但是如果提升了线程栈大小,那可以分配的对应线程数就变少了。
我们先来看官网给出的每个栈帧默认的大小分配:
Linux系统上默认就是1MB,当然我们可以通过-Xss进行大小的更改
3.方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
参考以下示例代码:
//方法内局部变量:线程安全
public static void method1(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}
//方法内局部变量引用对象:线程不安全
public static void method2(StringBuilder sb){
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}
//方法内局部变量引用对象提供暴露:线程不安全
public static StringBuilder method3(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
4.栈内存溢出
什么原因导致栈内存溢出(Stack Overflow)
1)栈帧过多导致内存溢出, 将抛出StackOverflowError异常。
常见的情况就是递归调用,不断产生新的栈帧,前面的栈帧不释放
我们可以通过以下代码来测试和实验:
/**
* @Description: VM Args: -Xss128k
对于不同版本的Java虚拟机和不同的操作系统, 栈容量最小值可能会有所限制, 这主要取决于操作系统内存分页大小。 譬如上述方法中的参数-Xss128k可以正常用于32位Windows系统下的JDK 6, 但是如果用于64位Windows系统下的JDK 11, 则会提示栈容量最小不能低于180K, 而在Linux下这个值则可能是228K, 如果低于这个最小限制, HotSpot虚拟器启动时会提示:The Java thread stack size specified is too small. Specify at least 228k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
打印结果:
2)栈帧过大导致内存溢出, 将抛出StackOverflowError异常。
我们这次可以尝试将每一个栈帧的局部变量给多占用一点空间,这样每个栈帧的大小就会变大,我们还是设定每个线程栈空间为128K,看看以下代码运行后,多少次就会撑满内存:
/**
* @Description: VM Args: -Xss128k
*/
public class JavaVMStackSOF2 {
private static int stackLength = 0;
public static void test() {
long unused1, unused2, unused3, unused4, unused5,
unused6, unused7, unused8, unused9, unused10,
unused11, unused12, unused13, unused14, unused15,
unused16, unused17, unused18, unused19, unused20,
unused21, unused22, unused23, unused24, unused25,
unused26, unused27, unused28, unused29, unused30,
unused31, unused32, unused33, unused34, unused35,
unused36, unused37, unused38, unused39, unused40,
unused41, unused42, unused43, unused44, unused45,
unused46, unused47, unused48, unused49, unused50,
unused51, unused52, unused53, unused54, unused55,
unused56, unused57, unused58, unused59, unused60,
unused61, unused62, unused63, unused64, unused65,
unused66, unused67, unused68, unused69, unused70,
unused71, unused72, unused73, unused74, unused75,
unused76, unused77, unused78, unused79, unused80,
unused81, unused82, unused83, unused84, unused85,
unused86, unused87, unused88, unused89, unused90,
unused91, unused92, unused93, unused94, unused95,
unused96, unused97, unused98, unused99, unused100;
stackLength++;
test();
}
public static void main(String[] args) throws Throwable {
try {
test();
}catch (Error e){
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
打印结果:
我们发现仅51次就撑爆了!
小结:
无论是由于栈帧太大还是虚拟机栈容量太小, 当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。