JVM笔记1--Java内存区域

1、运行时数据区域

image.png
从上图可以看出来,Java虚拟机运行时数据区域整体上可以分成5大块:

1.1、程序计数器

程序计数器是一块较小的内存空间。它可以看做当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条所需要执行的字节码指令。它是程序控制流的指示器、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器。
由于Java虚拟机的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的。在任何一个时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条字节码执行。因此,为了线程切换后能恢复到正确的执行位置每个线程都需要一个独立的私有的程序计数器。各个线程间互不影响独立存储。我们称这种类型的内存区域为“线程私有”的内存。
如果线程正在执行一个Java方法,那么这个计数器记录的就是正在执行的字节码指令的地址。如果正在执行的是本地(Native)方法,那么这个计数器的值为空。同时,此内存区域是唯一一个在**《Java虚拟机规范》没有规定任何OutOfMemoryError**情况的区域。

1.2、Java虚拟机栈

与程序计数器类似,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行时,Java虚拟机都会同步创建一个栈帧用于存储局部变量表操作数栈动态链接方法出口等信息每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程
经常有人把Java内存区域笼统的换分为堆内存和栈内存。这种划分方式直接继承自C,C++程序内存布局结构。但是对于Java这种划分方式就显得比较粗糙。实际的内存区域划分比这更复杂。不过这种划分方式流行,也间接说明了程序员最关注的内存区域就是“堆”和“栈”。这里面的“栈”通常指的就是虚拟机栈。或者更多的情况下指的是虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一个字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位的long和double类型占用两个局部变量槽其余的数据类型占用一个变量槽局部变量表所需的内存空间在编译期完成分配当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请注意,这里的**“大小”指的是变量槽的数量**。虚拟机真正使用多大的内存空间(例如一个变量槽占用32位还是64位)来实现一个变量槽,完全是由具体的虚拟机实现自行决定。
在《Java虚拟机规范》中,对这个内存区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverflowError异常;如果,Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常

1.3、本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似。不过虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机执行本地方法服务
《Java虚拟机规范》对本地方法栈中方法使用的语言。使用方式与数据结构并没有任何强制规定。也就是说具体的虚拟机可以根据需要自由的实现它。甚至有的虚拟机(Hot-Spot虚拟机)直接将本地方法栈和虚拟机栈合二为一。与虚拟机栈一样本地方法栈也会在栈深度超出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

PS:在HotSpot虚拟机中,并不区分虚拟机栈和本地方法栈,因此只能通过-Xss来设置栈的大小。
对于栈中的OutOfMemoryError,Java虚拟机规范中规定实现者自主选择是否支持栈动态扩展。如果不支持栈的动态扩展,那么在运行时是不会出现OutOfMemoryError异常错误的。只有在创建线程时,申请内存时就无法获取到足够内存才会出现OutOfMemoryError异常。在运行时只会出现由于栈容量无法容下新的栈帧而出现StackOverflowError。

验证出现StackOverflowError异常的方法:

1、使用-Xss来设置栈的容量
2、定义大量的本地变量,增大此方法栈中本地变量表的长度。

1.4、Java堆

Java堆是虚拟机管理的最大的一块内存区域。Java对是被所有线程共享的一块内存区域,在虚拟机启动时创建。在《Java虚拟机规范》中对Java堆的描述是“所有的对象实例以及数组都应当在堆上分配”。在《深入理解Java虚拟机》中作者是这样说的:“在Java世界里,几乎所有的对象实例都在Java堆上分配内存”。这里作者用的是“几乎”是指从实现角度上来说的。随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持。即使在现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配标量替换优化手段已经导致一些微妙的变化。所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
如果从分配内存的角度来看Java堆,所有线程共享的Java堆可以划分外出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配的效率。但是无论怎么划分,都不会改变Java堆存储内容的共性,无论哪个区域存储的都是对象实例。
在《Java虚拟机规范》中规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上应被视为连续的。
Java堆既可以被实现成固定大小,也可以被实现为可扩展的。可以通过**-Xms,-Xmx**来设置堆的最小,最大内存(如果Xms和Xmx一样大,则不能动态扩展)。如果Java堆没有足够内存用来新实例创建,且无法扩展时,将会抛出OutOfMemoryError异常。

