JVM大工厂

刚开始学Java的时候,被贯彻最多的两句话就是“一次编译,到处运行”和“Java不需要手动释放内存”。能做到这两点都是由于Jvm的存在。记得大学第一个启蒙语言c,电脑安装了一个cfree(一个体积超小的ide)就可以直接写了。而Java还需要下载一个叫JDK的东西,来开发。JDK包含一个叫JRE的东西,是Java的运行环境,之所以可以运行,是jre下拥有着JVM虚拟机。JVM作为一个程序,一定会占用电脑内存,而它所管辖内存间数据的互动,驱动着Java的工作。

线程的指挥官:程序计数器

作为面向对象语言,Java每个类都有自己的属性和使命,并且暴露方法出来供其他成员调用。一个业务逻辑,不同对象之间调用方法、返回调用者,一个方法内部分支、循环等基础功能,都需要一个指挥官来完成,指挥官告诉这个线程内的对象执行的先后顺序。这个指挥官就叫做 程序计数器 。程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。因为一个CPU同一时间只能操作一个线程中的指令,所以每个线程需要私有一个指挥官,所以程序计数器这类内存也叫做 线程私有 内存。

如果一个线程正在执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码 指令地址;如果是正在执行的Native方法,这个计数器值则为空(Undefined)。Native方法就是Java调取本地其他语言的方法,此方法实现不受JVM管控,所以无法感知到地址,计数器值自然为空。

另外,程序计数器区域是唯一一个Java虚拟机规范中没有规定任何OutOfMemoryError情况的内存区域。

引用的地盘: Java虚拟机栈

我们使用Java新建一个对象,首先需要声明类型,此时就出现了一个 引用 ,引用指向创建出的对象。我们都知道引用在栈中,对象在堆中,此时说的栈就特指Java虚拟机栈。Java虚拟机栈同样属于线程私有的,所以生命周期和线程相同。每个方法在创建的同时,都会创建一个 栈帧 用于储存局部变量表、操作数栈、动态链接、方法出入口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了 编译 时克制的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)。对象引用直接或者间接指向堆中对象的地址。由于此过程是在编译时期完成的,所以局部变量内存分配大小是固定的,不会在运行时改变大小。其中64位长度的long和double类型的数据都占用了2个局部变量空间(Slot),其他数据类型只占1位。

在这个区域可能会出现两种异常:如果线程请求的栈深度过大,也就是说虚拟机栈在自己管辖的内存造成的原因,会抛出StackOverflowError异常,这个一般比较深的递归可能会造成。如果虚拟机栈发现自己内存不够,动态扩展,并且无法申请到足够的空间时,就会抛出OutMemoryError异常。

虚拟机栈的孪生兄弟:本地方法栈

本地方法栈几乎与虚拟机栈发挥的作用基本相似,毕竟孪生兄弟嘛。区别是Java虚拟机栈是为字节码服务的,也就是Java方法本身。而本地方法栈是为了Native方法服务的,这个涉及调取本地的语言,例如C。

这里插个小曲,natice对于咱们Java编程者来说很少直接操作,但是这东西无处不在,比如说Object类,你看源码,很多方法都有natice关键字。这些方法具体实现在java代码里面无论如何都找不到的,因为具体实现就是调取的本地,并且调取本地的代码不受JVM控制!在编译的过程中,如果发现一个类没有显示继承,那么就会被隐式继承Object类,也就有了Object类所有的方法。

GC最喜欢的地方:Java堆

我们常说的堆栈,说的就是这个堆。可以说Java堆是虚拟机所管辖最大的一块内存空间,并且此空间是所有线程 共享 的。几乎所有的对象实例都分配在这里,所有的对象实例和数组都要在堆上索取空间。Java堆也是垃圾收集器管理的主要区域,这个以后会细讲。 Java堆可以处于物理上不连续的空间中,只要逻辑上是连续的即可。如果堆中没有内存完成实例分配,并且对也无法再拓展时,将会抛出OutOfMemoryError异常。

