java内存模型详细解析 (上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: java内存模型详细解析

一. java结构体系


Description of Java Conceptual Diagram(java结构)

1187916-20200702051701692-2106274658.png

我们经常说到JVM调优,JVM和JDK到底什么关系,大家知道么?这是java基础。

这幅图很重要,一定要了解其结构。这是jdk的结构图。从结构上可以看出java结构体系, JDK主要包含两部分:


第一部分:是java 工具(Tools&Tool APIs)


比如java, javac, javap等命令. 我们常用的命令都在这里


第二部分: JRE(全称:Java Runtime Enveriment), jre是Java的核心,。


jre里面定义了java运行时需要的核心类库, 比如:我们常用的lang包, util包, Math包, Collection包等等.这里还有一个很重要的部分JVM(最后一部分青色的) java 虚拟机, 这部分也是属于jre, 是java运行时环境的一部分. 下面来详细看看:


  • 最底层的是Java Virtual Machine: java虚拟机
  • 常用的基础类库:lang and util。在这里定义了我们常用的Math、Collections、Regular Expressions(正则表达式),Logging日志,Reflection反射等等。
  • 其他的扩展类库:Beans,Security,Serialization序列化,Networking网络,JNI,Date and Time,Input/Output等。
  • 集成一体化类库:JDBC数据库连接,jndi,scripting等。
  • 用户接口工具:User Interface Toolkits。
  • 部署工具:Deployment等。


从上面就可看出,jvm是整个jdk的最底层。jvm是jdk的一部分。


二. java语言的跨平台特性



1. java语言是如何实现跨平台的?

1187916-20200702052610244-118474122.png


跨平台指的是, 程序员开发出的一套代码, 在windows平台上能运行, 在linux上也能运行, 在mac上也能运行. 我们知道, 机器最终运行的指令都是二进制指令. 同样的代码, 在windows上生成的二进制指令可能是0101, 但是在linux上是1001, 而在mac上是1011。这样同样的代码, 如果要想在不同的平台运行, 放到相应的平台, 就要修改代码, 而java却不用, 那么java这种跨平台特性是怎么做到的呢?


原因在于jdk, 我们最终是将程序编译成二进制码,把他丢在jvm上运行的, 而jvm是jre的一部分. 我们在不同的平台下载的jdk是不同的. windows平台要选择下载适用于windows的jdk, linux要选择适用于linux的jdk, mac要选择适用于mac的jdk. 不同平台的jvm针对该平台有一个特定的实现, 正是这种特点的实现, 让java实现了跨平台。


2. 延伸思考


通过上面的分析,我们知道能够实现跨平台是因为jvm封装了变化。我们经常说进行jvm调优,那么在不同平台的调优参数可以通用么?显然是不可以的。不同平台的jvm尤其个性化差异。


封装变化的部分是JDK中的jvm,JVM的整体结构是怎样的呢?来看下面一个部分。


三. JVM整体结构和内存模型



1.JVM由三部分组成:


  • 类装载子系统
  • 运行时数据区(内存模型)
  • 字节码执行引擎

1187916-20200702055039170-323159396.png



其中类装载子系统是C++实现的, 他把类加载进来, 放入到虚拟机中. 这一块就是之前分析过的类加载器加载类,采用双亲委派机制,把类加载进来放入到jvm虚拟机中。

然后, 字节码执行引擎去虚拟机中读取数据. 字节码执行引擎也是c++实现的. 我们重点研究运行时数据区。


2.运行时数据区的构成


运行时数据区主要由5个部分构成: 堆,栈,本地方法栈,方法区,程序计数器


3.JVM三部分密切配合工作


下面我们来看看一个程序运行的时候, 类装载子系统, 运行时数据区, 字节码执行引擎是如何密切配合工作的?

我们举个例子来说一下:

package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

当我们在执行main方法的时候, 都做了什么事情呢?


第一步: 类加载子系统加载Math.class类, 然后将其丢到内存区域, 这个就是前面博客研究的部分,类加载的过程, 我们看源码也发现,里面好多代码都是native本地的, 是c++实现的

第二步: 在内存中处理字节码文件, 这一部分内容较多, 也是我们研究的重点, 后面会对每一个部分详细说


第三步: 由字节码执行引擎执行java虚拟机中的内存代码, 而字节码执行引擎也是由c++实现的


这里最核心的部分是第二部分运行时数据区(内存模型), 我们后面的调优, 都是针对这个区域来进行的.


下面详细来说内存区域


1187916-20200702062131730-606421934.png


这是java的内存区域, 内存区域干什么呢?内存区域其实就是放数据的,各种各样的数据j放在不同的内存区域

四. 栈


栈是用来存放变量的

4.1. 栈空间


还是用Math的例子来说,当程序运行的时候, 会创建一个线程, 创建线程的时候, 就会在大块的栈空间中分配一块小空间, 用来存放当前要运行的线程的变量


public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }

