JVM系列-内存划分

Java程序不需要手动的申请和释放内存,所有的这些操作都是JVM来完成的。这样会在一定程度上简化我们的开发工作,而且很少出现内存泄露和内存溢出的问题。但是,我们也需要了解JVM内部是如何分配和释放内存的,知道了原理,才能写出更优的代码,才能在出现问题时快速的定位和排查。

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这就是我们常说的内存划分。注意这里和Java内存模型不是一回事,Java内存模型是Java虚拟机规范中定义的,用来屏蔽掉各种硬件和操作系统的访问差异,让Java程序在不同平台都能有相同的访问效果。Java内存模型涉及的知识点是工作内存,主内存,happens-before原则等内容,这个以后的文章会详细说明。

接下来继续说内存划分,先看如下的图片:
在这里插入图片描述
我们一般会把空间划分为程序计数器,栈,堆,方法区这些。下面详细说下不同区域的作用。

程序计数器:
这个是线程私有的一片区域,可以看成是当前线程所执行字节码的行号指示器。在任意时刻,一个线程最多只能执行一个方法。如果当前线程正在执行一个Java方法,则程序计数器记录的是字节码指令地址,如果当前线程正在执行的是Native方法,则程序计数器记录的内容为空。
什么是Native方法?是JVM中的一些本地方法,我们有时候看源码的时候,一直往内部追踪,就会看到一些方法被标明为native的方法,而且还看不到方法体。这种方法一般都是基于C/C++实现的。就是Native方法。

程序计数器有什么作用呢?其实最常见的场景还是在多线程执行时,因为一个处理器或者一个内核只能处理一个线程,不同线程之前会存在切换,当这个线程切换回来执行时,需要从之前暂停的位置继续执行,因为程序计数器就存储了这个要执行的字节码指令地址,所以线程切换回来之后,就可以知道正确的执行地址。这也是程序计数器需要是线程私有空间的原因。

栈:
栈还可以分为虚拟机栈和本地方法栈,虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行native方法服务的。

栈帧:
对于虚拟机栈来说,入栈和出栈的元素,就是栈帧。当进行函数调用时,就会有一个对应的栈帧入栈,当函数调用结束时,就对应栈帧的出栈。
栈帧中会存储方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。对于一个方法的入参,以及方法内部定义的局部变量,都会存到局部变量表里。方法内部的指令在执行过程中,就是操作数栈的不断入栈和出栈的过程。动态连接,就是记录了当前栈帧所属的方法在运行时常量池中的引用,为了支持方法调用过程中的动态连接。方法的返回地址,就是记录的方法退出后需要返回的被调用的位置,这样才可以让程序继续执行。如果方法是正常返回,那就返回的是程序计数器里存放的指令地址,如果方法是异常退出,则需要根据异常处理表来返回地址。

堆:
堆是JVM内存管理中最大的一块,是线程共享的一块区域。我们new出来的对象,一般而言都是分配到堆上的空间。这个空间也是垃圾回收的主要作用区域。对于分代收集的垃圾回收算法,就会把堆再细分为新生代和老年代。
Java虚拟机规范规定了堆可以处于物理上不连续的内存空间,只要是逻辑上是连续的即可。

方法区:
方法区也是线程共享的一块区域,存放的是类信息,常量,静态变量,即时编译后的代码。关于方法区,比较适合和类加载一起学习。类加载机制,会把我们的编译后的.class文件,加载到虚拟机中,放到方法区。

除了上面说的那些,其实还有个直接内存,这个内存不属于JVM管理的区域。我们在NIO或其它场景中,是可以直接通过native方法来分配空间来使用的,使用这些空间可以避免在Java堆和Native堆中复制数据,所以性能比较好。但是这些内存不会被JVM管理,同时也会受限于服务器的总内存限制。所以有时候,会发现JVM经常GC,但是排查后发现堆的空间使用不大,那就有可能是直接内存的原因。

对象的内存布局
说完了JVM的内存划分,可以顺便再简单说下对象的内存布局,因为JVM划分内存区域,无非也就是为了更好的管理在JVM中存储的数据和对象。既然要存对象,就需要了解下对象在JVM里是怎么存放的。

对象在JVM里,可以分成三部分,对象头,实例数据和对齐填充。

对象头:可以分为Mark Word和类型指针。
其中,Mark Word用来存储这个对象运行时的一些数据,比如HashCode,GC分代的年龄(垃圾回收的时候可以判断当前对象的年龄),锁状态标志,线程持有的锁,偏向线程的ID等信息。
类型指针,用来指向这个对象所属的类的元数据地址,这样就可以判断当前实例是属于哪个类的实例。不过,这个类型指针并不是必须的,跟不同JVM的实现有关。

实例数据:存放这个对象实际的信息。

对齐填充:对于Hotspot虚拟机来说,要求对象的起始地址必须要是8字节的倍数,也就是对象的大小必须要是8字节的整数倍。对象头的大小是8字节的整数倍,但是实例数据的大小不一定,所以如果实例数据大小不是8字节整数倍的时候,就需要对齐填充,起到一个占位符的作用。

对象的定位:
对象一般是存储在堆上的,栈中会有个变量存储当前对象的引用,通过这个引用来定位到实际的对象。

不同JVM的实现,定位方式也不同,主流的有两种方式,第一种是句柄,第二种是直接指针

在这里插入图片描述
对于句柄访问,会有一个句柄池,这个句柄池在堆空间中。句柄就会包含两个指针,一个指向堆中的实例数据,还有一个指向方法区中对象的类型数据。

在这里插入图片描述

对于直接指针方式,栈中的引用直接指向堆上的对象,但是对象中就需要有个指针,来指向方法区中该对象类型的数据。

对于这两种方式来说,各有优缺点。
句柄的这种方式,存储的句柄地址是稳定的,如果对象在垃圾回收的过程中被移动了,也只需要改一下句柄内部存储实例对象的地址。但是这种方式定位对象需要进行两次定位,先根据引用找到句柄,再根据句柄中的引用找到实例对象。
对于直接指针的方式,优点在于查找对象的速度比较快,因为只需要一次定位就可以找到实例对象。
对于常用的Hotspot虚拟机而言,采用的是直接指针的方式进行对象访问。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值