永久代的伪装:方法区

大佬书中讲这部分内容的时候还是以JDK1.6为范本,但是直接被堆内存所托管了。JDK1.8这部分已经变成元空间了,并且成为了堆外内存,不受JVM直接管辖。但是为了更好的理解JVM内存模型的设计理念还是看下这部分内容。

方法区也属于线程共享区间,它储存着类信息、常量、静态变量即时编译后的代码等数据

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这群有同样的内存回收目标主要是针对常量池的回收和堆类型的卸载,但是回收条件相当苛刻。同堆一样,可能会导致OutOfMemeoryError异常。

运行可变区域:运行时常量池

既然有运行时常量池,就会有普通的常量池(简称常量池)。常量池用于存放编译期生成的各种字面量和符号引用,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。

运行时常量池相对于普通的常量池(又称Class文件常量池)有一个重要特征 动态性 。Java语言并不要求常量只能在比那一起才能产生,运行期间也可以加入常量到常量池(运行时常量池)中,比如String的intern()方法。

运行时常量池属于方法区的一部分,自然受到方法去内存的限制,也会抛出OutOfMemoryError异常。

JVM外的世界:直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。还记着前面说的有native关键字的方法吗?包括netty模块的一些Native函数库都是直接分配堆外内存的,然后通过一个储存在Java堆中的DirectByteBuffer对象作为这块内存的引用来操作。这样做,就是以为需要操作的数据在Native堆(你电脑上不被JVM管辖的内存空间)上,避免了将Java堆数据和Native堆数据来回复制。当然这块内存也不能无限放大,比如超过你电脑的内存,所以也可能出现OutOfMemoryError异常。

让数据动起来

内存空间不在于划分,在于使用。大佬在书中继续以HotStop虚拟机堆内存为例,讲解了数据的创建、分布、与访问。

一个对象的诞生

内存分配

虚拟机遇到一条new指令时,首先将去 检查 这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。接下来,虚拟机会为这个新生儿 分配内存 (加载完成后的内存是完全确定大小的)。和计算机管理内存的方式一样,Java堆维护内存,有一张 空闲列表 ,用于记录堆内哪些空间没有被使用过。由于堆在物理上是不连续的,所以就需要有个地方记录哪些空间是被使用的,哪些是空闲的。还有一种记录方式叫 指针碰撞 ,假定Java堆中的内存是绝对规整的连续的(这显然很难做到,需要GC做 压缩整理)。在这条十分规整的,十分长的堆内存空间上,有一个指针,左右两侧分别是空闲区间和已使用空间,如果有空间需要被申请或者释放,指针就左右移动。就好像温度计,水银好似已使用空间,上方空闲部分就是空闲空间,当温度达到100度,到了温度计的量程,就会炸了(出现OutOfMemoryError异常)。

原子操作

为了保证内存在使用的时候是 线程安全的 ,需要采用一些机制。第一种就是 CAS 机制,这是一种乐观锁机制,再加上失败重试,可以保证操作的原子性。还有一种就是 本地线程分配缓冲 ,把内存的动作按照线程划分在不同的空间上进行,即每个线程在Java堆中预想分配一小块内存供自己使用,让Java堆的共享强制编程线程私有。

对象设置

接下来,虚拟机要对对象头进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象的对象头之中。完成上述操作,一个对象在虚拟机的层面已经完成了,但是在代码层面还需要设置初始值,按照程序员的意愿选择不同的构造函数,传入不同的参数进行初始化。

对象的内存分布

在HotSpot的虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据、对其填充。

HotStop虚拟的对象头包含两部分信息,第一部分用于储存对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程II、偏向时间戳。官方叫这部分是 Mark Word ,这部分虽然在对空间上,但是这部分会根据对象的状态服用自己的储存空间。除了储存自身状态外,还有一部分内容叫 类型指针 ,即指向它的类元数组的指针,虚拟机通过这个指针来确定这个给对象是哪个类的实例。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录组长度的数据。

