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开发全栈"的其他内容!

相关文章
|
10月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
871 55
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
379 27
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
8月前
|
存储 Java 编译器
深入理解Java虚拟机--类文件结构
本内容介绍了Java虚拟机与Class文件的关系及其内部结构。Class文件是一种与语言无关的二进制格式,包含JVM指令集、符号表等信息。无论使用何种语言,只要能生成符合规范的Class文件,即可在JVM上运行。文章详细解析了Class文件的组成,包括魔数、版本号、常量池、访问标志、类索引、字段表、方法表和属性表等,并说明其在Java编译与运行过程中的作用。
226 0
|
12月前
|
监控 测试技术 数据库
详解Hyper-V虚拟机CPU分配方法
在Hyper-V环境中,合理分配虚拟机的CPU资源至关重要。vCPU是物理CPU的虚拟化表示,管理员可通过指定处理器数量、核心数、设置兼容性和亲和性、启用动态分配等方法优化性能。使用性能监视工具监控并调整CPU资源,避免过度分配,确保虚拟机稳定运行。定期评估和优化资源分配策略,以适应业务变化,保持最佳性能。
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
362 28
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
185 3
|
前端开发 Java 应用服务中间件
【Java虚拟机】JVM类加载机制和双亲委派模型
【Java虚拟机】JVM类加载机制和双亲委派模型
【Java虚拟机】JVM类加载机制和双亲委派模型