JVM浅谈一:内存区域

旧城等待, 2023-07-03 12:07 59阅读 0赞

前言

本系列基本可以说是《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》的读书笔记,所以也是以jdk1.8之后的HotSpot为参照来讲解。

这里先get一个常识,Oracle制定了《Java虚拟机规范》,所以很多厂商也根据各种各样的目的按照规范开发了自己的虚拟机。而Oracle Jdk中自带的虚拟机就是的HotSpot。

这一系列博客就是根据自己的理解再将书中内容用更通俗的方式表达出来,而之所叫浅谈,是以为确实理解得很浅显…

作为Java程序员,应该都知道一些程序员的黑话,比如:

  • 堆:变量啊对象什么的都存在里面…吧?
  • 栈:这个我知道!先进后出,后进先出!
  • 内存泄漏:这个问题的解决方案主要是重启程序
  • 内存溢出:这个问题的解决方案主要是把-Xmx调大调大调大,次要解决方案还是重启程序

讲真之前遇到类似概念、问题时,我也基本是这么理解和处理的。而且像我们8线程序员要调试性能,就是一个字:“加硬件!”…而且恕我直言,我不是针对谁,我是说在座各位,包括我在内,都是辣鸡。代码都写得迷迷糊糊,还想挑虚拟机的刺??!!!

所以了解这块主要还是想在面试的时候让自己有逼可装吧(手动狗头)。


阅读目的

通过本文可以了解到Java程序在运行过程中是如何划分内存区域的,以及各个区域的作用是什么。


正文

Java虚拟机在运行Java程序时,会将内存划分为若干个区域,那么为什么要划分为这些区域啦,是因为《Java虚拟机规范》规定的,那么《Java虚拟机规范》为什么要这么规定啦,我也不知道…所以我们只需要把重点放到下面几个内存区域就ok了,总共有这么5个:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • Java堆
  • 元空间

下面对这7个区域做个通俗解释

程序计数器

(这玩意重要但不需要理解得特别深刻,因为它不会出啥问题,也不对会后文理解产生多大影响。)可以这么理解,这个计数器主要是用来记录虚拟机在执行代码(字节码指令)时,具体执行到哪一行了。 为了帮助理解,我们举个很不严谨的例子:

比如说有Test1和Test2两个程序,这两个程序主要干这些事情:

Test1

  1. 后宫=new ArrayList
  2. 大老婆=长泽雅美
  3. 小老婆=堀北真希
  4. 大老婆添加到后宫
  5. 小老婆添加到后宫

Test2

  1. 天降一位修车匠
  2. 修车匠开始修车

ok,我们现在同时运行两个程序,那么这个时候至少就会有两个线程:

  • 线程1:Test1
  • 线程2:Test2

然后这里有一个常识我们get一下:单个CPU核心在某个时间点只能处理一个线程,而我们的电脑之所以能同时运行多个程序,都是CPU轮流切换运行的效果,比如第1微秒处理微信的指令,第2微秒处理浏览器的指令,第3秒接着处理微信的指令。 所以Test1和Test2的运行也是通过CPU轮流切换来执行其中的代码的,比如:

  • 第1微秒:执行Test1中的第一行代码(后宫=new ArrayList)
  • 第2微秒:执行Test1中的第二行代码(大老婆=长泽雅美)
  • 第3微秒:执行Test2中的第一行代码(天降一位修车匠)
  • 第4微秒:执行Test1中的第三行代码(小老婆=堀北真希)
  • 直到所有代码(字节码)执行完毕

而第4微秒我们从Test2的线程切换回Test1的线程时候,Java虚拟机需要知道该让CPU接着执行哪一行代码,那么就需要一个计数器来记录当前线程执行到哪一行了,所以就有了这个程序计数器。由此我们可以知道它的一个特性就是 每个线程都独立拥有一个互不干扰的程序计数器。 另外根据Java虚拟机规范中的规定,程序计数器是唯一一个不会出现OutOfMemoryError的内存区域。

虚拟机栈(Stack)

虚拟机栈就是传说中“堆栈”的栈。在Java中,每个线程都会对应一个栈,而每个方法则对应栈里面的一个栈帧,方法的调用和返回就对应栈帧在栈里的入栈和出栈操作,而栈的生命周期也与线程的生命周期一致。看不懂?没关系,下面我用人类的语言解释下:

