您的位置:首页 > 教程文章 > 编程开发

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

:0 :2021-09-28 20:54:24

目录
一、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.StackOverflowError
 at 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、总结
这篇文章的内容就到这了,希望大家多多关注无名的其他内容!

在Jpa框架下拼接原生sql 并执行的操作
教你怎么用Springboot自定义Banner图案

同类资源