【详解】JVM——内存结构之程序计数器&虚拟机栈
程序计数器
定义:
Program Counter Register 程序计数器(寄存器)
作用:
- 右边是Java源代码。需要先编译成左边的二进制字节码(JVM指令)
- 这些指令需要经过解释器,解释成机器码,最后交给CPU执行
程序计数器的作用:
- <mark>记住下一条指令的执行的地址</mark>,最左边的数字,可以理解为“地址”
- 现将第一条指令交给解释器解释,然后将下一条指令的地址–3,放入程序计数器
- 当第一条指令解释完成后,解释器会去程序计数器中找到下一条指令,再重复上一条的过程
程序计数器特点:
-
是线程私有的
- 在多个线程执行的时候,CPU会给各个线程分配时间片,在一个时间片内,如果线程一没有执行完,则会保存他的状态,执行线程二
- 线程一在一个时间片执行完后,会将执行到的指令下一条地址保存在程序计数器中,该程序计数器只是属于线程一的
- 线程一再一次抢到时间片,则可以将程序计数器中的地址取出,继续向下运行
- <mark>每个线程都有自己的程序计数器</mark>
- 如果执行的是java方法,则存储着正在执行的虚拟机指令字节码对应的地址;如果执行的native方法,则为
空
-
<mark>不会存在内存溢出</mark>
物理硬件上通过寄存器实现程序计数器(寄存器读取速度最快)
虚拟机栈
- 栈类似于弹夹,不断的压入子弹
- 先进后出
概念
- 描述的是Java方法的内存模型,生命周期与线程相同,是线程私有。
- 在每个线程运行时,需要给每个线程划分内存空间,虚拟机栈是线程运行需要的内存空间。
- 每个线程都有一个虚拟机栈
- 每个栈内存放的是栈帧,<mark>每个栈帧对应着一次方法的调用,即每个方法需要的内存</mark>,栈帧存放着
局部变量表
、操作数栈
、对运行时常量池的引用
等信息 - 方法中的
参数
、局部变量
、返回地址
都需要内存 - 当调用第一个方法是,会把栈帧1压入栈内,为其开辟内存空间
- 当方法执行完后,会释放该方法的栈帧
- 当方法内部存在不同方法的调用,即会该方法对应的栈帧放入虚拟机栈
局部变量表
- 常说的
栈内存
指的就是局部变量表 - 存放着编译器可知的基本数据类型,包括(boolean、int等)
- 存放着对象的引用指针
- 所需的内存空间在
编译期
决定,在方法运行期间不会改变。
如下图所示:
定义
- Java Virtual Machine Stacks (Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存
- <mark>每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法</mark>
代码演示
/** * 演示栈帧 */
public class demo {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
debug模式启动结果
<mark>活动栈帧指的是,栈最顶部的栈帧</mark>
问题辨析
- 垃圾回收是否涉及栈内存?
不需要
每个方法执行后,都会被弹出栈,自动回收掉 - 栈内存分配越大越好吗?
不是
分配的越大,因为物理内存一定,会导致线程变少
分配的更多,只是帮助更多次的递归调用 - 方法内的局部变量是否线程安全?(看这个线程对变量是私有还是共享的)
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
如果变量变成static类型,需要考虑线程安全
方法内的局部变量代码
/** * 局部变量的线程安全问题 */
public class Demo1_18 {
// 多个线程同时执行此方法
static void m1() {
int x = 0;
for (int i = 0; i < 5000; i++) {
x++;
}
System.out.println(x);
}
}
线程安全参考实例代码
- 第一个m1方法属于线程安全,
- 第二个m2不属于线程安全,因为其方法的参数被main方法同时调用
- 第三个m3不属于线程安全,因为其内部对象,被当做返回值返回,即可以被其他方法修改
/** * 局部变量的线程安全问题 */
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
案例一:栈帧过大导致内存溢出
/** * 演示栈内存溢出 java.lang.StackOverflowError * -Xss256k */
public class Demo1_2 {
private static int count;
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
案例二:栈帧过多导致内存溢出
/** * json 数据转换 */
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
//修改一个可以终止递归调用
//@JsonIgnore
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
线程运行诊断–使用linux命令定位:
案例1:cpu占用过多
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号(需要将10进制线程编号转换为16进制)
案例2:程序运行很长时间没有结果(<mark>可能线程出现了死锁</mark>)
- 同样利用jstack 进程id 显示死锁信息