- 每个线程创建时,会在虚拟机栈分配一个固定的内存,一般为1M左右,也称为栈深度。 栈的总大小不受jvm限制,理论与进程可以申请到的内存一直
- 对象生成时,分配固定的大小在堆中。
- 方法区在jdk1.7之前也称为永久代, 1.7及以后改为元空间。元空间的好处是内存不受jvm限制,可以无限申请机器的内存
- Class的类型定义信息保存在方法区中,class对象还是分配在堆中
- 静态对象实例也是存储在堆中,即所有的对象实例都是存在堆中,不管是静态的还是class
- 方法区存储静态变量对象的引用
- 同时方法区中的静态变量可以作为GC ROOT存在
- 栈:方法时则一次入栈,保存一个栈帧,调用结束则出栈, 利用栈的先进先出的特性,维护方法调用链的先后关系,及层级套用关系。
- 进入方法时,则已经把该方法生成一个栈帧,局部变量表此时已生成(方法里面的变量及引用一开始就是已知的)
- 对象的长度,一定是完全固定的,因为对象内的变量只会有 基本类型或引用 ,这些内容的长度是绝对固定的。 这也是为什么String对象不能修改,如果想修改只能重新生成一个长度,String可以理解为固定长度数组。
生成一个对象的过程: (以下所有操作都是在jvm的 c++方法中处理)
- 虚拟机在遇到new指令时(new指令后面会跟上类型信息指令),内部调用对象生成方法
- 判断类型信息对应的类是否加载完成 (去方法区根据引用找到类型信息,并判断类型信息的加载状态)
- 如为未加载或未加载完,触发类加载
- 在堆区划分出固定大小的内存,准备用来存储对象实例
- 划内存的方式,如是带压缩归集的收集器,则将当前指针往后移动,同时返回对这块内存区间的引用
- 如是不带压缩归集的收集器,则有空闲内存表维护可用内存区域,调用查找维护表找到可用的空间
- 同时注意申请空间时,需考虑同步问题,加锁申请(使用CAS锁,比对失败机制)
- 为避免频繁加锁申请,可以设置线程生成时直接给线程在堆上分配独占一块缓存内存,线程生成实例时直接从缓存内存中分配内存,不需要加锁,以提高效率。 缓存内存使用完了,才会去同步的去公共内存中分配内存。
- 将分配的内存空间都置为0 (不包括对象头),保证了对象即使没有赋值,也可以访问到默认值。这块并发访问的时候,要注意这个问题。 (0值对于引用来说就是null)
- 对象实例需要指向类型数据,有两种方式
- 生成对象实例时,会同时生成对象类型信息的指针 (放在对象头中,称为元数据),指针指向方法区的类型信息。hotSpot使用的这种方式
- 用句柄池维护两者的关系,两个指针分别指向对象实例、对象类型数据。
- 虚拟机栈中,当前线程的栈的—>本方法的栈帧中的—>本地变量表内的—>reference引用,指向实例或句柄池
- 调用构造方法进行实例化,各个变量的值填到对象实例对应的内存中
- 完成对象的创建
(这里延伸出一个问题, 对象内存分配好、擦0了、引用分配了。 此时另外一个线程使用引用调用对象里内容,会全部给0值 。 生成单例对象时要用锁将整个对象生成过程包成同步。)
溢出场景
- 程序计数器占用内存极小,指定不会溢出。 想要此溢出只能申请超多的线程,而此时一定是栈先溢出
- 方法区,jdk1.7 的 hotSpot已经用元空间实现,直接使用进程的内容,仅受物理硬件的限制
- 虚拟机栈的线程本地栈,初始一般会分配1M的内存,存在调用深度过深,如无终止条件的递归,只压栈帧,不出栈帧,会导致线程级别的栈深度溢出
- 整个虚拟机栈本身也没有大小限制,申请线程过多时,因机器内存不够,会机器内存溢出
- 主要发生在堆溢出,生成的对象过多会导致堆内满载,无法在申请新的内存分配对象实例,堆内存溢出