Java内存区域

一、运行时数据区域

1、程序计数器
  当前线程所执行的字节码的行号指示器,线程私有,各个线程的计数器是独立的,互不影响,正在执行的是Native方法,计数器值为空。

2、Java虚拟机
    线程私有,Java方法执行的内存模型,每个方法执行都会创建一个栈帧,包括局部变量表,操作数,动态链接,方法出口等信息。每个方法的执行开始到结束完成,对应一个栈帧从虚拟机栈入栈到出栈的过程。
局部变量表:存放了编译器的各种基本类型和对象引用类型,64位的long和double类型占用2个局部变量空间,其他的类型都是一个。局部变量表所需的内存空间在编译期间已经确定,方法运行期间布局变量表大小不会发生改变。
异常:线程请求的深度大于虚拟机允许的深度,抛出StackOverflowError,虚拟机动态扩展,无法申请到足够的内存,抛出OutofMemeryError.

本地方法栈:和虚拟机栈类似

3、java 堆
       所有线程共享的内存区域,存放对象实例及数组,垃圾回收的主要区域,Java堆可以是物理上不连续的空间,逻辑上连续就可以了,大小即可固定,也可动态扩展,当堆中无法完成实例分配,也无法扩展时,抛出OutofMemeryError。

4、方法区域
      所有线程共享的区域,主要存储虚拟机加载的类信息,常量,静态变量,编译器编译后的代码数据。
运行时的常量池:是方法区的一部分,类的文件中除了有类的版本信息,方法,字段,接口等信息外,还有一项信息,常量池,用于存放编译期生成的各种字面量和符号引用。运行时的常量池,虚拟机规范没有做任何要求,不同的提供商可按需实现这块内存区域。

5、直接内存
 NIO类,可以使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象对这块内存进行操作。

二、对象的创建
   1、当遇到一个new指令,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,检查符号引用所对应得类是否已加载,解析,初始化,如果没有,则进行类的加载过程。
   2、对象内存分配:类加载完成后,将为新生对象分配内存,对象所需内存空间在类加载完成时已确定大小。根据内存是否规整,内存分配分为两种方式:指针碰撞,空闲列表。
  指针碰撞:Java堆内存绝对的规整,用过的和空闲的内存各放一边,分配时只需移动指针即可。
  空闲列表:用过的和空闲的内存交错在一起,虚拟机通过维护一个列表,记录哪块内存是可用的, 当分配内存时,从列表中找出一块分配给对象,同时更新列表。
 对象的内存分配是一个很频繁的事情,仅仅是个指针修改的操作,在并发时也存在线程安全的问题,虚拟机通过两中方式解决:1、内存分配空间采用同步处理,虚拟机采用cas失败重试机制;2、把内存分配按线程分配在不同的空间中,即每个线程在Java堆中预先分配一块小内存,称为本地线程分配缓存。
3、初始化零值:虚拟机将对象分配的内存空间都初始化为零值,保证了对象的实例字段在java代码中不赋值也可以直接使用。
4、设置对象头:对对象进行必要的设置,如对象是哪个类的实例,如何能找到类的元数据信息,对象的哈希吗值,对象的GC分代年龄等信息,这些信息都设置在对象头中。
5、代码初始化:执行方法,把对象按照程序代码,进行初始化。

三、对象的内存布局
     对象内存布局分为3个区域:对象头,实例数据和对齐填充。
  对象头:包括两部分信息,一部分存储对象本身运行时的数据包括哈希码,GC分代年龄,锁状态标志,线程持有的锁等,另一部分,类型指针,通过这个指针确定对象是哪个类的实例。
   实例数据:对象真正的存储的有效信息,代码中定义的各种字段类型,包括从父类中继承下来的。
  对齐填充:因为对象的启始地址必须是8字节的整数倍,当对象的实例数据部分没有对齐时,需填充补齐。

四、对象的访问定位
  通过栈上的reference数据来操作堆上的具体数据,访问方式有使用句柄和指针两种。
  句柄:java堆会分配一块内存来作为句柄池,reference中存放的就是对象的句柄地址,句柄中包含了指向对象的实例数据和类型数据的地址信息。
 指针:reference中存储的直接是对象的地址。

你可能感兴趣的:(Java内存区域)