JVM笔记
内存分配
数据区域
1.8之前
- 线程共享:堆、方法区
- 线程私有:本地方法栈、虚拟机栈、程序计数器
1.8之后
- 线程共享:堆、
元空间
- 线程私有:本地方法栈、虚拟机栈、程序计数器
程序计数器
作用
- 读取指令,实现流程控制(选择、循环、顺序、异常处理)
- 多线程情况下,记录线程当前位置
- 唯一不会出现OOM的地方,随线程生命周期决定
虚拟机栈
会出现两个错误。StackOverFlowError
和 OutOfMemoryError
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
- OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
- 每次调用方法都会创建栈帧(包括局部变量表、操作数栈、动态链接、返回地址)
- 用来保存局部变量、参加方法计算与返回
本地方法栈
- 使用Native方法服务,也会出现上述两种错误
- Native方法使用C语言编写
堆
- 内存中最大的一块。共享区域,存放对象和数组,都会在这里分配内存
不是所有对象,有个逃逸的概念。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。 - 是垃圾回收的主要区域
- 1.8以前,堆内存通常分为:新生代、老生代、永久代
- 1.8之后移除永久代,加入元空间,使用的直接内存(物理内存)
- 新生代又分为eden区和survivor区,survivor区分为from survivor 和 to survivor又叫s0,s1
OOM的几种形式java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
: 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值
方法区
- 放常量、静态变量,类加载信息,又被称为永久代。于1.8之后移除,用元空间代替
- 类信息包含版本、字段、方法、接口、父类等信息
元空间
- 1.8后出现,使用本地内存
运行时常量池
方法区的一部分,存放类信息和常量池表,当无法申请到内存时会OOM。字符串常量池在堆
直接内存
也会出现OOM的问题,受到本机总内存大小和处理器寻址空间的限制
HotSpot虚拟机
Java创建对象的过程
- 类信息加载
- 分配内存
- 初始化零值
- 设置对象头
- 执行init方法
类信息加载
一个判断过程,看是否需要类加载。虚拟机遇到new指令,判断在是否能在常量池中定位到这个类的引用,是否被加载,解析和初始化过。都没有执行类加载过程
分配内存
在类加载完后确定内存大小,从堆中划分一块区域。
内存分配方式
由堆决定,是否规整。分为
指针碰撞
和空闲列表
两种。
规整则碰撞,有用过的放一边,没用过的放另一边。指针指向没用过的一块区域。代表GC:serial、parnew
不规整则空闲:虚拟机维护一个表,记录哪些内存可用,分配的时候,给一块足够大的。代表GC:CMS
如何保证创建对象线程安全
CAS+失败重试。保证更新操作的原子性
TLAB:首先在TLAB分配,如果不够,再采取上述的CAS分配
初始化零值
初始化一个值,保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
设置对象头
初始值完成后,虚拟机将对象的一些信息放于对象头中(类的元数据信息、对象的哈希码、对象的 GC 分代年龄)
init
对于虚拟机来说,对象已经创建完了,从程序视角看,创建才刚开始,init会按照程序员的意愿进行初始化
垃圾回收
基本结构
堆是内存回收的主要区域,又称为GC堆
-xmn:设置新生代堆大小,xms:设置最小堆空间,xmx:设置最大堆空间
一般情况下,对象从eden区分配,gc后,如果还存活,进入s0或s1,年龄+1,默认增加到15岁,会放入老年代中,可以通过参数 -XX:MaxTenuringThreshold
来设置老年代默认岁数。大对象(需要连续调用内存空间,字符串,数组)则直接进入老年代
GC分类
- 部分收集:
- 新生代(Minor GC / Young GC)只收集新生代
- 老年代(Major GC / Old GC)只收集老年代
- 混合收集(Mixed GC)整个新生代和部分老年代
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。
判断对象死亡
在gc收集的时候,首先会判断对象是否已经死亡。常见的算法有引用计数法、可达性分析算法
引用计数法
如果有一个地方引用计数器+1,反之-1,为0的时候表示对象不可在被使用。主流虚拟机不会采用此算法,难以解决对象相互循环引用的问题
可达性分析算法
以GC roots为起点,向下搜索,如果走不通,则对象不可用
判断类无用
达到下面3个条件就可以
被回收了
- 所有类实例被回收,不存在该类的实例
- classloader被回收
- java.lang.Class对象没有在任何地方被引用,无法通过反射方式获取到该类
回收算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
标记-清除
分为两阶段,标记和清除(清除不标记的)首先标记处不需要回收的对象,在标记完成后统一回收掉没有被标记的对象,最基础的算法
带来的问题
- 效率不高
- 空间问题(不连续的碎片)
标记-复制
解决上述效率问题,将内存分为大小相同的两块,每次使用其中的一块,这块的用完后,将存活的对象复制到另外一块内存中去,再将先前一半清空。这样每次回收都是内存的一半
标记-整理算法
标记对象后,让存活的对象向一端移动,在清理边界之外的内存
分代收集算法
主流的回收算法。根据存活周期将内存分为几块,一般分为新生代和老年代,再根据不同的代选择不同的算法
在新生代中,每次回收都有大量对象死去,可选择标记-复制算法(只需要付出少量对象的复制成本就可以完成每次垃圾收集)
老年代对象存活几率高,也没有额外空间分配担保(新生代内存不够了,放到老年代中),必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
收集器种类
上述方法,下述具体实现
- serial
- parnew
- paraller scavenge
- cms
- G1
没有最好的收集器,只有最适合的收集器
新生代回收器:Serial、ParNew、Parallel Scavenge 一般采用的是复制算法
老年代回收器:Serial Old、Parallel Old、CMS 一般采用的是标记-整理的算法
整堆回收器:G1
serial
串行收集器,最基本的收集器,单线程,工作的时候必须停止其他的工作线程。但简单而高效(与单线程比)
新生代采用标记-复制算法,老年代采用标记-整理算法
parnew
serial的多线程版本,新生代采用标记-复制算法,老年代采用标记-整理算法
paraller scavenge
- 同parnew,关注点是吞吐量(CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。吞吐量越高,cpu利用率越高),CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
- 新生代采用标记-复制算法,老年代采用标记-整理算法。
- jdk8的默认收集器 = paraller scavenge + Parallel Old(Parallel Scavenge的老年代版本)
- 注重吞吐量可以选择该收集器
CMS
- 注重用户体验,获取最短停顿时间为目标
- hotspot真正意义上的并发收集器,实现了垃圾收集线程和用户线程同时工作(基本)
- 标记-清除算法实现
- 对CPU资源敏感
- 无法处理浮动垃圾
- 有空间碎片
G1
- 面向服务器,满足停顿时间还有高吞吐量
ZGC
- 标记-复制算法,不过做了重大改进
什么时候FGC
- system.gc()
- 老年代空间不足
- 永久代空间不足