java 虚拟机内存管理_《Java虚拟机》之内存管理机制

悠悠 2022-11-08 15:27 378阅读 0赞

《Java虚拟机》之内存管理机制

为什么Java程序员要理解jvm原理

Java作为一门面向对象的编程语言,与C++之间有一个明显的隔离区,即为内存动态分配和垃圾回收机制。对于我们Java程序员来说,相对于C++里面的需要为每一个new操作去手动配写delete/free代码,得益于虚拟机自动内存管理机制,我们完全可以省去这一繁杂的步骤。当然了,这个有利必有弊。由虚拟机自动管理内存,表面看来不错,但是也正是由于我们把内存控制的权限交给了虚拟机,一旦发生内存泄漏或是内存溢出,我们将无从下手解决。由此看来,理解虚拟机的内存管理机制势在必行了!

运行数据区域

Java虚拟机在执行程序时会将其管理的内存划分为若干个不同的数据区域,其各自之间都有特殊的用途,大致可以划分为以下五大区域:

程序计数器

程序计数器,顾名思义,它是作为一块较小的内存空间,充当着当前线程所执行的字节码的行号指示器。其工作原理是通过改变这个计数器的值来选取下一条需要执行的字节码指令,从而实现分支,循环,跳转,异常处理,线程恢复等等基础功能。

在Java虚拟机当中,多线程是通过线程轮流切换并分配处理器时间的方式啦,即轮流分配时间片的方式来实现的。在任何一个确切的时刻,都只是执行一条线程中的指令。而我们所说的多线程,是仅从表面上得来的结论,实际上时间片是以毫秒级来分配的,在短短的一秒内,处理器已经将多个时间片分配给多个不同的线程,给人的感觉是处理器在这一时刻内同时处理了多个线程的假象。为了保证多个线程在切换后能够恢复到之前的正确执行位置,每一条线程都需要有一个独立的计数器来保证各个线程之间互不影响,独立存储,也就是说这些内存都是“线程私有的”。

2.java虚拟机栈

与程序计数器相同,虚拟机栈也是线程私有的,其生命周期与线程相同,伴线程生,版线程死。虚拟机栈描述的是Java方法执行的内存模型:在每一个方法被执行时都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

在Java虚拟机规范中,表明这一区域有两种异常状态:

StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度

OutOfMemoryError:在基于Java虚拟机可以实现动态扩展的前提下,如果扩展时无法申请到足够的内存,就会出现这一情况。

3.本地方法栈

本地方法栈与虚拟机栈的功能大致相当,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。同样的,它也会出现 StackOverflowError和 OutOfMemoryError两种异常状态。

4.Java堆

Java堆,通俗的来说,就是用于存放对象实例。Java堆是被所有的线程共享的一块内存区域,几乎所有的对象实例及数组都会在这里分配内存。根据虚拟机规范的规定,Java堆如同磁盘空间一样,可以处于物理上不连续的内存空间,主要逻辑上是连续的即可。

对于垃圾收集器来说,Java堆是其主要作用对象,其将Java堆划分为新生代,老年代。如果在堆中没有内存完成实例分配,而且堆也无法扩展的时候,将会抛出 OutOfMemoryError异常。

5.方法区

方法区同Java堆一样,都是各个线程共享的内存区域,它主要用于存放已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译过后的代码等数据,也称为“Non-Heap”.

6.运行时常量池

运行时常量池时方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这一部分内容将在磊加载到方法区的运行时常量池中存放。相对于Class文件常量池,运行时常量池的一重要特征时具备了动态性。Java语言并不要求常量一定主要编译期才能产生,也就是非预置入Class文件常量池的内容才可以进入方法区里的运行期常量池,运行期间也可以将新的常量放入池中,其方法为String类的intern()方法。

对象的创建

作为一门面向对象的编程语言,Java显然是重点关注对象。在Java的语言层面上看,创建一个对象,仅是调用关键字new就可以实现,深入到虚拟机中,显然不会这么简单。

当虚拟机在编译过程中遇到一条new指令,首先将去检查这条指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,解析和初始化。如果没有,那么必须先执行相应的类加载过程

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期包括

加载loading

验证verification

准备Preparation

解析Resolution

初始化Initialization

使用Using

卸载Unloading

在类加载检查通过后,虚拟机将会为新生的对象分配内存,对象所需内存的大小在类加载完成过后就可以完全确定了。为对象分配内存,实际上等同于把一块大小确定的内存从Java堆里面划分出来,作为这个新生对象的私有所属。具体内存划分又可以分为两种不同的方式

(1) 指针碰撞

假设Java堆中的内存是绝对规整的,将整个内存划分为已使用的和未使用的(空闲可用的)两个板块,两者界限间用一个指针作为分界点的指示器。所谓的指针碰撞就是将作为指示器的指针向着空闲内存那一块板块挪动一段与新生对象所需内存大小相当的距离,指针挪动出来的那一段内存就作为新生对象的内存,即实现了内存分配

(2)空闲列表

假设Java堆的内存不是绝对规整的,出现已使用的内存与未使用的内存相互交叉存在的现象。这时显然不能实现“指针碰撞”了。为了解决这个问题,虚拟机将会为内存块编号并维护一张列表,上面记录了哪些内存卡已经使用,哪些内存块尚未使用的情况,在需要分配内存的情况下,将从列表中寻到到一块足够大的空间划分给新生对象实例,并且更新列表上的记录。

对象的内存布局

在HotSpot虚拟机中, 对象在内存中的存储的布局可以划分为三块区域:

(1)对象头(MarkWord)

(2)实例数据(Instance Data):是对象真正存储的有效信息

(3)对齐填充(Padding)

对象头:可以分为两个部分,(1)第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。(2)另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机将通过这个指针来确定这个对象是哪个类的实例。

存储内容

标志位

状态

对象哈希码,对象分代年龄

01

未锁定

指向锁记录的指针

00

轻量级锁定

指向重量级锁的指定

10

膨胀(重量级锁定)

空,不需要记录信息

11

GC标记

偏向线程ID,偏向时间戳,对象分代年龄

01

可偏向

对象的访问定位

建立对象说到底还是为了使用它,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。因为虚拟机规范中规定了reference类型只有一个指向对象的引用,而没有定义这个引用该通过什么样的方式区定位和访问堆中的对象的具体位置。一般的,我们常用两种方式:

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

87ddda4107af05505898d4ee3ae4c1a3.png

(2)直接指针:reference中存储 的直接就是对象地址。

c2dd259435b9e0a46ddaa89eff6c01cc.png

两相比较,各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要改动。使用直接指针最大的好处是速度更快,节省了一次指针定位的时间开销。

参考《深入理解Java虚拟机》

争渡争渡,惊起一滩欧鹭。

欲知后事如何,请见下回分解

发表评论

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

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

相关阅读

    相关 Java虚拟内存管理

    对于Java程序员来说,在虚拟机自动内存管理机制帮助下,不需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和溢出方面的问题。一旦出现内存泄漏和溢出

    相关 java虚拟内存管理

    java内存区域和内存溢出 HotSpot VM是SunJDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的java虚拟机。 如果一段java方法被调用次数达