JVM:内存区域与内存分配

一、Java运行时内存中的数据区域:

        每一个Java程序启动后,会获得一个JVM的实例,用来管理内存。Java 虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有个字的用途,以及创建和销毁时间。Java虚拟机所管理的内存将会包括以下几个运行时数据区域:

1、程序计数器:程序计数器是一块较小的空间,是当前线程执行的字节码的行号指示器,字节码解释器工作时,就是通过改变计数器的值来选取下一条需要执行的字节码指令。(线程私有,每个线程都有一个独立的程序计数器,互不影响,独立存储)

2、Java虚拟机栈:线程私有,生命周期与线程的生命周期相同。它描述java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧,以帧的形式存放本地方法的调用状态,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,就对应着一个栈帧在虚拟机中入栈到出栈的过程。


3、本地方法栈:本地方法栈的作用和虚拟机栈的作用非常相似,区别是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

4、Java堆:占用内存最大的区域,在虚拟机启动时创建,用于存放对象实例,可以划分为新生代和老年代。

5、方法区:用于存储被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据。



二、内存分配:

       首先将class文件加载到方法区(存放类的基本信息,静态变量,编译后的代码等信息),然后new一个类,为这个类在Java堆中分配内存(主要是给字段属性分配内存)。调用一个方法时,根据方法的引用在方法区找到对应方法,然后在Java虚拟机栈中分配一个栈帧给这个方法存储相关信息。

1、new对象创建的过程:

(1)首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;

(2)检查这个符号引用代表的类是否已被加载、解析和初始化;

(3)如果没有,那必须先执行相应的类加载过程;

(4)如果有,虚拟机将为新生对象分配内存,并使用CAS保证操作原子性,分配方式有:指针碰撞和空闲列表;

(5)虚拟机将分配到的内存空间都初始化为零值,这一步保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用;

(6)对对象进行必要的设置;

(7)执行<init>方法,把对象按照程序员意愿进行初始化。


2、分配方式:

(1)指针碰撞:如果java堆是绝对规整的,所有用过的内存都放在一边,所有没用过的内存存放在另一边,中间存放一个指针作为分界点指示器。分配内存时,将指针从用过的内存区域向空闲内存区域移动等距离区域。

(2)空闲列表:如果java不是规整的,这时,虚拟机就必须维护一张列表,列表上记录了可用的内存块,在分配内存时,从列表上找到一个足够大的连续内存块分配给对象,并更新列表上的记录。

 

3、对象的内存布局:

在HotSpot中,对象的内存布局分成三部分:对象头,实例数据,对齐填充。

(1)对象头:包括两部分的信息:

        第一部分用于存储对象自身的运行时数据,如哈希码,GC代年龄,锁状态标志等。

        第二部分是对象指向它的类元数据的类元指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,那对象头中还必须有一块用于记录数组长度的数据。

(2)实例数据:是对象真正存储的有效信息,是在程序代码中所定义的各种类型的字段内容,相同宽度的字段会被分配到一起。

(3)对齐填充:并不是必然存在的,仅起着占位符的作用。


4、对象的访问定位:

    Java程序通过栈上的reference数据来操作堆上的具体对象。目前访问对象主要有两种方式:通过句柄访问对象和直接指针访问对象。

(1)通过句柄访问对象:


(2)通过直接指针访问对象:


(3)优劣对比:

   ① 使用句柄,reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。

    ②直接指针,速度快,节省一次指定定位的开销。