验证OutOfMemoryError异常方法

1、通过-Xms,-Xmx来设置堆的最小和最大容量,然后创建大量的类。

1.5、方法区

方法区与Java堆一样,也是线程间共享的内存区域。用于存储被虚拟机加载的类型信息常量静态变量、以及即时编译器编译后的代码缓存等数据。虽然**《Java虚拟机规范》中把方法区描述为堆的一部分**,但是它却有一个别名叫“非堆(Non-Heap)”,目的是与Java堆区分开。
在JDK8以前,由于很多人习惯在HotSpot虚拟机上开发,很多人更愿意把方法区称之为“永久代(Permanent Generation)”,或将两者混为一谈。但是本质上两者还是有区别的。因为仅仅是当时的HotSpot虚拟机的开发团队选择把收集器的分代设计扩展至方法区,或者使用永久代实现方法区而已。这样就可以让垃圾收集器管理方法区的内存回收。
JDK6之后,HotSpot虚拟机,就有了放弃永久代,逐步采用本地内存(Native Memory)来实现方法区的计划。在JDK7的HotSpot虚拟机中,已经将方法区中的字符串常量池、静态变量等移出(放到堆中)。到了JDK8,已完全放弃永久代的概念用在本地内存中实现的元空间(Meta-space)来替代,把JDK7中剩余的内容(主要是类型信息)全移动到元空间
《Java虚拟机规范》中规定,当方法区无法满足新的内存分配时,将抛出OutOfMemoryError异常。

MetaSpace VM 参数

1、-XX:MaxMetaspaceSize:设置元空间最大值,默认时-1,表示没有限制,或者说只受限于本地内存大小。
2、-XX:MetaspaceSize:设置元空间的初始大小。以字节为单位,达到该值就会触发垃圾收集进行类型卸载,,同时收集器会调整该值:如果释放了大量的空间,就适当降低该值;如果释放了少量空间,在不超过MaxMetaspaceSize情况下,适当提高该值。
3、-XX:MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量百分比。可以减少由于元空间不足导致的垃圾收集频率。相应的还有-XX:MaxMetaspaceFreeRatio控制元空间最大的剩余容量百分比。

1.6、运行时常量池

运行时常量池是是方法区的一部分Class文件中除了有类的版本字段方法接口等描述信息。还有一项信息就是常量池表,用于存放在编译期生成的各种字面量与符号引用这部分内容在类加载好之后存放到运行时常量池中
Java虚拟机对Class文件的每一部分(自然包括常量池)都有严格的规定。如每一个字节用来存储哪种数据都必须符合规范上的要求才能被虚拟机认可、加载和执行。但是对于运行时常量池,《Java虚拟机规范》并没有作任何细节上的要求。
运行时常量池相对于Class文件中的常量池区别在于,其具备动态性。Java语言并没有要求常量只能在编译期才能产生。也就是说,并非内置于Class文件常量池中的内容,在类加载后才能进入运行时常量池中。在运行期间也可以将新的常量放入到池中。就是String类的intern方法
运行时常量池也是方法区的一部分,所以其在没有足够内存进行新的内存分配时,也会抛出OutOfMemoryError异常。

验证OutOfMemoryError异常方法

1、在JDK7之前,常量池是放在永久代中的。因此可以使用-XX:PermSize=6m和-XX:MaxPermSize=6m来变相限制常量池的大小。
2、在JDK7之后,永久代逐渐被metaSpace取代,在JDK8中,永久代就已经不存在了。而常量池而迁移到到了堆中。因此只能通过限制堆的大小来限制常量池的大小。

1.7、直接内存

直接内存既不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也频繁的使用,且也可能产生OutOfMemoryError异常
在JDK1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I/O方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不受Java堆大小的限制。但是,既然是内存,那肯定还是会受到本机总内存大小的限制,一般服务器管理人员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,是得各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

直接内存可以通过-XX:MaxDirectmemorySize来控制大小,如果不指定大小,则默认与Java堆最大值(-Xmx)一致。

