内存学习概述

一、数据区域简述

  java虚拟机在执行程序时一般会将其内存分成多的不同的数据区。一般来说会有五个。分别是方法区、堆区、虚拟机栈区、本地栈区以及程序计数器。

1、方法区:方法区中主要存储了类的相关信息,这里的信息是指加载完毕的类信息(类的名字->全限定名、类的修饰词、字段、方法、版本号等等)、常量等等。

2、堆区:堆区中主要存储了实例化后的对象(也就是说new出来的对象)

3、虚拟机栈区:虚拟机栈描述了线程的内存模型,比如这个线程执行了什么方法,方法里有什么变量,下一个该执行的方法是哪个,方法的返回值是什么等等。当然正式点说的话本区域存储了局部变量表、操作数栈、动态链接、方法出口等信息。

4、本地方法栈区:本地方法栈区与虚拟机栈区极其相似,他们的差别在于此区是给Native方法所留,被Native修饰的方法回去调用对应平台的方法。

5、程序计数器:程序技术去它的作用是告诉我们应该去执行那一条字节码指令(像是一个路标)。

  上述五个数据区其中方法区与堆区是所有线程共享的
  虚拟机栈区、与本地方法栈区、程序计数器这三位是属于单个线程的。

  每个线程都是特殊的,他要有一个私有的模型去描述,以来告诉虚拟机我这个线程执行了什么方法、用了什么变量、下一步往哪里走。所以我们为每一个线程都准备了一个私有地盘。
  同时每一个线程又有公共的部分。比如我new了一个对象,A线程用得我B线程就用不得吗?一个类,线程A可以实例化那么线程B就不可以实例化吗?所以我们划分了一个的区域分给了对象和类,并让线程们共享。

  除了上述五个区域虚拟机还能操作一个不属于虚拟机的空间->直接内存区。java使用direct memory对象操作这部分空间。操作系统I/O过程中,需要把数据从用户拷贝到内核,然后再输出到I/O设备,所以从java堆内存输出到I/O设备需经过两次拷贝,而direct memory对象可以操纵直接内存空间将数据直接写入磁盘,所以只需经过一次拷贝。

二、对象的创建

1、对象的结构:在知道对象是如何创建之前我们要知道对象长什么样子。
  对象由三部分组成:对象头、实例数据、对齐填充

1)对象头: 对象头中主要存储了对象自身运行时的数据,换句话说这部分数据是对对象自身的描述。如哈希码、分代年龄、锁状态等信息。
  同时存储这部分数据的空间大小是32或64位Bitmap(一种用较小的空间表示大量数据的方法),但是对象头的信息其实超过了32或64位bitmap所能记录的限度同时这部分信息又和实际的数据没有关系所以我们复用了这个空间。换句话说我们根据标志位的不同使得这边块空间存储了不同的信息。
  对象头中除了存储描述自身的数据之外有时还会存储器类元数据的地址,虚拟机通过这来确定对象属于哪一个类。关于这点需要结合下面关于对象的访问定位部分来思考。

2)实例数据: 这部内容是我们真正存储的信息,也就是我们声明的变量。

3)对齐填充: 此部分仅仅起着占位符的作用。
    其作用应该是为了保证cpu仅读取一次便能获得到此数据(不保证正确)

2、对象的创建过程
  从大的方向上我们知道了内存是如何划分的,那么我们就可以尝试初步的了解一个对象的建立过程。一个对象的成功建立一般分为三步。检测、分配空间、初始化。

1)、检测:当我们使用new关键字时第一步就要去看这个对象有没有被加载过,如果加载过那么就可以去分配空间,反之就要去加载这个类。

过了这个阶段被加载的静态属性会被两次赋值。
  第一次赋值:赋0值;
  第二次赋值:调用< clinit >静态属性初始化。

2)、空间分配:本阶段负责将对象使用的空间在堆内存中将其分配出来。

在空间分配这里还有两点是要提到的就是如何分配空间,以及如何确保我们的操作安全
一、空间分配方法
   1、指针碰撞法:首先我们将一个长条内存分为两部分左边是使用过的(存储了数据),右侧是没有使用过的(没有存储数据)然后我们使用一个指针将其分割开来。每当有一个数据被放入内存或者清出内存我们只需要移动指针然后将数据添加或者抹除即可。
   2、空闲列表法:此方法使用一个列表记录了内存中可以使用的空间,当我们有数据进入内存时我们只需要在列表中找到一个足够大的空间分配出去即可。
二、安全问题
   1、对操作进行同步处理,使用失败重试的方法保证操作的原子性。
   2、提前为每一个线程分配一个内存空间,使得线程仅在此部分进行操作,这样就不会出现线程A操作线程B的对象这种问题。当然如果空间用完那么就同步锁定再去处理相关操作。

3)初始化:执行实例构造器< init >(第三次赋值)

  到此一个对象的创建就结束了
  实际上一个对象的创建远比现在描述的复杂的多

三、对象的访问定位

  知道了对象如何创建以及对象住在哪里那么接下来就得看一下我们要如何找到这个对象了。
1、句柄池访问:栈中存储了一个引用,这个引用指向了一个句柄池,这个池子里存储了对象的地址以及对象所属类的地址。
2、直接地址访问:栈中存储了一个对象的引用,这个引用指向了一个对象,这个对象在对象头中存储了其类的元数据地址。

  两者的区别在于句柄池在对象地址变更时只需要改变句柄池的指针指向即可,而直接指针访问对于频繁访问对象的场景有者较好的表现(毕竟句柄池要先访问对象指针再访问对象,而直接引用法则是直接指向了实例对象)。