Jvm笔记总结(一):Java内存区域概况

Java内存区域概况

PS : 本文乃学习整理参考而来 ,目录参考 [ Jvm系列目录 ]

        Java虚拟机在执行Java程序过程中,会把所管理的内存划分为几个不同的数据区域(程序计数器虚拟机栈方法区本地方法栈),统称为运行时数据区域。如下图:

运行时数据区


1. 程序计数器(Program Counter Register):

        当前线程执行代码的行号指示器。字节码解释器通过改变程序计数器的值选取下一条要执行的字节码指令。由于Java虚拟机多线程是通过随机切换线程分配处理器执行时间的方式实现的,所以在任一确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。为了切换后能正确恢复到原本的执行位置,因此每条线程都需要独立的程序计数器,所以这类区域为**“线程私有” **的内存。

Ps:此区域是唯一在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。


2. 虚拟机栈(Java Virtual Machine Stacks):

        与程序计数器一样,Java虚拟机栈也是“线程私有”的,生命周期同线程相同。虚拟机栈描述的是Java方法执行的内存模型每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈动态链接方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。程序员说的栈内存,一般指虚拟机栈,或者说是虚拟机栈中的局部变量表局部变量表存放了编译器可知的各种基本数据类型(8种基本数据类型)、对象引用(reference类型,指向堆中对象起始地址的引用指针,也可能是指向代表对象的句柄或对象想关的位置),returnAddress类型基本废用。
       其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余基本类型只占用1个。局部变量表所需内存空间编译期完成分配。当进入一个方法时,这个方法需要在帧中分配多大的变量空间完全确定的,在方法运行期间不会改变局部变量表大小

Ps:在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够内存,会抛OutOfMemoryError异常。


3. 堆(Java Heap):

        堆是Java虚拟机所管理的内存中最大的一块。也是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例几乎所有的对象实例都在这里分配内存。在Java虚拟机规范中描述是:所有的对象以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟(可能导致部分对象可在栈上分配,而随栈帧出栈随即回收内存),栈上分配对象成为了可能性,所有对象都分配在堆上也不是那么“绝对”了。
    Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为**“GC”堆**(Garbage Collected Heap)。现在内存回收基本都采用分代收集算法,所以Java可以细分为:新生代老年代再细分新生代可分为:EdenFrom SurvivorTo Survivor,比例为8:1:1。根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的空间中,只要逻辑连续即可。

PS:如果在堆中没有内存完成实例分配,并且堆无法扩展时,将会抛出OutOfMemoryError异常.


4. 方法区(Method Area):

        与Java堆一样,是各个线程共享的区域,他用于存储已经被虚拟机加载类信息常量静态变量即时编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的是为了与堆区分开。对于习惯在HotSpot虚拟机上开发、部署的开发者来说,很多人把方法区称为“永久代(Permanent Generation)”。本质上,两者不等价。仅仅是因为HotSpot虚拟机的设计团队把GC分代收集扩展至方法区,或者说用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。对于其他虚拟机**(JRockit、J9)等,不存在永久代概念**。由于使用“永久代”来实现方法区可能导致内存溢出,所以HotSpot虚拟机官方发布的信息,现在也有放弃永久代,改为采用Native Memory来实现方法区的规划。现目前发布的1.7的HotSpot虚拟机中,已经把原本放在永久代(方法区)的字符串常量池移出

Ps:方法区可选择不进行垃圾收集,垃圾收集在该区域也较少出现,但并非数据进入了方法区就不会被回收,该区域回收主要针对常量池回收类型的卸载。回收的可能性是存在的。当方法区无法满足内存分配是将抛出OutOfMemoryError异常。


5. 运行时常量池(Runtime Constant Pool):

        是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池(Constant Pool Tbale),用于存放编译期生成的各种字面量符号引用这部分内容在类加载后,进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也是就并非预置入Class文件中的常量池的内容才能进入方法区运行时常量池运行期间也可能将新的常量放入池中,这种被开发人员利用得多的便是String类的intern()方法因为是方法区一部分,所以会抛出OutOfMemoryError [ intern()的用法 ]


6. 本地方法栈(Native Method Stack):

        与虚拟机栈所发挥的作用相同,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务。Java虚拟机规范并未强制规定其具体实现,如HotSpot虚拟机把虚拟机栈和本地方法栈合二为一

