jvm中的程序计数器、虚拟机栈和本地方法栈
引言
本文主要介绍一下jvm
虚拟机中的程序计数器、虚拟机栈和本地方法栈。
程序计数器
作用
作用:记录下一条jvm指令的执行地址。
下面具体描述一下程序计数器的作用。
这里有两个代码,右边的为源代码,左边为编译之后的字节码。
当我们直接写完源代码之后这个代码是不能直接交给CPU运行的,需要转化为对应的机器码才能让CPU
运行。
具体的步骤:
源码 —> 字节码 —> 解释器 —> 机器码 —> CPU 运行。
在这整个过程中,程序计数器的作用就体现在字节码被解释器转化为机器码的过程中,在这个过程中,解释器每解释一条指令之后就会到程序计数器中取得下一次条指令的地址,然后程序计数器再指向下一条指令。
除此之外程序计数器在多个线程运行中也起到关键的作用,接下来就顺便介绍一下程序计数器的特点,并探究一下在多线程中起到了什么作用。
特点
特点:
● 是线程私有的
● 不会存在内存溢出(唯一一个不会内存溢出)
这里具体介绍一下线程私有和程序计数器作用之间的关系,我们都知道CPU
会给每个线程都分配时间片,当时间片用完之后线程就会停止运行被挂起了,所以在这个时候也需要记录一下接下来程序需要执行指令的地址,这正是程序计数器的作用。所以想要正确的保证时间片被用完之后还能记录程序运行的位置,为之后重新获得时间片继续执行程序,程序计数器是必不可少的。
同时我们也能发现为什么是线程私有的,因为每个线程都有自己所要执行的程序,并且需要分别记录自己程序将要执行位置,所以程序计数器是线程私有的,这样多个线程运行才不会乱套。
虚拟机栈
总结来说,虚拟机栈其实就是线程运行所需要的内存空间。
虚拟机栈里存放的内容称为栈帧,而所谓的栈帧其实就是每个方法运行所需要的内存。里面包括方法的参数、局部变量、返回地址。所以每个方法执行时就需要提前将这些内存给分配好,然后每执行一个方法就会存放一个相对应的栈帧。
但是并不是虚拟机栈中只能存放一个栈帧,如果存在方法的嵌套的话,就会放入多个栈帧,并且是按照栈的数据结构进行保存的,当方法执行完之后再从栈中弹出。
虚拟机栈只能有一个活动栈帧,指的就是当前正在执行的方法。
栈的演示
这里就在写一个嵌套方法来演示栈。
public class Demo1 {
public static void main(String[] args) {
method1();
}
public static void method1(){
method2(1, 2);
}
public static int method2(int a, int b){
int c = a + b;
return c;
}
}
我们在main
方法上打断点 debug 启动。
这里左边就是虚拟机栈里面存放着栈帧,右边就是这个栈帧的内容,这里面只有参数所以就显示了参数。当前的活动栈帧就是main
方法。
当我们走到method1
这时活动栈帧就是method1
方法,并且由于没有任何参数、局部变量和返回值所以就没有右边的栈帧内容。
接下来我们走到method2
这时我们的活动栈帧就是method2
方法,并且也有栈帧内容。
当活动栈帧对应的方法走完之后,就会弹出这个栈帧。这里我们在点下一步就会把method2
弹出,并且方法返回到method1
。
后续也是相同的。
问题辨析
垃圾回收是否涉及栈内容?
这里其实并不会,因为栈帧内存其实在每一次方法之后就会自己弹出栈,然后就把内存释放掉了,所以不需要专门垃圾回收进行内存回收。
栈内存分配越大越好吗?
其实栈内容也并不是越大越好的,因为栈内存指的是当前线程运行所需要内容,而我们虚拟机分配的内存并不是无限大的,所以给栈分配的内存也不是无限的。而如果当我们栈的内存过大的话,就会导致我们能够创建的线程数就减少了,也可能会影响性能,所以并不是越打越好。
这里顺便介绍一个jvm的指令参数来设置栈内存大小。
-Xss size // 后面跟一个大小
例如:
-Xss 1m
-Xss 1024k
-Xss 1048576
这个参数可以在idea中进行设置。
方法内的局部变量是否线程安全?
这里我们来观察一下代码
static void m1(){
int x = 0;
for (int i = 0; i < 5000; i++){
x++;
}
System.out.println(x);
}
这个方法内的x
其实是线程安全的,因为当我们有多个线程去调用这个方法时,那么就会创建多个对应m1
方法的栈帧,然后在执行这个方法的时候每个线程都会创建自己单独的x
局部变量,所以这个是安全的。
但是如果这个x
是static
修饰的话,就不是线程安全的,因为这个变量就会被多个线程同时访问到,就会造成线程安全的问题。
接下来我们来看一下下面三个方法是不是线程安全的。
public void method1(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public void method2(StringBuilder sb){
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public StringBuilder method3(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
首先看第一个,这个StringBuilder
方法是一个局部变量,类似于上面的变量x所以这个是一个线程安全的,每个线程都会创建自己的StringBuilder
。
再看第二个,这个sb
变量是通过参数传过来,这个就会有线程问题,可能调用者通过多线程来调用的这个方法,然后再主线程中同时对这个sb进行了操作,这样就会有问题。这时候就应该使用StringBuffer
。
类似于以下这种调用:
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
new Thread(() -> {
method2(sb);
});
第三个同样有问题,当创建完之后的对象给返回出去了,那么别的线程拿到这个数据同样可能会进行多线程的操作造成了线程问题。
所以总结来说:
● 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
● 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出:
造成内存溢出的原因具体有两种:
● 无限递归的方法调用没有返回,导致栈帧太多因此内存溢出
● 栈帧内存直接大于栈内存大小,导致内存溢出
由于第二种并不好实现,所以这里模拟一下第一种的情况。
具体代码:
public static void main(String[] args) {
method();
}
public static void method(){
method();
}
这时就会报一个StackOverflowError
的错误就是栈内存溢出了。
线程诊断
cpu 占用高
这里写一个while(true)
代码,然后再linux
上进行运行。
接下来进行排查过程:
-
使用
top
命令查看资源占有情况
定位到 32655 进程资源占有高 -
查找相对应的线程
这里使用ps
命令可以查看线程的对cpu的使用情况。
ps H -eo pid,tid,%cpu
H
:表示把当前进程下的所有线程都打印出来
-eo
:表示后面添加的参数表示最后要展示哪些列,比如pid
、tid
、%cpu
还可以使用grep
进行筛选
ps H -eo pid,tid,%cpu | grep 32655
可以查看到 32655 进程对应的 32665 的线程占有率很高。
- 查看所有线程
使用 jdk 提供的工具 jstack 工具可以看查看当前进程中所有的线程。
jstack 32655
- 查找具体的线程
查找通过ps
命令找到的有问题的线程id(注意:这里需要将十六进制转化为十进制)
32665 ==> 7f99
这里状态还是运行中,并且显示了有问题的代码行数。
程序运行很长时间没有结果
当我们启动完一个java程序之后没有给到相对应的返回结果时,命令行会返回一个当前启动的进程号。
然后通过这个进程号使用jstack
命令来直接查询当前所有的线程
然后找到这个工具最后输出的内容:
如果有死锁导致没有输出结果的话,这里就会提示有死锁,并且提示有问题的代码行号。
总结:
定位:
● 使用 top
命令查看哪个进程占用的cpu
高
● 使用ps H -eo pid,tid,%cpu | grep 进程id
查看哪个线程占用cpu
高
● jstack 进程id
命令查看当前进程下的所有线程
○ 进一步查找有问题的线程,并定位到有问题的源码行数
本地方法栈
本地方法栈的作用就是给本地方法提供的一个内存空间,因为Java有些源码并不是用java写的,而是用c和c++写的本地方法(这是因为Java代码有一定的限制,不能直接跟操作系统底层打交道),所以为了能够调用这些本地方法,就专门有一个本地方法栈,专门用来调用这些方法供使用的。
具体有哪些本地方法,以Object
类举例子:
像这种以native
修饰的都是本地方法