JVM
作者:Guide哥
JVM内存区域(运行时数据区)
Java虚拟机在执行Java程序时会把它管理的内存区域划分成若干个区域,JDK1.8和之前的版本略有不同。
JDK8之前
JDK8
线程私有的:
- 虚拟机栈
- 本地方法栈
- 程序计数器
线程共有的
- 堆
- 方法区
- 直接内存(非运行时数据区的一部分)
程序计数器
程序计数器是一块较小的内存空间,它记录着当前线程的字节码文件执行的行号。
-
字节码解释器通过程序计数器来完成程序执行的流程控制,如:循环,分支,跳转,异常处理。
-
每个线程有独立的程序计数器,互不干扰这样线程在阻塞唤醒的时候可以继续执行未完成的流程。
ps 程序计数器是唯一一个不会出现OOM的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同,它主要存放着方法执行时的栈帧和其中的数据。
-
Java虚拟机栈会出现两种错误
- StackOverflow 栈溢出 若Java虚拟机栈的内存不允许动态扩展,那么当请求栈的深度超过当前Java虚拟机栈的最大深度的话就会出现StackOverflow错误。
- OOM 内存溢出 若Java虚拟机堆中没有空闲内存 并且垃圾回收器也无法提供多余内存是 就会抛出OOM错误。
-
Java方法返回的方式有两种
- return
- 抛出异常
无论是哪种栈帧都会弹出。
本地方法栈
和虚拟机栈作用类似,不同的是Java虚拟机栈为Java方法服务,而本地方法为Native方法服务。
堆
Java虚拟机中最大的一块内存区域,在虚拟机启动时创建,堆得唯一作用就是存放对象实例,几乎所有对象实例和数组都在这里分配内存。(逃逸分析?)
Java堆是垃圾回收器的主要回收区域,由于垃圾回收器大部分都采用分代算法所有堆可以划分为,年轻代和老年代,年轻代还可以划分为eden区和survivor区。进一步划分是为了更好的回收垃圾,更快的分配内存。
JDK8之前还有一部分叫做永久代也在堆内存中,但是JDK8已经将他移除了,转而直接使用系统内存。
堆中最容易出现的就是OOM错误
- OutOfMemoryError: GC Overhead Limit Exceeded:当JVM花费很多时间进行垃圾回收并且只能回收很少的内存时就会发生此错误。
- java.lang.OutOfMemoryError: Java heap space: 当新建对象申请内存时 堆内存空间不足以存放新对象 那么就会发生此错误。
方法区
方法区也是各个线程共享的,它主要存放已经被虚拟机加载的类信息,常量,静态变量等数据。方法区也被称为永久代。(方法区是Java虚拟机的规范标准,永久代是HotSpot的实现方式1.8之后叫做元空间。)
运行时常量池
运行时常量池也是方法区的一部分,Class文件中除了有类的版本,字段,方法等信息还有常量池标。
Java对象创建过程 待补充
类加载机制 待补充
类加载过程
JVM内存分配与回收
内存分配
大多数情况下,对象在新生代eden区分配内存,大对象会直接进入老年代。(默认年轻代:老年代 = 1:3 并且eden:s1:s0 = 8:1:1)
回收
在eden区满了的时候会触发YGC,通常是复制算法,将存活对象放到另一个survivor区中,年龄+1,如果有年龄大于15的就会进入老年代(可以改变)。
如何判断一个对象是否为垃圾
引用计数法
给对象添加一个引用计数器,每当有一个地方引用他计数器就+1,当引用失效时就-1。可能会出现循环引用的垃圾。
根可达算法
从一系列被称为根的对象为起点,查看当前对象是否被根对象直接引用或间接引用。
根对象
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
强软弱虚引用
强引用
强引用是我们最常用的引用,垃圾回收器宁可抛出OOM异常,也不会回收强引用对象。
软引用
如果一个对象只具有软引用那么内存不足时他就会被回收,软引用可以实现内存敏感的高速缓存。
弱引用
如果一个对象只具有弱引用,当垃圾回收线程发现他时就会立刻回收。
虚引用
虚引用就像它的名字一样,形同虚设,在任何时候都可能被回收,与软引用不同的是他需要和引用同队列配合使用。
垃圾回收算法
标记-清除 算法
该算法首先标记出所有不需要被回收的对象,然后回收掉所有没有标记的对象,这是最基础的算法。
他会产生大量不连续的内存碎片
复制算法
该算法将内存分为两个区域,每次只使用其中一个,他会将存活的对象复制到另一个区域,然后将原来的区域全部回收。
他不会产生内存碎片但是有一半内存空间的浪费。
标记整理算法
根据老年代特点提出的一种算法,标记过程与标记清除算法一样,但是他会先将存活对象放到内存一端,然后清理掉端边界以外的内存。
他不会产生内存碎片但是消耗的时间比较多。
分代算法
分代算法没有新的思想,他将内存区域分为年轻代和老年代然后针对他们的特定选择算法,通常年轻代使用复制算法,老年代使用标记整理算法。
常见的垃圾回收器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收方法的具体实现。
Serial收集器
串行收集器是最基本的收集器,他是一个单线程的收集器,并且他在回收的过程中会全程stopTheWorld。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,其他行为(控制参数,收集算法,回收策略)与Serial收集器一致。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
一般与CMS配合使用。
Parallel Scavenge
他与ParNew收集器类似但是他更关注吞吐量(高效率利用CPU),它提供了很多参数让用户找到最合适的停顿时间或者最大的吞吐量。
新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
PS:JDK1.8 默认使⽤的是 Parallel Scavenge + Parallel Old
Serial Old收集器
Serial收集器的老年代版本。
Parallel Old收集器
Parallel Scavange老年版本。
CMS收集器
CMS(Concurent Mark Sweep)收集器是一种注重减少停顿时间的收集器,更注重用户体验。
他的算法分为几步
- 初始标记 暂停其他所有线程 并记录下与root之间相连的对象 速度很快
- 并发标记 GC线程与用户线程并行,进一步标记根可达对象,由于用户线程的引用会不断更新,所以做不到实时性标记。
- 重新标记 重新标记是为了修正并发标记过程中用户线程继续运行导致的变动 比初始标记慢 但比并发标记快很多 会STW
- 并发清除 开启用户线程 GC线程开始对未标记区域做清理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VYkNPGgT-1615794587960)(C:\Users\A\AppData\Roaming\Typora\typora-user-images\image-20210315153402839.png)]
优点: 并发收集,低停顿。
缺点:使用并发清除 有大量内存碎片产生 ,无法处理浮动垃圾。
浮动垃圾:重新标记(Remark) 的作用在于:
之前在并发标记时,因为是 GC 和用户程序是并发执行的,可能导致一部分已经标记为 从 GC Roots 不可达 的对象,因为用户程序的(并发)运行,又可达 了,Remark 的作用就是将这部分对象又标记为 可达对象。
G1收集器
G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机 器. 以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征。
他将内存区域分为多个大小相同的region 每个region可能是年轻代也可能是老年代,当初发GC时G1有一个STW的目标时间(默认200ms),他会尽力把STW控制在这个时间内,没有清理完的垃圾下一次继续清理。
ZGC
十分优秀的新一代回收器,他的停顿时间会更短。但是目前可能不太稳定。