现在有一个公司,公司的老板突然想看看上周的运营周报,就找到总经理说:“把周报发我一下。”那么总经理怎么来执行啦?

  1. 总经理找到运营经理说上周周报给我一下,急
  2. 运营经理找到运营小黄说马上把上周周报给我
  3. 运营小黄找到研发李师傅说大佬马上帮我统计一下上周的数据呗,中午外卖我包了
  4. 李师傅说好吧,然后先sleep了1000x60x30毫秒,然后开始统计周报,并把统计好的周报给到运营小黄
  5. 运营小黄把周报给到运营经理
  6. 运营经理把周报给到总经理
  7. 总经理把周报给到老板

我们用动画来表示就是这样:
在这里插入图片描述

那么我们现在就把刚才描述的事情当作一个线程,那公司就是这个线程对应的栈,每个人就是栈里面的一个栈帧。而老板就是这个栈里面的第一个栈帧,也是这个线程的入口方法。类似老板找到总经理,总经理找到运营小黄时,他们的出现就叫“入栈”。后面研发李师傅把周报给到运营小黄之后,李师傅的任务就完成了,李师傅就消失了,这个消失就叫“出栈”。

那么现在我们就知道了,每个线程运行时,都会分配一个栈,而每调用一个方法时,在栈里面都会有一个栈帧入栈,而每当方法执行完毕之后,方法对应的栈帧就出栈了。所有栈帧都出栈之后就意味着这个线程里面所有的方法都执行完毕了,也意味着该条线程也执行完毕了,所以线程就死亡了,线程中的栈当然也死亡了。所以我们说栈的生命周期就对应线程的生命周期。

栈能容纳的栈帧数量肯定是有限的,所以当栈无法再分配新的栈帧的时候就会出现StackOverFlowError。(这一块我们在以后的文章验证。)

那栈里面存的是栈帧,栈帧里面有什么啦?先看看下面这个栈结构图:

在这里插入图片描述

从上图可知,栈帧里面保存有“局部变量表”、“操作数栈”、“动态链接”、“方法出口”等信息。其中局部变量表就是保存方法中的局部变量参数以及方法参数。这些参数支持int、long、byte等基本类型,也支持某个对象实例的引用,称作reference。操作数栈、动态链接等信息这里就先不作介绍了。

####本地方法栈
这一块也不多作介绍。本地方法栈和虚拟机栈是类似的,区别只是虚拟机栈是为你写的程序服务,而虚拟机栈是为本地方法服务(Native)。并且在HotSpot中,虚拟机栈和本地方法栈是合二为一。


Java堆(Heap)

传说中“堆栈”中的堆。堆是Java虚拟机中最大的一块内存区域,它主要保存对象的实例,由所有线程共享。同时也是垃圾处理器主要工作的区域以及OutOfMemoryError的高发区域。这里只对堆做简单介绍,后面会单开一章做详细介绍。

元空间

元空间是jdk1.8之后方法区的一种实现(jdk1.8之前称之为永久代),我们先说方法区是什么。方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

其实对于这一块我们只需要暂时了解以下几点就够了:

  • 元空间保存了类、方法的描述信息(类加载完成后会放入内存)
  • 元空间还保存了运行时常量池(字面量和直接引用)
  • 元空间这块内存区域不属于java虚拟机,而直接在系统内存里面开辟

关于元空间的更多内容,后面会详细讲。


ok了,写完了,JVM中的内存区域主要就是这些区域,当然这边文章只能算一个引子,只是对各个区域做了一个科普,如果有错误的地方请一定提出来,在下虚心改正。后面经过我“深入”的学习之后,会将更多内容提炼出来发到博客中,观众朋友们,我们下期再见~


参考内容

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
  • 深入理解Java虚拟机笔记—运行时栈帧结构
  • 【JVM笔记】class文件常量池(JVM 常量池)、运行时常量池、字符串常量池的总结
  • 【JVM笔记】永久代(PermGen)和元空间(Metaspace)的区别

发表评论

表情:
评论列表 (有 0 条评论,59人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM内存区域

    Java语言最重要的特点之一:跨平台使用,正是由于JVM的存在。 想要Java开发稳步进阶,学JVM这条路绕不开。 1.为什么要学JVM? 我们都知道,要做Java开

    相关 JVM内存区域

    程序计数器 1.字节码解释器通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个程序计数器来完成。 2.每

    相关 JVM内存区域

    Java虚拟机所管理的内存包括以下几个运行时数据区域,如下图 ![20160706211017869][] 方法区 用于存储已经被虚拟机加载的类信息、常量、静态变量

    相关 Java内存区域划分

    Java虚拟机在执行Java的过程中会把管理的内存划分为若干个不同的数据区域: ① 程序计数器:通过改变程序计数器的值来指定下一条需要执行的指令,分支,循环等基础功能就是依赖