对象与运行时内存
和大多数猴子一样,我原来也抵触对原理的学习,
后来发现掌握了原理才有了那种了然于胸,运筹帷幄的感觉,也就是顿悟。
这里主要介绍Java对象与运行时内存的知识。
java运行时内存
Program Counter Registe(程序计数器):
记录当前线程执行字节码的位置,相当于行号指示器,为线程私有的。
Java Virtual Machine Stacks(java虚拟机栈):
存放方法出口信息、局部变量表(对象引用、基本类型数据等),为线程私有的。
Native Method Stack(本地方法栈):
和java虚拟机栈一样,只不过是为本地方法服务的。
Java Heap(堆):
存放new出来的对象,是线程共享的,也是垃圾收集器管理的主要区域(可以细分为:新生代和老年代),堆的大小通过-Xmx和-Xms控制。
Method Area(方法区):
存放类信息(版本、字段、方法、接口等描述信息)、常量、静态变量等,是线程共享的。
(Runtime Constant Pool)运行时常量区:
存放编译时产生的字面量和符号引用,属于方法区的一部分。
内存分配的方式
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。分配内存时就把指针向空闲空间那边挪动一段与对象大小相等的距离;
一些新生代GC收集器使用的是复制算法,所以采用指针碰撞方式分配内存。
空闲列表
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录;
老年代垃圾收集器使用了标记清理、整理算法,所以配合空闲列表方式分配内存。
访问对象的方式
句柄访问
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息;
使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
直接指针
reference中存储的直接就是对象地址,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息;
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,HotSpot采用了直接指针方式。
对象在java堆的过程
普通对象在堆中从创建到销毁的整个过程大概可分为:new > 类加载 > 分配内存 > 初始化 > 使用 > 回收
new:
向虚拟机发送一条new指令
类加载:
方法区内存进行校验(检查该对象所属类是否加载、解析、初始化),在确定是否进行类加载;
对象所需内存的大小在类加载完成后便可完全确定。
分配内存:
在新生代的Eden区分配内存空间,如果Eden区内存空间不够就从FromSurvivor内存分配,还是不够的话分配到老年代内存区:
分配方式有指针碰撞、空闲列表,分配内存存在线程安全问题,有两种解决方案:同步、TLAB线程私有空间。
初始化:
将分配的内存空间初始化为默认值, 比如int类型的为0,String为null;
设置对象头信息(对象是哪个类的实例、如何才能找到类的元数据信息、哈希码等)。
使用:
通过引用访问对象,访问的方式有指针碰撞、句柄访问两种,取决于虚拟机。
回收:
当JVM内存不够时,如果该对象超出了作用域,并且在GCRoots搜索不到,就进行回收标记;
如果下一次GC之前,该对象还是没有被使用,就回收内存。