PS:由直接内存导致的内存溢出,一个明显的特征是在HeapDump文件中不会看到有什么明显的异常情况。如果发现内存溢出之后产生的Dump文件很小,而程序中又间接使用了DirectMemory(典型的间接使用就是NIO),那就可以重点检查下直接内存方面的原因。

2、对象创建过程

  1. 代码执行到new指令位置时,首先去检查这个指令的参数能否在常量池定位到一个类的符号引用,并检查这个类的符号是否已被加载,如果没有加载将执行相应的类加载过程。
  2. 在类加载检查通过后,进行内存分配。为对象分配空间等同于将一块固定大小的内存从Java堆上划分开来。内存分配的方式有两种:
    1. 指针碰撞方式:假设Java堆是规整的,已经使用的内存放到一边,没使用的放到另一边,中间放着一个指针作为分界点的指示器。那么分配内存就是将指针向空闲的一方移动一段与对象大小相等的距离。这种分配方式为“指针碰撞”。
    2. 空闲列表:如果Java堆是不规整的。那么就需要一个列表存储哪些内存是已经使用的,哪些是没使用的。内存分配就是从空闲列表中找到一块足够大小的区域划分出与对象相同大小的内存区域,并更新列表上的记录。

选择哪种方式取决于Java堆是否规整,而Java堆是否规整,取决于采用的垃圾收集器是否带有空间压缩整理功能。因此当采用Serial、ParNew等带有压缩整理功能的收集器时,系统采用的就是指针碰撞方法;而当使用CMS这种基于清除算法的收集器时,系统采用的就是空闲列表方法。
除如何划分空间外,还有一个问题需要考虑:对象创建在虚拟机是非常频繁的操作,即使是移动指针的方式,在多线程下也不是线程安全的。虚拟机采用两种解决方案一种是采用CAS和失败重试的方法来保证更新操作的原子性;另一种是把内存分配的动作按照线程划分到不同的空间之中进行。即每个线程在java堆中先预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)哪个线程要分配内存,就在哪个线程中的本地缓冲区中分配只有当本地缓冲区用完了,分配新的缓存区时,才会进行同步锁定。虚拟机是否使用TLAB,通过**-XX:+/-UseTLAB**参数来设定。

  1. 内存分配之后,虚拟机将分配到的内存空间(不包括对象头)初始化零值,如果使用了TLAB的话,这一步也可以提前到TLAB执行。这步操作,保证Java对象的实例字段在不赋初始值时就能使用。读取到的就是各个数据类型的零值。
  2. 接下来,Java虚拟机对对象进行必要的设置,比如这个对象是哪个类的实例如何才能找到元数据信息对象的哈希码(实际上延迟到真正调用Object::hashCode()方法才会进行计算)、对象的GC分代年龄等。
  3. 到这一步,从Java虚拟机的角度来看,一个新的对象已经产生,但是从程序的角度来看,对象创建才刚刚开始——构造函数。即Class文件的**()**方法还未执行,这时对象的所有字段为默认的零值,对象需要的其他信息还未按照约预定的意图构造好。等执行()方法之后,一个真正的对象才算完全构造出来。

3、对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三部分对象头(Header)实例数据(Instance Data)对齐填充(Padding)

3.1、对象头

HotSpot虚拟机对象头部分主要包括两部分数据:一个是Mark Word(标记字段),另一个就是类型指针(Klass Point)。其中如果Java对象是一个数组的话,那么还需要一块内存用来存储数组的长度。虚拟机介意通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息确定数组的大小。对象头的结构如下图:

长度内容说明
32/64位对象头存储hashCoe或者锁信息等
32/64位类型指针存储到对象类型数据的指针
32/32位数组长度数组的长度

3.1.1、Mark Word

Mark Word 用来存储对象自身运行时的数据,如哈希码GC分代年龄锁状态标志线程持有的锁偏向锁线程ID偏向时间戳等。这部分的数据在32位和64位的虚拟机(未开启压缩指针)中的长度分别为32位和64位。官方称之为“Mark Word”。在32位的虚拟机中Mark Word结构如下:
image.png
image.png

3.1.2、类型指针

类型指针即对象指向它的类型元数据的指针,Java通过这个指针来确定这个对象属于哪个类的实例。并不是所有虚拟机的实现都必须在对象数据上保留类型指针。

