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

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

JVM 基本上是每家招聘公司都会问到的问题,它们会这么无聊问这些不切实际的问题吗?很显然不是。由 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

图片.png

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

JVM JRE JDK的比较

JVM JRE JDK的区别:

图片.png

学习步骤

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

图片.png

二、内存结构

整体架构

图片.png

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

Program Counter Register

图片.png

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

图片.png

Java指令执行流程:

1.每一条二进制字节码(JVM指令) 通过 解释器 转换成 机器码 然后 就可以被 CPU 执行了!

2.当 解释器 将一条jvm 指令转换成 机器码后 其会 向程序计数器 递交 下一条 jvm 指令的执行地址!

3.程序计数器在硬件层面 其实是通过 寄存器 实现的!

4.所以程序计数器的作用就是:用于保存JVM中下一条所要执行的指令的地址!

1.2 特点

# 线程私有

CPU会为每个线程分配时间片,当当 前线程的时间片使用完以后,CPU就

会去执行另一个线程中的代码

程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来

执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令

# 不会存在内存溢出

图片.png

2、虚拟机栈

Java Virtual Machine Stacks

图片.png

2.1 定义

每个线程运行需要的内存空间,这一空间被称为虚拟机栈(Frames)

每个栈由多个栈帧(Frame) 组成,对应着每个方法运行时所占用的内存

每个线程只能有一个活动栈帧,对应着当前正在执行的方法,当方法执行

时压入栈,方法执行完毕后 弹出栈

2.2 演示

图片.png

代码

/**
 * @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 一下看一下方法执行的流程:

图片.png

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

图片.png

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

2.3 面试问题辨析

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

    不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完

    毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去

    回收内存。

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

图片.png

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

举例:如果物理内存是500M(假设),如果一个线程所能分配的栈内存为2M的话,那么可以有250个线程。而如果一个线程分配栈内存占5M的话,那么最多只能有100 个线程同时执行!

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

图片.png

图片.png

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

看一个案例:

/**
 * 局部变量的线程安全问题
 */
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 栈内存溢出

发生原因

图片.png

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// 栈帧个数,不同的虚拟机大小能存放的栈帧数量不一样

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

图片.png

当我们将虚拟机栈内存缩小到指定的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过高

图片.png

图片.png

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

图片.png

jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换

可以通过线程id,找到有问题的线程,进一步定位到问题代码的源码行数!

图片.png

图片.png

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

图片.png

3、本地方法栈

图片.png

图片.png

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

如图:

图片.png

图片.png

本地接口的作用是融合不同的编程语言为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、总结

这篇文章的内容就到这了,希望大家多多关注"java开发全栈"的其他内容!

相关文章
|
1月前
|
存储 Java 数据安全/隐私保护
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
【JVM】Java虚拟机栈(Java Virtual Machine Stacks)
36 0
|
26天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
73 0
|
7天前
|
缓存 监控 Java
深入理解Java虚拟机(JVM)性能调优
【4月更文挑战第18天】本文探讨了Java虚拟机(JVM)的性能调优,包括使用`jstat`、`jmap`等工具监控CPU、内存和GC活动,选择适合的垃圾回收器(如Serial、Parallel、CMS、G1),调整堆大小和新生代/老年代比例,以及代码优化和JIT编译策略。通过这些方法,开发者能有效提升应用性能并应对复杂性挑战。性能调优是持续过程,需伴随应用演进和环境变化进行监控与优化。
|
21天前
|
存储 缓存 监控
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
|
1月前
|
存储 安全 编译器
C++智能指针:更简单、更高效的内存管理方法
C++智能指针:更简单、更高效的内存管理方法
12 0
|
1月前
|
人工智能 自然语言处理 物联网
极大降低大模型训练内存需求,Meta等推出高效方法
【2月更文挑战第27天】极大降低大模型训练内存需求,Meta等推出高效方法
33 2
极大降低大模型训练内存需求,Meta等推出高效方法
|
1月前
|
存储 JSON 监控
Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
【2月更文挑战第30天】Higress Controller**不是将配置信息推送到Istio的内存存储里面的**。
14 1
|
1月前
|
存储 C语言
C语言--------数据在内存中的存储
C语言--------数据在内存中的存储
26 0
|
5天前
|
存储 NoSQL Oracle
Oracle 12c的内存列存储:数据的“闪电侠”
【4月更文挑战第19天】Oracle 12c的内存列存储以超高速度革新数据处理,结合列存储与内存技术,实现快速查询与压缩。它支持向量化查询和并行处理,提升效率,但需合理配置以平衡系统资源。作为数据管理员,应善用此功能,适应业务需求和技术发展。
|
16天前
|
存储 C语言
数据在内存中的存储2
数据在内存中的存储2