1、运行时数据区域
运行时数据区域:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。
非运行时数据区域:直接内存。
(1)程序计数器
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。在多线程的情况下,记录当前线程执行的位置。
生命周期随着线程的创建而创建,随着线程的结束而死亡。
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域。
注:1)Java编译器,将Java源文件(.java文件)编译成字节码文件(.class文件,二进制文件),即javac.exe。2)Java解释器,JVM的一部分,Java解释器用来解释执行Java编译器编译后的程序,即java.exe。
(2)Java虚拟机栈
JVM运行时数据区域的一个核心,由栈帧组成。
方法调用的数据通过栈进行传递,每一次方法调用都会有一个栈帧被压入栈中,方法调用结束后栈帧被弹出。
生命周期随着线程的创建而创建,随着线程的结束而死亡。
栈帧随着方法调用而创建,随着方法结束而销毁。
服务于虚拟机执行的Java方法(也就是字节码)。
若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误。
如果栈的内存大小可以动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
(3)本地方法栈
与Java虚拟机栈不同的是,它服务于虚拟机使用到的Native方法。
(4)堆
Java虚拟机所管理的内存中最大的一块。
存放对象实例。
Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆。
Java堆被划分为新生代、老年代和永久代,新生代又被划分为Eden区和两个Survivor区(S0、S1)。大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入Survivor区,并且对象的年龄还会加1(Eden区到Survivor区后对象的初始年龄变为1),对象在Survivor中每熬过一次垃圾回收,年龄就增加1岁,当它的年龄增加到一定程度(默认晋升年龄并不都是15,这个是要区分垃圾收集器的),就会被晋升到老年代中。老年代存放一些生命周期较长的对象(像新生代晋升过来的)或者较大对象。JDK8版本之后永久代已被元空间取代,元空间使用的是直接内存。
当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生java.lang.OutOfMemoryError: GC Overhead Limit Exceeded错误。
假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会发生java.lang.OutOfMemoryError: Java heap space错误。
对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
(5)方法区
存储已被虚拟机加载的类信息、字段信息、方法信息等(例如名称、修饰符等),即编译器编译后的代码缓存等数据。
在不同的虚拟机,方法区的实现是不同的。永久代和元空间是方法区的两种实现方式。
整个永久代有一个JVM本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制。
当元空间溢出时会得到如下错误java.lang.OutOfMemoryError: MetaSpace。
可以使用-XX:MaxMetaspaceSize标志设置最大元空间大小,默认值为unlimited。-XX:MetaspaceSize调整标志定义元空间的初始大小,如果未指定此标志,则Metaspace将根据运行时的应用程序需求动态地重新调整大小。
(6)直接内存
堆外内存或系统内存。
2、HotSpot虚拟机对象的创建
(1)类加载检查
(2)分配内存
分配方式有指针碰撞和空闲列表两种。
指针碰撞适用场合,堆内存规整的情况下。用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
空闲列表适用场合,堆内存不规整的情况下。虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
堆内存是否规整,取决于GC收集器的算法,标记清除算法内存是不规整的,标记整理算法、标记复制算法内存是规整的。
在创建对象的时候有一个很重要的问题,就是线程安全,虚拟机采用两种方式来保证线程安全,CAS+失败重试和TLAB。
CAS+失败重试,CAS是乐观锁的一种实现方式,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
TLAB,为每一个线程预先在Eden区分配一块内存(该内存区域,线程只享有私有的分配权,对于使用还是共享的),JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
(3)初始化零值
(4)设置对象头
例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象头中。
(5)执行init方法
3、HotSpot虚拟机对象的内存布局
对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
4、HotSpot虚拟机对象的访问定位
Java程序通过栈上的reference数据来操作堆上的具体对象。
目前主流的访问方式有:使用句柄、直接指针。
使用句柄,Java堆中将会划分出一块内存来作为句柄池,reference中存储的是句柄地址,而句柄中包含了对象的地址。
直接指针,reference中存储的是对象的地址。