欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM--内存结构--程序计数器&虚拟机栈

程序员文章站 2022-06-07 09:08:13
...

程序计数器

定义:

Program Counter Register 程序计数器(寄存器)

作用:

JVM--内存结构--程序计数器&虚拟机栈

  • 右边是Java源代码。需要先编译成左边的二进制字节码(JVM指令
  • 这些指令需要经过解释器,解释成机器码,最后交给CPU执行

程序计数器的作用

  • 记住下一条指令的执行的地址,最左边的数字,可以理解为“地址”
  • 现将第一条指令交给解释器解释,然后将下一条指令的地址–3,放入程序计数器
  • 当第一条指令解释完成后,解释器会去程序计数器中找到下一条指令,再重复上一条的过程

程序计数器特点

  • 是线程私有的

    1. 在多个线程执行的时候,CPU会给各个线程分配时间片,在一个时间片内,如果线程一没有执行完,则会保存他的状态,执行线程二
    2. 线程一在一个时间片执行完后,会将执行到的指令下一条地址保存在程序计数器中,该程序计数器只是属于线程一的
    3. 线程一再一次抢到时间片,则可以将程序计数器中的地址取出,继续向下运行
    4. 每个线程都有自己的程序计数器
  • 不会存在内存溢出

物理硬件上通过寄存器实现程序计数器(寄存器读取速度最快

虚拟机栈

JVM--内存结构--程序计数器&虚拟机栈

  • 栈类似于弹夹,不断的压入子弹
  • 先进后出

概念

  • 在每个线程运行时,需要给每个线程划分内存空间,虚拟机栈是线程运行需要的内存空间。
  • 每个线程都有一个虚拟机栈
  • 每个栈内存放的是栈帧
  • 每个栈帧对应着一次方法的调用,即每个方法需要的内存
  • 方法中的参数、局部变量、返回地址都需要内存
  • 当调用第一个方法是,会把栈帧1压入栈内,为其开辟内存空间
  • 当方法执行完后,会释放该方法的栈帧
  • 当方法内部存在不同方法的调用,即会该方法对应的栈帧放入虚拟机栈

如下图所示:

JVM--内存结构--程序计数器&虚拟机栈

定义

  • Java Virtual Machine Stacks (Java 虚拟机栈)
  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

代码演示

/**
 * 演示栈帧
 */
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模式启动结果

JVM--内存结构--程序计数器&虚拟机栈
活动栈帧指的是,栈最顶部的栈帧

问题辨析

  1. 垃圾回收是否涉及栈内存?

    不需要
    每个方法执行后,都会被弹出栈,自动回收掉

  2. 栈内存分配越大越好吗?

    不是
    分配的越大,因为物理内存一定,会导致线程变少
    分配的更多,只是帮助更多次的递归调用

  3. 方法内的局部变量是否线程安全?(看这个线程对变量是私有还是共享的

    如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
    如果变量变成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.*Error
 * -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:程序运行很长时间没有结果可能线程出现了死锁

  • 同样利用jstack 进程id 显示死锁信息
相关标签: 深入理解JVM