Java虚拟机内存区域详解

  1. 云栖社区>
  2. 博客>
  3. 正文

Java虚拟机内存区域详解

caoyongc 2018-04-12 16:31:06 浏览2583
展开阅读全文

更多文章 访问我的博客:http://www.caoyong.xin:8080/blogger 

Java虚拟机内存区域详解

半年前买了一本深入理解Java虚拟机,买了就放在那里去了,期间拿出来想研究一下,还没有看一会,哇 !脑袋疼j_0004.gif。也就又放回原处,这段时间事情不多,自己也静下心来,看看这本被誉为佳作的书。


目录结构

    1:Java虚拟机介绍

    2:内存区域介绍

        2.1:程序计数器

        2.2:Java虚拟机栈

        2.3:Java堆

        2.4:方法区

        2.5:本地方法栈

    3:对象的创建(转载)


1:Java虚拟机介绍

    学习Java的人,都听说过Java虚拟机,也叫JVM,估计也就停留在这里的,(我也差不多,刚开始)。Java语言的诞生在1995年(我出生)Java发布了第一个版本Java1.0,这个时候Java喊出了一句口号 Write Once,Run AnyWhere 一次编写,随处运行 而那个时候的Java虚拟机是 Sun Classic VM 。到2000年的时候Java1.3,就把HotSpot作为了一直沿用至今的Java虚拟机。所以我们现在用大部分Java虚拟机都是HotSpot 


2:内存区域介绍

        Java虚拟机在执行Java程序的时候会把内存分为几个数据区域,看下面的图,介绍了Java虚拟机的运行时数据区的划分

    QQ截图20180412135323.png

    下面我们就来一一介绍这些数据区域

    2.1:程序计数器

    程序计数器是一块较小的内存区域,在java的字节码解析器当中,需要辨别当前的字节码解析到了哪个地方,同时需要来控制程序的流程,如果在程序当中没有一个东西来记录当前程序执行到哪个,同时下一步应该执行哪一步操作例如:分支、循环、跳转、异常处理等操作都不是按照原本程序书写的顺序来执行的,所以为了能够引导程序的运行,就需要引进一个用来引导字节码解析顺序的东西,就叫做程序计数器。

        Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。如果一个线程运行一半,就被挂起,等待另一个线程执行完毕后在接着执行。为了线程切换后可以正确的恢复到原来执行的位置,所以每个线程都应该有一个独立的程序计数器,也就是说程序计数器这一块内存区域是私有的。也就叫"线程私有"。

        还有一点,如果线程正在执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址。如果执行的native方法,计数器当中的内容应当是空。 还有此内存区域在java的虚拟机规范当中是唯一一个没有规定OutOfMemoryError(内存溢出错误)的区域。

     2.2:Java虚拟机栈

    也叫Java栈,他也是线程私有的,Java虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会在Java虚拟机栈中创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方发从调入到执行完毕,也就对应这栈帧进栈到出栈的过程。

      局部变量表存放着编译期可知的各种基本数据类型和对象的引用,我们通常说的Java栈存放对象引用,Java堆存放对象实例,现在应该明白了具体存放哪里了。

QQ截图20180412145340.png

Java虚拟机栈有两种异常状况

    第一种 线程请求的栈深度大于最大可用深度,则抛出stackOverflowError;

    第二种栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。

   2.3:Java堆

    Java堆是Java虚拟机所管理的最大的一块内存区域,Java堆是线程共享的一块区域,当然Java堆也是垃圾收集器管理的主要区域,可以分为新生代和老年代(tenured)。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。新生代又可进一步细分为eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。

QQ截图20180412150950.png


2.4:方法区

        方法区也是各个线程共享的内存区域,它 用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码数据。

       方法区中有三个池(jdk1.6之前),

        QQ截图20180412152907.png

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。

  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。

  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。


  1. 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。

  2. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

  3. 字面量:文本字符串、声明为final的常量值等;

  4. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

      JDK1.6之前字符串常量池位于方法区之中。 
      JDK1.7字符串常量池已经被挪到堆之中。

2.5:本地方法栈

和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C实现的。Java底层会调用C编写的的类库中的方法,在Java中调用本地方法使用native关键词。而本地方法栈就是管理这些本地方法的。


下面这一部分是对象在Java虚拟机创建的一系列过程,看到有位博主写了篇关于这部分的内容,所以就转载一下。写的很具体

转载:https://blog.csdn.net/sc313121000/article/details/50819741

一、对象的创建

new Animal();

1.类加载检查:

检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。

2.为对象分配内存

对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

2.1根据Java堆中是否规整有两种内存的分配方式:

(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)

指针碰撞(Bump the pointer):

Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。

空闲列表(Free List):

Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。

2.2分配内存时解决并发问题的两种方案:

对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

对分配内存空间的动作进行同步处理—实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性; 
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

3.内存空间初始化

虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。 
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.对象设置

虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

5.init

在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。 
但是从Java程序的角度看,对象的创建才刚刚开始方法还没有执行,所有的字段都还是零。 
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。

二、对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

1.对象头:

HotSpot虚拟机的对象头包括两部分信息。

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

HotSpot虚拟机对象头Mark Word

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

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考 三对象的访问定位) 
QQ截图20180412155353.png

2.实例数据:

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类 
中继承下来的,还是在子类中定义的,都需要记录下来。 
HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。

3.对齐填充:

对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。 
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

三、对象的访问定位

建立对象是为了使用对象,我们的Java程序需要通过栈上的引用数据来操作堆上的具体对象。 
对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。

使用句柄: 
如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

QQ截图20180412155403.png


通过句柄访问对象

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针: 
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而引用中存储的直接就是对象地址。 

QQ截图20180412155411.png

通过直接指针访问对象

优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。(例如HotSpot)

网友评论

登录后评论
0/500
评论
caoyongc
+ 关注