比如,这段代码要运行,首先会在大块的栈空间中给他分配一块小空间. 这里的math这个局部变量就会被保存在分配的小空间里面.


在这里面我们运行了math.compute()方法, 我们看看compute方法内部实现


public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }


这里面有a, b, c这样的局部变量, 这些局部变量放在那里呢? 也放在上面分配的栈小空间里面.

1187916-20200702063356710-1824256156.png

效果如上图, 在栈空间中, 分配一块小的区域, 用来存放Math类中的局部变量


如果再有一个线程呢? 我们就会再次在栈空间中分配一块小的空间, 用来存放新的线程内部的变量


1187916-20200702063600461-773695288.png


同样是变量, main方法中的变量和compute()方法中的变量放在一起么?他们是怎么放得呢?这就涉及到栈帧的概念。


4.2. 栈帧


1.什么是栈帧呢?


package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

还是这段代码, 我们来看一下, 当我们启动一个线程运行main方法的时候, 一个新的线程启动,会现在栈空间中分配一块小的栈空间。然后在栈空间中分配一块区域给main方法,这块区域就叫做栈帧空间.


当程序运行到compute()计算方法的时候, 会要去调用compute()方法, 这时候会再分配一个栈帧空间, 给compute()方法使用.


2.为什么要将一个线程中的不同方法放在不同的栈帧空间里面呢?


一方面: 我们不同方法里的局部变量是不能相互访问的. 比如compute的a,b,c在main里不能被访问到。使用栈帧做了很好的隔离作用。


另一方面: 方便垃圾回收, 一个方法用完了, 值也返回了, 那他里面的变量就是垃圾了, 后面直接回收这个栈帧就好了.

1187916-20200702064551284-1741652326.png


如下图, 在Math中两个方法, 当运行到main方法的时候, 会将main方法放到一块栈帧空间, 这里面仅仅是保存main方法中的局部变量, 当执行到compute方法的时候, 这时会开辟一块compute栈帧空间, 这部分空间仅存放compute()方法的局部变量.


不同的方法开辟出不同的内存空间, 这样方便我们各个方法的局部变量进行管理, 同时也方便垃圾回收.


3.java内存模型中的栈算法


我们学过栈算法, 栈算法是先进后出的. 那么我们的内存模型里的栈和算法里的栈一样么?有关联么?


我们java内存模型中的栈使用的就是栈算法, 先进后出.举个例子, 还是这段代码

package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
    public int add() {
        int a = 1;
        int b = 2;
        int c = a + b;
        return c;
    }
    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
        math.add();   // 注意这里调用了两次compute()方法
    }
}


这时候加载的内存模型是什么样呢?


1187916-20200702193050090-2004068096.png


  1. 最先进入栈的是main方法, 会首先在线程栈中分配一块栈帧空间给main方法。
  2. main方法里面调用了compute方法, 然后会在创建一个compute方法的栈帧空间, 我们知道compute方法后加载,但是他却会先执行, 执行完以后, compute中的局部变量就会被回收, 那么也就是出栈.
  3. 然后在执行add方法,给add方法分配一块栈帧空间。add执行完以后出栈。
  4. 最后执行完main方法, main方法最后出栈. 这个算法刚好验证了先进后出. 后加载的方法会被先执行. 也符合程序执行的逻辑。


