Connor学JVM - Java内存区域

在这里插入图片描述

Learn && Live
虚度年华浮萍于世,勤学善思至死不渝

前言

本文主要参考了周志明老师所著《深入理解Java虚拟机》,如有纰漏偏差,欢迎各位大佬指正!原创不易,转载请注明出处:http://t.csdn.cn/UORhr

JVM体系结构

相信在简历中赫然列出“熟悉Java编程语言”的你对下面这幅图一定不会感到陌生吧,JVM体系结构作为各大面经文章的常客,即将带领我们进入深入理解JVM的崭新世界。
在这里插入图片描述
由上图可见,JVM的体系结构包含以下五大部分:

  1. 类加载器子系统(Class Loader SubSystem)

  2. 运行时数据区(Runtime Data Area)

  3. 执行引擎(Execution Engine)

  4. 本地库接口(Java Native Interface)

  5. 本地方法库(Native Method Libraries)

那么上述这五大部分各自发挥着怎样的作用呢?我们平时写出的代码又是怎么跑起来的呢?

五大部分各自的作用会在后续的学习中逐步细化,这里只给出简述。

  1. 我们平时写代码得到的java文件,经过编译器编译生成字节码文件

  2. 通过类加载器(Class Loader)加载到JVM中

  3. 运行时数据区(Runtime Data Area)将字节码加载到内存中

  4. 字节码文件只是针对JVM的一套指令集规范,并不能直接交给底层操作系统去执行,需要将字节码文件经执行引擎(Execute Engine,实际是执行引擎中的即时编译器)编译成具体的机器码

  5. 根据生成的机器码找到对应的本地接口,调用操作系统的本地方法库完成具体的指令操作

如此这般,我们的代码历经五道关卡,就可以顺利地跑起来了。

JVM线程

  1. 在多核操作系统上,JVM允许在一个进程内并发执行多个线程。

  2. JVM中的线程与底层操作系统中的线程是相互对应的,在JVM线程的本地存储、缓冲区分配、同步对象、栈、程序计数器等准备工作完成后,JVM会调用操作系统的接口创建一个与之对应的操作系统原生线程;在JVM线程运行结束时,原生线程也会被回收

  3. JVM后台运行的线程主要有:虚拟机线程、周期性任务线程、GC线程、编译器线程、信号分发线程。

JVM运行时数据区

在这里插入图片描述

首先明确下面几个JVM线程的相关概念,方便我们之后对各个区域的展开学习:

  1. 运行时数据区可分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)。
  2. 线程私有区域的生命周期与线程相同,与JVM线程“同生共死”,每个线程与操作系统的原生线程直接映射。
  3. 线程共享区域则是与JVM“同生共死”,即随虚拟机的启动而创建,随虚拟机的关闭而销毁。

明确了JVM线程的相关概念后,我们就可以继续深入运行时数据区的各个子区域了

程序计数器

有一定计算机基础的小伙伴对这个名字一定不会陌生,事实上它的作用也是与操作系统中的类似。

  1. 线程私有
  2. 可以看作当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来执行下一条字节码指令,可以完成程序中分支、循环、跳转、异常处理、线程恢复等基础功能
  3. 保存线程执行状态。由于JVM的多线程是通过线程轮流切换、分配处理器执行时间来实现的,因此在任一时刻,一个处理器都只会执行一条线程中的指令,当当前线程暂停执行时,线程私有的程序计数器会停留在正在执行的位置,从而保存当前线程的执行状态
  4. 计数器记录内容视线程正在执行的方法而定。若线程正在执行Java方法,计数器记录的是正在执行的字节码指令的地址;若正在执行的是本地(Native)方法,这个计数器则应为空(Undefined)
  5. 异常情况:唯一一个没有规定任何OutOfMemoryError情况的区域

Java虚拟机栈

相信之前在学习Java的基础内容是你一定了解过这样一个概念:在Java代码中定义的局部变量(方法内定义的变量)被存储到JVM中的栈中,按照栈后进先出(FILO)的方式进行维护。但你同时可能也会有这样一个疑问:既然局部变量是定义在方法内的,那如果我们不对这些变量做一定的区分,一股脑地塞到栈中,那么在被所在的方法调用时会是怎样“壮观”的景象呢?相信下面的解释会让你豁然开朗。

JVM栈的特点概念:

  1. 线程私有
  2. 栈帧(Stack Frame)模型。每个方法被执行时,JVM都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在JVM栈中从入栈到出栈的过程
  3. 局部变量表存放了编译期可知的基本数据类型数据(boolean,byte,char,short,int,float,long,double)、对象引用类型数据(注意是引用,存储的是地址信息,真正的对象存储在堆中,之后会涉及到)和returnAddress类型(指向一条字节码指令的地址);这些数据在局部变量表中所占的内存以**局部变量槽(Slot)**表示,64位类型数据(double,long)占两个变量槽,其他类型数据占一个变量槽
  4. 局部变量表的内存空间在编译期间即完成分配,在方法运行期间不会改变局部变量表中变量槽的数量
  5. 异常情况:
    • JVM栈的深度大于JVM所允许的深度,抛出StackOverflowError
    • 若JVM栈容量可以动态扩展,当栈扩展到无法申请到足够的内存会抛出OutOfMemoryError

本地方法区

