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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 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

相关文章
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
6天前
|
数据采集 存储 Web App开发
Java爬虫:深入解析商品详情的利器
在数字化时代,信息处理能力成为企业竞争的关键。本文探讨如何利用Java编写高效、准确的商品详情爬虫,涵盖爬虫技术概述、Java爬虫优势、开发步骤、法律法规遵守及数据处理分析等内容,助力电商领域市场趋势把握与决策支持。
|
6天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
17 1
|
10天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
11天前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
|
16天前
|
存储 算法 Java
Java Set深度解析:为何它能成为“无重复”的代名词?
Java的集合框架中,Set接口以其“无重复”特性著称。本文解析了Set的实现原理,包括HashSet和TreeSet的不同数据结构和算法,以及如何通过示例代码实现最佳实践。选择合适的Set实现类和正确实现自定义对象的hashCode()和equals()方法是关键。
25 4
|
4月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
392 0
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
64 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。