4.3 栈帧的内部构成


我们上面说了, 每个方法在运行的时候都会有一块对应的栈帧空间, 那么栈帧空间内部的结构是怎样的呢?


栈帧内部有很多部分, 我们主要关注下面这四个部分:


1. 局部变量表
2. 操作数栈
3. 动态链接
4. 方法出口

4.2.1 局部变量表: 存放局部变量


局部变量表,顾名思义,用来存放局部变量的。


4.2.2 操作数栈


那么操作数栈,动态链接, 方法出口他们是干什么的呢? 我们用例子来说明操作数栈


1187916-20200704070301419-1058065719.png


那么这四个部分是如何工作的呢?

我们用代码的执行过程来对照分析.

我们要看的是jvm反编译后的字节码文件, 使用javap命令生成反编译字节码文件.

javap命令是干什么用的呢? 我们可以查看javap的帮助文档


1187916-20200703070157315-1066054858.png


主要使用javap -c和javap -v

javap -c: 对代码进行反编译
javap -v: 输出附加信息, 他比javap -c会输出更多的内容

下面使用命令生成一个Math.class的字节码文件. 我们将其生成到文件

javap -c Math.class > Math.txt

打开Math.txt文件, 如下. 这就是对java字节码反编译成jvm汇编语言.

Compiled from "Math.java"
public class com.lxl.jvm.Math {
  public static int initData;
  public static com.lxl.jvm.User user;
  public com.lxl.jvm.Math();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public int compute();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/lxl/jvm/Math
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method compute:()I
      12: pop
      13: return
  static {};
    Code:
       0: sipush        666
       3: putstatic     #5                  // Field initData:I
       6: new           #6                  // class com/lxl/jvm/User
       9: dup
      10: invokespecial #7                  // Method com/lxl/jvm/User."<init>":()V
      13: putstatic     #8                  // Field user:Lcom/lxl/jvm/User;
      16: return
}

这就是jvm生成的反编译字节码文件.


要想看懂这里面的内容, 我们需要知道jvm文档手册. 现在我们不会没关系, 参考文章(https://www.cnblogs.com/ITPower/p/13228166.html)最后面的内容, 遇到了就去后面查就行了


我们以compute()方法为例来说说这个方法是如何在在栈中处理的

源代码
public int compute() {
  int a = 1;
  int b = 2;
  int c = (a + b) * 10;
  return c;
}
反编译后的jvm指令
public int compute();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12: ireturn

jvm的反编译代码是什么意思呢? 我们对照着查询手册


0: iconst_1 将int类型常量1压入操作数栈, 这句话的意思就是先把int a=1;中的1先压入操作数栈

1187916-20200703111214468-2001276415.png

1: istore_1 将int类型值存入局部变量1-->意思是将int a=1; 中的a变量存入局部变量表

注意: 这里的1不是变量的值, 他指的是局部变量的一个下标. 我们看手册上有局部变量0,1,2,3

1187916-20200704070911987-1281109966.png

0表示的是this, 1表示将变量放入局部变量的第二个位置, 2表示放入第三个位置.

对应到compute()方法,0表示的是this, 1表示的局部变量a, 2表示局部变量b,3表示局部变量c


1: istore_1 将int类型值存入局部变量1-->意思是将int a=1; 中的a放入局部变量表的第二个位置, 然后让操作数栈中的1出栈, 赋值给a


1187916-20200703111358928-685154040.png

1187916-20200704071401126-1655123487.png


2: iconst_2 将int类型常量2压入栈-->意思是将int b=2;中的常量2 压入操作数栈


1187916-20200704072320876-1329267965.png

相关文章
|
29天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
31 0
|
3天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
47 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
11天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
8天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
29天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
1月前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
28天前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
42 0
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
10天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析

热门文章

最新文章

推荐镜像

更多