程序计数器

定义:

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

作用:

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

程序计数器的作用

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

程序计数器特点

  • 是线程私有的

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

问题辨析

  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.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 显示死锁信息