<span>JVM -- Java内存区域</span>

Java内存区域

这里的Java的内存区域指的是,运行时数据区域

Java好处有很多,在我看来,java最大的好处是不用手动管理内存了。Java使用虚拟机自动管理,自动回收内存,很少会出现内存泄漏内存溢出的问题。但是,如果虚拟机一旦出现了这些问题,如果对JVM堆内存的使用有所了解,那么排查这个问题会相对容易一些。

Java虚拟机在执行程序过程中,会把它管理的内存划分成若干个不同的数据区域,而且每个区域都有自己的用途。比如,方法区,虚拟机栈,本地方法栈,堆和程序计数器。

内存区域(Memeory Area)

如图,上面的灰***域是线程共享的内存区域,白***域是线程私有的内存区域。

矩形区域会有OOM的情况,可见程序计数器是唯一一个没有任何OOM情况的区域。

程序计数器(Program Counter )

  • 线程私有:每条线程都需要有一个独立的程序计数器

  • 内存空间较小

  • 无OutOfMemoryError情况

  • 计数器记录内容:

    • 正在执行一个Java方法:

      计数器记录的是正在执行的虚拟机字节码指令的地址

    • 正在执行一个Native方法:

      计数器值为空(Undefined)

Java虚拟机栈(Java Virtual Machine Stack)

  • 线程私有
  • 生命周期与线程相同
  • 抛出异常:OutOfMemoryErrorStackOverFlowError

虚拟机栈描述的是一个方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)[1]用于储存局部变量表,操作数栈、动态链接、方法出口等等信息。每一个方法调用直至完成的过程,就对应这个一个栈帧在虚拟机中入栈到出栈的过程。

现在,程序员口中常说的栈内存(Stack)一般是指虚拟机栈,或者说着重指的是局部变量表部分。局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型[2])和returnAddress类型的数据[3]

如果线程请求的栈深度大于虚拟机允许的深度,则抛出StackOverFlowError异常

如果动态扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常

本地方法栈(Native Method Stack)

  • 本地方法栈则为虚拟机是用到的Native方法服务

  • 和java虚拟机栈发挥的作用十分相似,性质也基本相同

  • 有一些虚拟机比如HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一

  • 抛出异常:OutOfMemoryErrorStackOverFlowError

Java堆(Heap)

  • 线程共享

  • 唯一目的:存放对象实例

    所有的对象实例以及数组要在堆上分配

  • Java堆是垃圾收集管理的主要区域,因此很多时候也称为GC堆Garbage Collectoin Heap

  • Java堆可以处于物理上不连续的内存空间中,只要逻辑连续的即可。

  • 抛出异常:OutOfMemoryError

  • 可扩展参数:

    • -Xmx:设置你的应用程序(不是JVM)能够使用的最大内存数
    • -Xms:设置程序初始化的时候内存栈的大小

方法区(Method Area)

  • 线程共享

  • 存储内容:加载类的信息、常量池[4]、静态变量

运行时常量池(Runtime Constant Pool)

  • 隶属于方法区的一部分
  • 类加载的时候,Class文件里面的Constant Pool将在类加载后进入方法区的运行时常量池存放。
  • 运行时常量池具有动态性,即运行期间也可以放入常量,譬如,String.intern()函数,可以将新的字符串放入常量池中

直接内存(Direct Memory)

  • 非运行时数据区的一部分
  • JDK1.4中加入了NIO引入了基于通道(Channel)缓冲区(Buffer)的I/O方式。可以直接使用Native函数库分配堆外内存。
  • 使用DirectByteBuffer对象作为这块内存的引用进行操作。
  • 这块内存区域的存在可以避免Native堆和Java堆之间来回复制数据
  • 抛出异常:OutOfMemoroyError

对象的探秘

对象的创建

类加载 ==> 方法区 ==> 创建对象 ==> Java堆

创建对象详细过程:

分配内存 ==> 初始化为零值(JVM对对象的初始化) ==> 设置对象头 ==> 执行<init>方法

  1. 类加载,包括类的加载,解析,初始化过程。

  2. 类加载完成后,就可以为新生的对象分配内存了。对象所需要的内存在类加载后是可以完全确定的。

    • 分配方式

      内存情况 分配方式 GC收集器
      内存规整 “指针碰撞” Serial、ParNew
      内存不规整 “空闲列表” CMS

      使用CMS这种基于Mark-Sweep算法的收集器时候,通常采用空闲列表

    • 线程安全问题

      并发情况下,正在给A分配内存,指针还没来得及修改,对象B又同时使用了原来指针分配内存的情况。

      解决方案有二:

      • 一、对分配空间采用同步处理--实际上虚拟机采用CAS配上失败重试的方式保证操作的原子性

      • 二、分配动作按照线程划分在不同的空间中进行。每个线程预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer)。(HotSpot使用的解决方案就是TLAB)

        只有TLAB使用完以后才会分配新的TLAB,才需要同步锁定。

        虚拟机是否使用TLAB可以配置参数-XX:+/-UseTLAB

  3. 然后虚拟机会将内存空间都初始化为零值(不包括对象头)。如果使用TLAB可以提前到TLAB分配时进行这个操作。

  4. 虚拟机对对象进行必要的设置 -- 设置对象头

  5. 在虚拟机里面,可以认为新的对象产生了。但是程序员的角度看,对象创建才刚刚开始 -- <init>方法还未执行

    <init> 方法是类默认继承的一个Magic Method。类创建对象的时候,会将Constructor的实参传到 <init> 方法中。 </init> </init>

对象的内存布局

对象储存的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)

对象头(Header)

第一部分用于储存对象自身的运行时数据。如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

这部分数据长度在32位和64位操作系统中,长度分别为32bit和64bit,官方称为"Mark Word"。

Mark Word被设计为一个非固定的数据结构以便在极小的空间里面储存尽量多的信息。

第二部分就是类型指针,即对象指向它的元数据类型的指针,虚拟机通过这个指针来确定这是哪个类的实例。

查找对象的元数据信息并不一定要经过对象本身。

实例数据(Instance Data)

这部分是对象真实储存的有效的信息。HotSpot的分配策略是相同宽度的字段总是被分配到一起

如果CompactFields参数为true,那么子类变量较窄的变量也可能会插入到父亲变量的空隙当中。

对齐填充(Padding)

HotSpot要求对象起始地址必须是8的整数倍。也就是对象的大小必须是8字节的整数倍。如果对象没有对齐那么就需要对其填充。

对象的访问定位

对象的访问定位有两种方式,一种是通过句柄访问,另一种是直接访问

句柄访问

直接指针访问

HotSpot虚拟机使用的就是直接指针访问


<section class="footnotes">
  1. 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区域中的虚拟机栈(Virtual Machine Stack)的栈元素 ↩︎

  2. 可能是指向对象起始地址的引用指针,也可能是一个代表对象的句柄或与此对象相关的位置 ↩︎

  3. 指向一条字节码指令的地址 ↩︎

  4. JDK1.6存放在方法区的永久代PermGen)中;JDK1.7常量池被移除出方法区,转移到了Native Memory;JDK1.8完全抛弃永久代,用元空间Metaspace)代替 ↩︎

</section>
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务