JVM内存结构:程序计数器、虚拟机栈、本地方法栈

目录一、JVM 入门介绍JVM 定义JVM 优势JVM JRE JDK的比较学习步骤二、内存结构整体架构1、程序计数器(寄存器)1.1 作用1.2 特点2、虚拟机栈2.1 定义2.2 演示2.3 面试问题辨析2.4 内存溢出2.5 线程运行诊断3、本地方法栈4、总结

一、JVM 入门介绍

JVM 定义

Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)

JVM 优势 一次编写,到处运行 自动内存管理,垃圾回收机制 数组下标越界检查 常见的JVM

注:我们笔记所使用的的是HotSpot 版本

JVM JRE JDK的比较

JVM JRE JDK的区别:

学习步骤

学习顺序如下图:(由简到难)

二、内存结构

整体架构

1、程序计数器(寄存器)

Program Counter Register

1.1 作用

程序计数器用于保存JVM中下一条所要执行的指令的地址

0:getstatic #20  // PrintStream out = System.out;1:astore_1 // --2:aload_1 // out.println(1);3:iconst_1 // --4:invokevirtual #26  // --5:aload_1     // out.println(2);6:iconst_2 // --7:invokevirtual #26  // --8:aload_1     // out.println(3);9:iconst_3     // --10:invokevirtual #26  // --11:aload_1 // out.println(4);12:iconst_4 // --13:invokevirtual #26  // --14:aload_1     // out.println(5);15:iconst_5 // --16:invokevirtual #26  // --return

Java指令执行流程:

每一条二进制字节码(JVM指令) 通过 解释器 转换成 机器码 然后 就可以被 CPU 执行了! 当 解释器 将一条jvm 指令转换成 机器码后 其会 向程序计数器 递交 下一条 jvm 指令的执行地址! 程序计数器在硬件层面 其实是通过 寄存器 实现的! 所以程序计数器的作用就是:用于保存JVM中下一条所要执行的指令的地址!

1.2 特点 线程私有 CPU会为每个线程分配时间片,当当 前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码 程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令 不会存在内存溢出

2、虚拟机栈

Java Virtual Machine Stacks

2.1 定义 每个线程运行需要的内存空间,这一空间被称为虚拟机栈(Frames) 每个栈由多个栈帧(Frame) 组成,对应着每个方法运行时所占用的内存 每个线程只能有一个活动栈帧,对应着当前正在执行的方法,当方法执行时压入栈,方法执行完毕后 弹出栈

2.2 演示

代码

/** * @Auther: csp1999 * @Date: 2020/11/10/11:36 * @Description: 演示栈帧 */public class Demo01 {    public static void main(String[] args) {        methodA();    }    private static void methodA() {        methodB(1, 2);    }    private static int methodB(int a, int b) {        int c = a + b;        return c;    }}

我们打断点来Debug 一下看一下方法执行的流程:

接这往下走,使方法B执行完毕:

然后方法A执行完毕,其对应的栈帧出栈,main方法对应的栈帧为活动栈帧;最后main执行完毕 栈帧出栈,虚拟机栈为空,代码运行结束!

2.3 面试问题辨析 1.垃圾回收是否涉及栈内存? 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。 2.栈内存的分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。 举例:如果物理内存是500M(假设),如果一个线程所能分配的栈内存为2M的话,那么可以有250个线程。而如果一个线程分配栈内存占5M的话,那么最多只能有100 个线程同时执行!

3.方法内的局部变量是否是线程安全的?

从图中得出:局部变量如果是静态的可以被多个线程共享,那么就存在线程安全问题。如果是非静态的只存在于某个方法作用范围内,被线程私有,那么就是线程安全的!

看一个案例:

