一、JVM运行时数据区域
JVM运行时数据区域包括由所有线程共享的数据区:方法区、堆,和线程隔离的数据区:虚拟机栈、本地方法栈、程序计数器。
1、程序计数器
它是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器来获取下一条需要执行的字节码指令。
如果线程执行的是一个Java方法,那么它存放的就是正在执行的虚拟机字节码指令地址;如果执行的是本地方法,那么它的值就为空。 它是唯一一个没有OutOfMemoryError情况的区域。
2、虚拟机栈
线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
局部变量表:存放了编译器可知的各种Java虚拟机基本数据类型、引用类型、returnAddress类型(指向了一条字节码指令的地址)。
异常情况:如果线程请求的栈深度大于虚拟机执行所允许的深度,将抛出StackOverflowError异常,如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
3、本地方法栈
与虚拟机栈发挥的作用是十分相似的,区别在于本地方法栈是为虚拟机使用到的本地方法服务的。
4、堆
堆是虚拟机所管理的内存中最大的一块。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。次内存区域的唯一目的就是存放对象实例。几乎所有的对象实例都在堆上分配,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段,使得“所有对象实例都在堆上分配”渐渐变得不再绝对。
堆是垃圾收集器管理的内存区域,从回收内存的角度看,垃圾收集器分为基于分代收集理论设计的和基于分区收集理论设计的。
从分配内存的角度看,所有线程共享的堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提升对象分配的效率(如果TLAB内存不足,线程将采用CAS+失败重试的方式从堆中分配内存)。
异常情况:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,JVM将会抛出OutOfMemoryError
5、方法区
也是线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK8以前HotSpot使用永久代来实现方法区,这样使得垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。在JDK8中在本地内存中用元空间代替了永久代。
方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。方法区无法满足新的内存分配需求时,抛出OutOfMemoryError异常
运行时常量池
是方法区的一部分,Class文件中除了有类的版本、字段(类变量和实例变量)、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。
6、直接内存
直接内存不是虚拟机运行时数据区的一部分,也可能导致OutOfMemoryError异常出现。
JDK1.4新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存(在内核中分配内存),然后通过一个存储在堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以提高性能,避免了堆和Native堆中来回复制数据。(也就是数据在内核态和用户态的来回复制)
二、对象
1、对象的创建
(1)、当Java虚拟机遇到一条字节码new指令后,就会检查这个指令的参数能否在常量池中找到对应类的符号引用,并且检查这个符号引用所代表的类是否经过类的加载、连接、初始化过,如果没有就必须先执行相应的类加载过程。
(2)、类加载通过后,接下来就是为新生对象分配内存,分配内存有两种方法:指针碰撞和空闲列表,指针碰撞适用于内存规整的情况下,被使用过的内存放到一边,未使用的放到另一边,中间用一个指针作为分界点,当分配内存时就是将指针向未分配的内存方向移动一段与对象大小相等的距离。空闲列表适用于内存不规整的情况下,虚拟机会维护一个列表,记录了哪些内存块是可用的,当分配内存的时候就从列表中找到一块足够大的空间给这个对象。 使用哪种分配方式由内存是否规整来决定,内存是否规整又和使用哪种垃圾收集器有关,当使用Serial、ParNew等带压缩整理过程的收集器,就使用指针碰撞,这样既简单又高效,如果使用的是CMS这种标记-清除算法的收集器理论上只能使用空闲列表分配。(“理论上”是因为在CMS实现中,为了能在大多数情况下分配得更快,设计了一个叫做Liner Allocation Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,在它里边仍然可以使用指针碰撞方式来分配)。 由于堆内存是多线程共享的,所以分配内存时可能出现并发问题,也就是说当一个线程正在给它的对象分配内存时,另一个线程也想使用这块内存,就导致了这种线程不安全问题。解决方案:采用CAS配上失败重试的方式保证更新操作的原子性;另一种就是分别给每个线程在堆中分配一小块内存TLAB,需要分配内存时就先在TLAB上分配,只有TLAB内存不足了才使用CAS+失败重试的方式。
(3)、将分配到的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。
(4)、设置对象的对象头信息,包括这个类是哪个类的实例、如何找到类的元数据信息、对象的哈希码、GC分代年龄、锁的状态(是否启用偏向锁)
(5)执行构造方法,也就是Class文件中的()方法
2、对象的内存布局
对象的布局可以划分为三个部分:对象头、实例数据、对齐填充
对象头:包含两类信息,第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、如果是轻量级锁会存储指向对应线程栈帧中的锁记录指针,如果是重量级锁,就会存储指向重量级锁的指针,偏向锁的话会存储偏向线程ID,偏向时间戳,对象分代年龄。这部分叫做Mark Word。另一部分是类型指针,就是对象指向他的类型元数据的指针。如果对象是一个数组,那在对象头中还必须有一块记录数组长度的数据。
实例数据:对象真正存储的有效信息
对齐填充:起到占位符的作用,因为HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍。
3、对象的访问定位
使用句柄访问:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中分别包含了对象实例数据和类型数据。好处:reference中存储的是稳定的句柄地址,对象由于垃圾收集而被移动时,只需要改变对应句柄中记录的对象地址,reference不需要改变
使用直接指针访问:reference中存储的直接就是兑现地址。如果访问对象的话就不需要多一次间接访问的开销。好处:速度更快,因为节省了一次指针定位的时间开销。
三、内存溢出异常
1、堆溢出
可以使用-Xms参数设置堆的最小值,-Xmx设置最大值,如果一样的话就可以避免堆的自动扩展。
2、虚拟机栈和本地方法栈溢出
-Xss来设置栈的内存容量。
如果线程请求的栈深度大于虚拟机执行所允许的深度,将抛出StackOverflowError异常,如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
当给每个线程的内存过大时也可能导致OOM。因为操作系统给每个进程分配的内存是有限制的。这个时候可以通过减小最大堆内存和减少栈容量来获取更多的线程。