又名本地方法栈(Native Method Stack)

  1. 线程私有
  2. 与JVM栈相对应,JVM栈提供执行Java方法的服务,而本地方法区提供执行Native方法的服务
  3. 与JVM栈相同,涉及到栈的深度限制和申请内存限制时可能会抛出StackOverflowErrorOutOfMemoryError

Java堆

  1. 线程共享
  2. JVM中最大的一块内存区域,用于存放对象实例
  3. Java堆可以处于物理上不连续的内存空间,但逻辑上应当被视为连续的
  4. 可根据不同的逻辑对堆区域进行划分,但应注意,划分只是为了更好的进行管理(回收内存或分配内存角度),堆内存储的内容并不会发生变化
    • 回收内存的角度来看,此时堆也可称作“GC堆”,经典的划分设计分代设计,即划分为新生代、老生代(之后会详细介绍)
    • 分配内存的角度来看,可以将线程共享的堆划分为多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),提升对象分配时的效率
  5. 异常情况:当堆空间不足以完成示例分配,或无法再扩展时,抛出OutOfMemoryError

方法区

  1. 线程共享

  2. 存放已被JVM加载的类信息、final常量、static静态变量、即时编译器编译后的代码等数据

  3. 不需要连续的内存空间,可选择固定大小或可扩展,可以选择不实现垃圾回收

    方法区的垃圾回收主要涉及:(这里会在之后垃圾回收算法部分详细介绍)

    • 常量池回收
    • 类型卸载
  4. 异常情况:方法区不足以分配新的内存空间,抛出OutOfMemoryError

说到GC机制和经典分代的设计方法,你一定听到过**“永久代”**这一概念,甚至会在很多前辈的文章中看到将方法区等同于永久代,而事实真的如此吗?事实上两者是不等价的,下面以JDK版本作为时间轴解释

  • JDK8之前,“永久代”仅作为HotSpot虚拟机设计团队的一种实现方法区的实现方式,即参考堆的分代设计,方便管理内存
  • JDK7,HotSpot设计团队逐渐体会到这一设计的缺陷,将永久代中的字符串常量池、静态变量等移至堆中
  • JDK8,HotSpot彻底放弃永久代设计,改为在本地内存实现的元空间(Meta-space)存放JDK7之后永久代中的内容(主要是类信息)
运行时常量池
  1. 方法区的一部分

  2. 存放常量池表(Constant Pool Table),包含编译期生成的各种字面量、符号引用及符号引用翻译得到的直接引用

    • 字面量:int a = 1;中的1,String = “abc”;中的“abc”

    • 符号引用:一组由任意形式的字面量组成的符号,唯一标记一个不清楚所引用目标实际地址的类,如在A类中引用的B类,与JVM内存布局无关,所引用的类不一定加载到内存中

    • 直接引用:与符号引用相对应,与JVM布局相关,所引用的目标会加载到内存中,不同的JVM翻译所得结果不同。实现方式包括指针/地址、相对偏移量(下标)、间接定位句柄(这里先mark一下,需要后续了解学习)

  3. **动态性。**常量不一定只有编译期才能产生,运行期间也可以加入新的常量(intern()方法)

  4. 异常情况:方法区内存OutOfMemoryError

直接内存(Direct Memory)

不是运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域

NIO可以使用Native函数库直接分配堆外内存,然后通过堆中的DirectByteBuffer对象作为这块内存的引用进行操作,可以显著提高执行效率(有cache那味了哈)

直接内存的分配不会受到Java堆大小的限制,但总会受到本机内存空间的限制,因此超出内存限制时也会抛出OOMError

写在最后

种一棵树最好的时间是十年前,其次是现在

首先真的十分感谢你能够看到这里,感谢每一位来到这里的朋友对我的支持

这篇文章是作为科班出身的我,在大学三年的时光中发出的第一篇博客,也是我人生中创作的第一篇博客

回顾三年编程路上的寻觅与探索,从一字一句的纸质笔记,到利用md、pad完成的一篇篇电子笔记,算起来也留下了不少我在这三年“活过”的痕迹,但总觉得缺了点什么,总觉得和一起同行的朋友们差了些什么

直至最近看到很多朋友的博客、看到很多大佬用心创作的文章,我恍然大悟,找到了自己找寻了三年也没有找到的东西:分享,过去的三年中我大多处于闭门造车的状态,做笔记也只是将基础知识加上一点自己的看法、总结记录下来,对或错又有谁清楚?

但博客截然不同,在这里哪怕只是作为一种记录、一种分享,我也能感到以一种无形的责任与使命,驱动着自己向更深更广挖掘,尽可能全面、准确的表现出来,而这样深入探索的过程,让我感到前所未有的满足与快乐

“混了三年才写了一篇博客”,是的,但请在其中加个“第”字,种一棵树最好的时间是三年前,其次是现在,正如我的人生信条:勤学善思至死不渝,不要在乎起点在哪,而更要关注追逐的过程和冲向终点的喜悦……

那就让我们从现在出发吧。一起加油,努力记录自己每一次学习的过程!

,而这样深入探索的过程,让我感到前所未有的满足与快乐

“混了三年才写了一篇博客”,是的,但请在其中加个“第”字,种一棵树最好的时间是三年前,其次是现在,正如我的人生信条:勤学善思至死不渝,不要在乎起点在哪,而更要关注追逐的过程和冲向终点的喜悦……

那就让我们从现在出发吧。一起加油,努力记录自己每一次学习的过程!

十分欢迎各位大佬前辈针对内容和创作提出宝贵的意见,感谢阅读!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ConnorYan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值