接下了就是实例数据部分,即真实储存的有效信息,也就是程序代码中所定义的各种类型的字段内容。包含从弗雷继承的,和子类定义的。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略中可以看出,相同宽度的字段总是被非陪在一起。在满足这个前提条年间的情况下,在 父类中定义的不该能量会出现在子类之前 。

第三部分就是对其填充,没有什么特别的意义,就是个占位符。由于对象的大小必须是8字节的整数倍,由于对象头部分正好是8字节的倍数,实例数据不一定是,所以就需要填充一下。

对象的访问定位

我们都知道真正的对象实在堆上,但是我们操作对象使用的是引用,在虚拟机栈上的引用是如何访问对上的数据呢?主流的有两种方式。

句柄

Java堆中将会划分出一块内存来作为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

直接指针

Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,而reference中储存的直接就是对象地址。

这两种方式的优缺点就好像数组和链表一样,一个访问速度快,一个操作快。毕竟世界是公平的,省功不省力,省力不省功。句柄访问的最大优点就是reference中储存的是稳定的句柄地址,在对象被移动时指挥改变句柄中的实例数据指针,而reference本身不需要修改。所以修改数据特别快。

相应的直接指针访问最大的优势就是访问对象本身更快,毕竟少了一次指针的地址定位。HotShot最主要就是采用这种方式访问对象。

一些补充

大佬在本章还进行了抛OutOfMemoryError异常的实战,内容较长,还是看书讲的更清楚些。更主要的是,我觉得实战这种东西不能只看,具体问题还得具体分析,等遇到的多了,自然解决起来就会得心应手。不过这部分内容有一些值得记录的知识点。

一般来说,栈深度(比如递归)达到1000~2000是没有问题的,所以我们写代码的时候一定要注意栈的深度,不要过深,但也要充分使用递归这种用空间省时间的方式。
JDK1.6~JDK1.8常量池的位置变动,导致一些方法展现出来的现象不同。例如String.intern()方法,在1.6时代,intern()方法会将首次遇到的字符串实例复制到永久代中,返回永久代中这个字符串实例的引用。而1.7的intern()方法不会复制实例,只是在常量池中记录首次出现的实例引用。
动态代理(例如CGLib)是对类的一种增强,增强的类越多,就需要更大的内存来保存这些数据。
还有种动态生成就是JSP(虽然现在大多数都是前后端分离,不用这个了),JSP第一次运行需要编译成Servlet,也需要产生大量的空间。值得一提的是,原来我在上家公司,有个系统是JDK1.7,当时JSP编译出来的东西还存放在方法堆中,当时可能设置的堆内存不大,本地跑一天,每次打开JSP页面,电脑都会卡顿一下(当然机子差也是原因之一),普通的Java文件就没事,我想是不是也是这个原因呢。另外对于同一个文件,不同的加载器加载也会视为不同的类。
需要java学习路线图的私信笔者“java”领取哦!另外喜欢这篇文章的可以给笔者点个赞,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~