3.2、实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的还是子类中定义的字段都必须记录下来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义的顺序有关。HotSpot虚拟机默认的分配顺序为:longs/doubles、ints、shorts/chars、bytes/booleans、oops,从以上默认分配策略中可以看到,相同宽度的字段总是被分配到一起。在满足这个条件的情况下,在父类中定义的字段会出现子类之前。

3.3、对齐填充

对齐填充并不是必然存在的,也没有特别的含义,只是起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,话句话说就是任何对象的大小都必须是8字节的整数倍

4、对象的访问定位

创建对象自然是为了访问对象,我们Java程序通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置。因此对象的访问方式也是有虚拟机进行实现。主流的访问方式主要有:使用句柄直接指针

  • 使用句柄:如果使用句柄访问的话,Java堆中将可能会划分出一块内存作为句柄池reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息
  • 直接指针:如果使用直接指针的方式的话,Java堆中对象的内存布局就必须考虑如何放置访问类型的的相关信息,reference中存储的直接就是对象的地址,如果只是访问对象本身的话,就不要多一次间接访问的开销。

image.png
通过句柄访问对象
image.png
直接指针访问对象
以上两种访问方式各有优势。使用句柄的方式的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变
使用指针访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此此类开销积小成多也是一项可观的成本。HotSpot虚拟机主要使用的就是此种方式访问对象

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/319518.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue实现滚动条联动(一个滚动条控制两个或多个)

