程序计数器 (线程私有)
主要作用:
1、字节码解释器工作的时候通过改变计数器的值,来选取下一个需要执行的指令,从而实现对代码的流程控制;
2、在多线程的情况下,程序计数器可以记录当前线程执行的位置,当线程发生切换之后,下一次执行的时候能够从上一次执行的位置开始;
注意:程序计数器是唯一一个不会出现OOM的内存区域,他的生命周期随着线程的创建而创建,线程的结束而死亡;
虚拟机栈(线程私有)
生命周期:同线程相同;
虚拟机栈是由一个个的栈帧组成,每一个栈帧里面有:局部变量表、操作数栈、动态链接、方法出口信息;
虚拟机栈会出现StackOverFlowError
和 OutOfMemoryError
StackOverFlowError
:如果java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈道深度超过了当前java虚拟机栈的最大深度的时候,就会出现栈溢出;
OutOfMemoryError:当java虚拟机栈的内存大小允许动态扩展,那么当虚拟机栈在在扩展的时候,无法申请到足够的内存空间就会出现内存溢出;
注意:HotSpot虚拟机的栈容量是不允许动态扩展的,所以HotSpot是不会出现由于java虚拟机栈申请不到足够的内存无法扩展而出现OOM的异常;只有当HotSpot虚拟机在第一次申请java虚拟机栈的时候,内存空间不足申请失败才会有OOM,一旦java虚拟机申请成功了就不会出现出现OOM异常;
扩展:方法/函数如何调用
java栈中主要保存的事栈帧,当方法被调用的时候就会被压入栈,当方法结束之后就会有一个栈帧弹出;
java有两种返回方式:
1、return ; 2、抛出异常
本地方法栈
和java虚拟机的区别在于服务的对象不同,虚拟机栈是为java方法(也就是为字节码服务),本地方法栈时当虚拟机调用本地方法的时候,为本地方法服务;
HotSpot虚拟机中本地方法栈和虚拟机栈合二为一;
堆(线程共享)
是java虚拟机中内存最大的一部分,主要目的就是用来存放对象实例;(栈也可以用来存放对象实例);
堆是垃圾收集器管理的主要区域,也称为GC堆;
由于现代的垃圾收集器都采用分代垃圾收集算法,所以堆可以细分为:新生代和老年代;也可以分为:Eden、Survior、Old等空间;这么划分的目的就是为了更好的回收内存或者是更快的分配内存;
java7及之前:新生代(Eden、Survior)、老年代、永久代;
java8:新生代(Eden、Survior)、老年代、元空间(使用的是直接内存);
大部分情况下,对象会在Eden区域分配,在进行一次新生代垃圾回收后,如果对象还存活,则会进入Survior0或者Survior1,同时对象的年龄会加1,当年龄增加到一定程度(默认15),就会被晋升到老年代中;(-XX:MaxTenuringThreshold来设置年龄阀值);
“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。
OOM错误形式
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:
就是指JVM花了很多时间执行垃圾回收,但是还能回收很少的堆空间,就会出现此错误;
java.lang.OutOfMemoryError: Java heap space:堆内存空间不足;
在创建对象的时候,堆内存的空间不足以存下这个对象就会出现这个错误;(
和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx
参数配置,若没有特别配置,将会使用默认值(物理内存的1/4))
方法区(线程共享)
用于存储 类信息、常量、静态变量等;
java虚拟机中将方法区作堆的一个逻辑部分;方法区有一个别名——非堆(目的是和java堆区分开);
方法区又称为永久代;
方法区和永久代的关系
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
java8用元空间替代永久代的原因
1、元空间使用的是直接内存,受系统内存限制,内存溢出的可能性更低;
2、元空间里面存放的是类的元数据,由于内存是受系统实际可用空间控制,这样能够加载的更多类;
运行时常量池(方法区的一部分)
用于存放编译期生成的各种字面量和符号引用;
对象创建过程
第一步:类加载检查
当虚拟机在遇到new指令的时候,首先会去检查这个指令的参数是否能在常量池中定位这个类的符号引用(先去找该类的符号引用),然后检查这个类有没有被加载、解析、初始化过,如果没有就首先执行类的加载(类加载的过程可以确定要加载对象的内存大小)过程;
第二步:分配内存
根据第一步类加载之后确定的内存大小,从java堆空间中划分空间分配给要创建的对象;
补充内容:
分配方式:“指针碰撞”、“空闲列表”
分配方式的选择的取决于java堆是否规整决定,java堆是否规整又取决于所采用的GC收集器算法是“标记-清除”还是“标记-整理”(也叫标记-压缩),复制算法内存也是规整的;
内存分配中的并发问题:
CAS+失败重试:CAS是乐观锁的一种实现方式。每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性(因为失败之后就会重试)。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
第三步:初始化零值
将分配到的内存空间都初始化为零值(不包括对象头),这一步相当于将对象的属性进行初始化为对应数据类型的零值;
第四步:设置对象头
对象的属于哪个类的实例,类的元数据信息(类型指针),对象的哈希吗、GC分代年龄信息(对象自身的运行时数据);
第五步:执行init方法
<init>
方法还没有执行,所有的字段都还为零(第三步进行了初始化零值);按照设置的值对创建的对象进行初始化;
对象的内存布局
对象头:用来存储自身的运行时数据(哈希吗、GC分代年龄、锁状态标志)
实例数据:对象存储的真正的有效信息;(定义在对象中的各种数据类型的属性内容)
对齐填充:因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。就需要用这个来占位保证对象的大小是8字节的倍数;