JVM-Java高墙之内存模型相关推荐

  1. 【Java杂货铺】JVM#Java高墙之内存模型

    Java与C++之间有一堵由内存动态分配和垃圾回收技术所围成的"高墙",墙外的人想进去,墙外的人想出来.--<深入理解Java虚拟机> 前言 <深入理解Java虚 ...

  2. JVM#Java高墙之内存模型

    前言 <深入理解Java虚拟机>,学习JVM的经典著作,几乎学习JAVA的小伙伴人手一本.当初买了,翻看了一部分,到了字节码那边彻底读不下去了,遂弃之.最近打算看Spring源码,反射.动 ...

  3. java中数组的内存模型_Java如何在内存有限的情况下实现一个超大数组?jvm性能调优+内存模型+虚拟机原理能解决什么样问题...

    在上一篇文章中,小编为您详细介绍了关于<变频器调速问题?三星R458更换CPU>相关知识.本篇中小编将再为您讲解标题Java如何在内存有限的情况下实现一个超大数组?jvm性能调优+内存模型 ...

  4. java线程内存模型_深度解析Java多线程的内存模型

    内部java内存模型 硬件层面的内存模型 Java内存模型和硬件内存模型的联系 共享对象的可见性 资源竞速 Java内存模型很好的说明了JVM是如何在内存里工作的,JVM可以理解为java执行的一个操 ...

  5. JAVA基础驿站,Java基础:内存模型

    在java基础当中,有很重要一块的内容,就是关于java当中的内存模型,所涉及的问题无非就是关于java当中的类.实例.变量.常量.静态变量.方法等blabla的东西都分别存储在什么地方.这部分的内容 ...

  6. java关键字 valotile_Java内存模型-jsr133规范介绍,java中volatile关键字的含义

    最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范. 系统的看了jsr 133规范的前面几 ...

  7. 学习笔记【Java 虚拟机④】内存模型

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 总目录 学习笔记[Java 虚拟机①]内存结构 学习笔记[Java 虚拟机②]垃圾回收 学习笔记[Java ...

  8. JVM垃圾回收系列--内存模型/垃圾回收流程

    原文网址:JVM垃圾回收系列--内存模型/垃圾回收流程_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java各个代的关系(内存模型)及垃圾收集流程. 内存模型 JDK8的内存模型 在Java中所有 ...

  9. java happens before_java内存模型以及happens-before规则

    原创文章&经验总结&从校招到A厂一路阳光一路沧桑 image# 1. JMM的介绍 # 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程 ...

  10. 无招胜有招之Java进阶JVM(四)内存模型plus

    一.计算机内存模型: 在多CPU的系统中,每个CPU都有多级缓存,一般分为L1.L2.L3缓存,因为这些缓存的存在,提供了数据的访问性能,也减轻了数据总线上数据传输的压力,同时也带来了很多新的挑战,比 ...

最新文章

  1. Linux那些事儿 之 戏说USB(11)繁华落尽
  2. Linux sendmail 服务器
  3. top,right,bottom,left设置为0有什么用?
  4. 章节六、2-异常---运行时异常
  5. 云原生时代,底层性能如何调优?
  6. 反射在工厂模式中的使用
  7. iphone隔空投送android设备,苹果手机隔空投送怎么使用?iPhone隔空投送功能使用教程介绍[多图]...
  8. 后台事务开发之简单示例
  9. matlab随机欠采样,欠采样技术
  10. MATLAB运行程序后workspace是空的
  11. win7系统没有telnet服务器,Win7系统没有telnet协议服务解决方法
  12. 可能是最好用的单文件制作工具jexchan下载 | 含jexchan单文件制作工具使用详细教程
  13. 水果之王之猕猴桃-系列五(孕期可以吃猕猴桃吗?)
  14. 一份Python面试宝典
  15. 【华为认证】HCIA-DATACOM技术分享-VRP系统基本操作-入门级手册(一)
  16. PHP之ctype扩展
  17. Mybatis实现联合查询(六)
  18. 指数分布c语言,C语言下泊松分布以及指数分布随机数生成器实现
  19. Apples Prologue(吃苹果问题) C++
  20. Flutter中 解决自定义阿里妈妈图标一直显示不出来的问题

热门文章

  1. 数字化经济带领各行各业迎来碧海蓝天
  2. 7种流行PHP编辑器的比较(PHP IDE)
  3. 【转】Mac突然连不上WiFi解决步骤
  4. python实现直方图、条形图、折线图、饼图(参数详情)
  5. Transformer 五年引用超四万,其中六位作者创立五家创业公司
  6. pn532中遇到的坑-----Mifare1 Card(一)
  7. 山师计算机考研山大,研友分享:海大,山大,山师考研分析
  8. C语言小项目之扫雷游戏(简易版)
  9. 什么是入耳式监听器,谁应该使用它们?
  10. NOIP模拟赛csy2021/10/30