Ps:本地方法栈和虚拟机栈一样会抛出StackOverflowError异常。


6. 直接内存:

        并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域。在Jdk1.4中新加入了NIO
(New Input/OutPut)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式。


可通过如下参数设置内存空间

-Xms设置堆的最小空间大小。

-Xmx设置堆的最大空间大小。

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

-XX:PermSize设置永久代最小空间大小。

-XX:MaxPermSize设置永久代最大空间大小。

-Xss设置每个线程的堆栈大小。                

一张非常经典的Java内存区域详图。 引用博主图片地址

这里写图片描述

引用博主原文


HotSpot虚拟机对象

1. 对象的创建:当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用是否已被加载解析和初始化过如果没有则会进行类加载过程。在类加载检查通过后,虚拟机给对象分配内存,对象所需的内存大小在类加载完成后便可确定内存分配方式分为两种一是“指针碰撞”,在Java堆中内存是绝对规整时(已用内存和空闲内存各占一边),通过改变分界点的指针为对象分配内存。二是“空闲列表”,如果堆中内存不是规整的,虚拟机就必须维护一个列表,记录哪些内存块是可用的,为对象分配足够内存后,更新列表上的记录。

PS:内存规整:Java堆内存是否规整取决定于垃圾收集器是否带有压缩整理功能决定,因此在使用Serial、ParNew等带有Compact过程的收集器时,采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表

2. 对象的内存分部:在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机的对象的对象头包括两个部分信息第一部分用于存储对象自身的运行时数据,如哈希码GC分代年龄锁状态标志线程持有的锁偏向线程ID偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为**“Mark Word”**。对象需要存储的运行时数据很多,往往会超过32位64位的Bitmap结构的限度,但是对象头信息是与对象自身定义数据无关的额外存储成本,考虑到虚拟机的控件效率,Mark Word 被设计成一个非固定的数据结构以便存储尽量多的信息,换句话说对象头是一个大于32位的不固定的数据结构。在32位HotSpot虚拟机中,如果对象处于未被锁定状态下,25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。其他状态时对象存储内容如图:

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID、偏向时间戳、对象分代年龄01可偏向

对象头另外一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。并不是所有虚拟机实现都会在对象数据上保留类型指针,换句话说,查找对象元数据并不一定要经过对象本身。实例数据(Instance Data)部分是对象真正存储的有效信息,也是在程序代码中定义的各种类型字段内容。对齐填充(Padding)部分并不是必然存在的,也没有特别的含义,它仅仅起到占位符作用。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍。对象头部分正好是8字节的整数倍。因此当对象实例数据没有对齐时就需要通过对齐填充来补全。换言之,在未开启指针压缩情况下,32位的HotSpot中一个类对象极端最小内存为:4字节(对象头)+4字节(实例数据+填充)=8字节,** 64位的HotSpot中为**:8字节(对象头) + 8字节(实例数据+填充)=16字节现在主流大部分是64位虚拟机

3. 对象的访问定位:Java程序需要通过栈上reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用并没有定义这个引用应该通过何种方式去定位、访问堆中的位置,所以对象访问方式也是取决于虚拟机实现的。目前何种方式去定位、访问堆中的位置有使用句柄直接指针两种。如图:图片引用自 [ 引用博主地址 ]

①.如果使用句柄,那么Java堆中会划分出一块内存来作为句柄池reference中存储的就是对象的句柄地址,而句柄中包含对象实例数据对象类型数据(方法区的Class对象的地址)各自的具体地址信息
句柄

②.如果使用直接指针,那么Java堆中对象的布局必须考虑如何放置访问类型数据的相关信息(方法区的Class对象的地址),在HotSpot中的实现是将地址信息放入对象头的第二部分中,也即上文的**“对象头的另一部分”,而reference中存储的就是对象地址**。
直接指针

使用句柄的好处:在对象被移动时(垃圾收集移动对象)时只会改变句柄中的实例数据指针,而reference不需要修改。有人比作他为二级指针

使用直接指针的好处好处就是快,节省了一次指针定位的时间开销。就HotSpot而言,他使用该方式进行对象访问。有人比作他为一级指针


本文部分图片出自:
[ 引用博主图片地址 ]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值