/** * 局部变量的线程安全问题 */public class Demo02 {    public static void main(String[] args) {// main 函数主线程        StringBuilder sb = new StringBuilder();        sb.append(4);        sb.append(5);        sb.append(6);        new Thread(() -> {// Thread新创建的线程            m2(sb);        }).start();    }    public static void m1() {        // sb 作为方法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 作为方法m2()外部的传递来的参数,sb 不在方法m2()的作用范围内        // 不是线程私有的 ---> 非线程安全        sb.append(1);        sb.append(2);        sb.append(3);        System.out.println(sb.toString());    }    public static StringBuilder m3() {        // sb 作为方法m3()内部的局部变量,是线程私有的        StringBuilder sb = new StringBuilder();// sb 为引用类型的变量        sb.append(1);        sb.append(2);        sb.append(3);        return sb;// 然而方法m3()将sb返回,sb逃离了方法m3()的作用范围,且sb是引用类型的变量        // 其他线程也可以拿到该变量的 ---> 非线程安全        // 如果sb是非引用类型,即基本类型(int/char/float...)变量的话,逃离m3()作用范围后,则不会存在线程安全    }}

该面试题答案:

如果方法内局部变量没有逃离方法的作用范围,则是线程安全的

如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

2.4 内存溢出

Java.lang.stackOverflowError 栈内存溢出

发生原因

1.虚拟机栈中,栈帧过多(无限递归),这种情况比较常见! 2.每个栈帧所占用内存过大(某个/某几个栈帧内存直接超过虚拟机栈最大内存),这种情况比较少见!

举2个案例:

案例1:

/** * 演示栈内存溢出 java.lang.StackOverflowError * -Xss256k 可以通过栈内存参数 设置栈内存大小 */public class Demo03 {    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();// 方法无限递归,不断产生栈帧 到虚拟机栈    }}最后输出结果:java.lang.StackOverflowErrorat com.haust.jvm_study.demo.Demo03.method1(Demo03.java:21)     ...     ...39317// 栈帧个数,不同的虚拟机大小能存放的栈帧数量不一样

我们可以通过修改参数来指定虚拟机栈内存大小

当我们将虚拟机栈内存缩小到指定的256k的时候再运行Demo03后,会得到其栈内最大栈帧数为:3816 远小于原来的39317!

案例2:

/** * 两个类之间的循环引用问题,导致的栈溢出 *  * 解决方案:打断循环,即在员工emp 中忽略其dept属性,放置递归互相调用 */public class Demo04 {    public static void main(String[] args) throws JsonProcessingException {        Dept d = new Dept();        d.setName("Market");        Emp e1 = new Emp();        e1.setName("csp");        e1.setDept(d);        Emp e2 = new Emp();        e2.setName("hzw");        e2.setDept(d);        d.setEmps(Arrays.asList(e1, e2));        // 输出结果:{"name":"Market","emps":[{"name":"csp"},{"name":"hzw"}]}        ObjectMapper mapper = new ObjectMapper();// 要导入jackson包        System.out.println(mapper.writeValueAsString(d));    }}/** * 员工 */class Emp {    private String name;    @JsonIgnore// 忽略该属性:为啥呢?我们来分析一下!    /**     * 如果我们不忽略掉员工对象中的部门属性     * System.out.println(mapper.writeValueAsString(d));     * 会出现下面的结果:     * {     *  "name":"Market","emps":     *  [c     *      {"name":"csp",dept:{name:'xxx',emps:'...'}},     *      ...     *  ]     * }     * 也就是说,输出结果中,部门对象dept的json串中包含员工对象emp,     * 而员工对象emp 中又包含dept,这样互相包含就无线递归下去,json串越来越长...     * 直到栈溢出!     */    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;    }}

2.5 线程运行诊断

案例1:CPU占用过高

Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程 top命令,查看是哪个进程占用CPU过高

ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看具体是哪个线程占用CPU过高!

jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换 可以通过线程id,找到有问题的线程,进一步定位到问题代码的源码行数!

我们可以看到上图中的thread1 线程一直在运行(runnable)中,说明就是它占用了较高的CPU内存;

3、本地方法栈

一些带有native 关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法!

如图:

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须由调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies 目前该方法的使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍 本地方法栈(Native Method Stack):(它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库) native方法的举例: Object类中的clone wait notify hashCode 等 Unsafe类都是native方法

4、总结

这篇文章的内容就到这了,希望大家多多关注的其他内容!

青春气贯长虹,勇敢盖过怯懦,进取压倒苟安。

JVM内存结构:程序计数器、虚拟机栈、本地方法栈

相关文章:

你感兴趣的文章:

标签云: