200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > Java虚拟机内存管理

Java虚拟机内存管理

时间:2022-03-12 23:16:47

相关推荐

Java虚拟机内存管理

简要介绍JVM中内存管理机制。

C、 C++语言依赖开发人员管理内存,提高灵活性,但也带来了内存泄漏(OutOfMemeory,内存泄漏指是指由于疏忽或错误造成程序未能释放已经不再使用的内存。主要指指针跑飞)和内存溢出(StackOverflow,内存溢出是指系统在为某段执行指令(程序)分配内存时,发现剩余内存不足并抛出错误。主要指栈溢出)的隐患。严格依赖于程序员的对内存的认知水平。

1.屏蔽开发人员对内存的直接操作,2.保证内存的安全使用,JVM承担了这部分职责。

尽管JVM自动内存管理机制已经很完善,但是仍可能存在内存泄漏或内存溢出的问题。为增加在这类问题出现后的处理能力,有必要熟悉JVM的内存管理机制。

一、 概述

Java 虚拟机运行时数据区划分为:堆、方法区、程序计数器、虚拟机栈、本地方法栈。其概述图如下:

对于C语言内存区,可以发现JVM在C语言的基础上进行了调整,以配合Java语言面向对象、支持原生调用、支持多线程等特性。

Java 堆(Java Heap)

Java堆是供绝大多数类实例和数组对象分配内存的区域。(神域的小部分类实例分配在方法区–常量引用的对象)

Java堆是各个线程共享的运行时内存区域。

Java堆是垃圾收集器(GC)管理的主要对象,因此也被称为“GC堆”(Garbage Collected Heap,注意,这里未翻译成“垃圾堆”)。

JVM规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续即可。(物理可不连续,逻辑必须连续)

方法区(Method Area)

方法区用于存储已被虚拟机加载的类的结构信息运行时常量池编译后的代码等。

方法区是所有线程共享的内存区域,与堆类似。

方法区存储类数据,Java堆存储对象数据。类加载时主要和方法区交互,对象分配时主要和Java堆交互。

运行时常量池

运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到运行时常量池中。

同时,运行时的常量也会放到这里。如String类型的intern()方法。

运行时常量池与C语言的“数据区”类似。

程序计数器(Pprogram Counter Register)

程序计数器可看作当前线程所执行的字节码的行号指示器。所谓字节码指示器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

每个线程都有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储,这类内存区域为“线程私有”内存。

Java 栈(Java Stack)

Java 栈描述Java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧(Stack Frame)。栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等方法执行相关信息。方法的调用和完成过程,对应一个栈帧在JVM栈中从入栈到出栈的过程。

Java 栈与程序计数器一样,也是线程私有,它的生命周期与线程相同。

本地方法栈(Native Method Stack)

JVM根据方法的实现语言,将方法分为两类:Java方法和Native方法。其中Native方法特指C/C++方法。Java方法在Java 栈中存储、调用。Native方法则在本地方法栈中存储、调用。

本地方法栈与Java 栈一样,也是线程私有,它的生命周期与线程相同。

直接内存

直接内存不是虚拟机运行时数据区的一部分。在JDK 1.4 新加入NIO(New Input/Output)类时,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式。其使用Native函数库直接分配内存,然后通过一个存储在Java对立面的DirectByteBuffer对象作为这块内存的引用进行操作。

直接内存不受Java 堆的大小影响,但是会受到本机总内存大大小及处理器寻址空间的限制。

二、 HotSpot对象内存处理

作为最流行的虚拟机,研究HotSpot对象在内存的处理过程具有实用价值。虚拟机中对象的处理主要三个部分:对象创建、对象布局、对象访问。

1. 对象创建

Java中对象创建,在Java语言层面,仅仅是一个new关键字。而当虚拟机遇到一条new指令时,会进行一序列对象创建的操作。HotSpot虚拟机中对象创建主要分为五步:(1)加载类;(2)分配内存;(3)初始化零值;(4)设置对象头;(5)执行Init方法。参考

(1)加载类

当虚拟机遇到一条new指令时,首先会去检查该指令的参数能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、初始化过,如果没有,则必须先执行相应的类加载过程。类加载过程可参考类加载机制一文。

(2)分配内存

类加载检查完成后,虚拟机将为新对象分配内存空间。该过程就是在堆中划分一小部分确定大小的空间,用于存储对象信息。其中分配方式有以下两种:指针碰撞空闲列表。指针碰撞是指在内存绝对规整的前提下,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。而分配内存就是将指针向空闲空间挪动一段与对象大小相等的距离。空闲列表则假定内存不规整,无法简单地进行指针碰撞,此时必须维护一个列表,记录可用内存块。内存是否规整,与垃圾收集器是否带有压缩整理功能决定。

另一个需要考虑的问题就是并发场景下内存分配。在并发场景下,可能存在给对象A分配内存,指针未及时修改,对象B又同时使用原来的指针来分配内存的情况。解决这个问题的方案有两种:一种是“CAS+失败重试”保证更新操作的原子性;另一种是优先使用TLAB(Thread Local Allocation Buffer,线程本地分配缓存),TLAB是指每个线程在Java堆中预先分配的一小块内存。

(3)初始化零值

内存分配完毕后,虚拟机将该对象分配的内存空间全部设置初始值零(不包含对象头部分),该操作可以保证对象的实例字段在代码中即使不赋予初始值也可以直接使用。程序能访问到这些字段的数据类型所对应的零值(不包括对象头)。

(4)设置对象头

初始化零值完成后,虚拟机将对象的一些必要信息存放在对象头中,这些信息包括:例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

(5)执行Init方法

虚拟机完成一个对象的创建后,对于java程序而言,对于该对象的一些定制的内容还未进行,方法中包含了程序员的定制需求和意愿,执行完init方法后,对象完成了初始化,此时才是一个可用对象。

2. 对象布局

虚拟机中,对象在内存中的存储包括三个部分:对象头、实例数据和对齐填充。

对象头

对象头包括两部分信息,第一部分用于存储对象自身运行时数据(哈希码、GC分代年龄、锁状态标志等等);另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据

实例数据是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充

该部分不是必然存在的,仅起占位作用,可类比C语言的结构体的对齐特性。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3. 对象访问

Java程序通过java栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有两种:(1)使用句柄;(2)使用直接指针。

两种访问方式各有优势:使用句柄访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时(垃圾回收时,移动对象是非常普遍的行为),只需改变句柄中的实例数据的指针,而reference本身不需要调整。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

HotSpot使用第二种方式进行对象访问,但从整个软件开发的范围开看,使用句柄访问的方式也很常见。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。