两个表格需要进行比对,两个表格是互相独立的,如果滚动条不能同步,用户就要操作两个两次,体验不是太好,如下图: 因此想使两个滚动条同步,思路如下: 给两个表格定义ref(便…

环形链表的判断方法与原理证明

(题目来源:力扣) 一.判读一个链表是否是环形链表 题目: 解答: 方法:快慢指针法 内容:分别定义快慢指针(fast和slow),快指针一次走两步,慢指…

机器学习的指标评价

之前在学校的小发明制作中,在终期答辩的时候,虽然整个项目的流程都答的很流畅。 在老师提问的过程中,当老师问我recall,precision,accuracy等指标是如何计算的,又能够表示模型的哪方面指标做得好。我听到这个问题的时候&#xff…

如何选购骨传导耳机?精选五大拔尖宝藏骨传导耳机,闭眼入也不踩雷!

尽管目前市面上的骨传导耳机热度非常高,一度成为当下最热门的耳机款式,但作为有着资深工作经验的数码测评师,我仍然要提醒大家:在选择骨传导耳机的时候,不要盲目选择网红品牌,因为市场上的许多骨传导耳机过…

用LM Studio搭建微软的PHI3小型语言模型

什么是 Microsoft Phi-3 小语言模型? 微软Phi-3 模型是目前功能最强大、最具成本效益的小型语言模型 (SLM),在各种语言、推理、编码和数学基准测试中优于相同大小和更高大小的模型。此版本扩展了客户高质量模型的选择范围&#x…

golang判断通道chan是否关闭的2种方式

chan通道在go语言的办法编程中使用频繁,我们可以通过以下2种方式来判断channel通道是否已经关闭,1是使用 for range循环,另外是通过 for循环中if 简短语句的 逗号 ok 模式来判断。 示例代码如下: //方式1 通过for range形式判断…

现代循环神经网络(GRU、LSTM)(Pytorch 14)

一 简介 前一章中我们介绍了循环神经网络的基础知识,这种网络 可以更好地处理序列数据。我们在文本数据上实现 了基于循环神经网络的语言模型,但是对于当今各种各样的序列学习问题,这些技术可能并不够用。 例如,循环神经网络在…

centos7 openresty lua 自适应webp和缩放图片

目录 背景效果图准备安装cwebp等命令,转换文件格式安装ImageMagick,压缩文件下载Lua API 操控ImageMagick的依赖包 代码参考 背景 缩小图片体积,提升加载速度,节省流量。 效果图 参数格式 : ?image_processformat,…

PyVista 3D数据可视化 Python 库 简介 含源码

Pyvista是一个用于科学可视化和分析的Python库 ;我认为它适合做一些网格数据的处理; 它封装了VTK(Visualization Toolkit)之上,提供了一些高级接口, 3D数据可视化变得更加简单和易用。 1.安装 pyvista&…

蓝桥杯-路径之谜

题目描述 小明冒充X星球的骑士,进入了一个奇怪的城堡。城堡里面什么都没有,只有方形石头铺成的地面。 假设城堡的地面时n*n个方格。如下图所示。 按习俗,骑士要从西北角走到东南角。可以横向或者纵向移动,但是不能斜着走&#x…

【设计模式】函数式编程范式工厂模式(Factory Method Pattern)

目录标题 定义函数式接口函数式接口实现类工厂类封装实际应用总结 定义函数式接口 ISellIPad.java /*** 定义一个函数式接口* param <T>*/ FunctionalInterface public interface ISellIPad<T> {T getSellIPadInfo();}函数式接口实现类 HuaWeiSellIPad.java pu…

JAVA系列 小白入门参考资料 接口

目录 接口 接口的概念 语法 接口使用 接口实现用例 接口特性 实现多个接口和实现用例 接口间的继承 接口 接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本上的 USB 口&#xff0c;电源插座等。 电脑的 USB 口上&am…

初识C语言——第九天

ASCII定义 在 C 语言中&#xff0c;每个字符都对应一个 ASCII 码。ASCII 码是一个字符集&#xff0c;它定义了许多常用的字符对应的数字编码。这些编码可以表示为整数&#xff0c;也可以表示为字符类型。在 C 语言中&#xff0c;字符类型被定义为一个整数类型&#xff0c;它占…

WPF之绑定验证(错误模板使用)

1&#xff0c;前言&#xff1a; 默认情况下&#xff0c;WPF XAML 中使用的绑定并未开启绑定验证&#xff0c;这样导致用户在UI上对绑定的属性进行赋值时即使因不符合规范内部已抛出异常&#xff08;此情况仅限WPF中的数据绑定操作&#xff09;&#xff0c;也被程序默认忽略&…

C#调用skiasharp操作并绘制图片

之前学习ViewFaceCore时采用Panel控件和GDI将图片及识别出的人脸方框和关键点绘制出来&#xff0c;本文将其修改为基于SKControl和SKCanvas实现相同的显示效果并支持保存为本地图片。   新建Winform项目&#xff0c;在Nuget包管理器中搜索并安装一下SkiaSharp和ViewFaceCore…

AD单通道/多通道

1.AD单通道&#xff08;单次转换&#xff0c;非扫描模式&#xff09; 1.1 接线图 1.2 AD.c #include "stm32f10x.h" // Device headervoid AD_Init(void) {RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA时钟RCC_APB2PeriphCl…

node.js 解析post请求 方法二

前提&#xff1a;以前面发的node.js解析post请求方法一为模板&#xff0c;具体见 http://t.csdnimg.cn/ABaIn 此文我们运用第二种方法&#xff1a;使用第三方模块formidable对post请求进行解析。 1》代码难点 *** 在Node.js中使用formidable模块来解析POST请求主要涉及到处理…

OpenWRT部署Zerotier虚拟局域网实现内网穿透

前言 细心的小伙伴肯定已经发现了&#xff1a;电脑上部署了Zerotier&#xff0c;如果路由器也部署了OpenWRT&#xff0c;那是否能远程访问呢&#xff1f; 答案是肯定的。 OpenWRT部署Zerotier有啥好处&#xff1f; 那好处必须多&#xff0c;其中的一个便是在外远程控制家里…

初识MVC

初识MVC 理论部分 今天第一次学MVC&#xff0c;拿到一个练手项目。现在来记录一下学习过程。 项目的背景就是个学生管理系统。我只做后端。 从大的来说MVC将应用程序分为三个主要组件&#xff08;部分&#xff09;&#xff1a; 模型&#xff08;Model&#xff09;是应用程序…

sql 中having和where区别

where 是用于筛选表中满足条件的行&#xff0c;不可以和聚类函数一起使用 having 是用于筛选满足条件的组 &#xff0c;可与聚合函数一起使用 所以having语句中不能使用select中定义的名字