参考资料
- JVM极简教程
JVM结构
- JIT编译器,对于经常需要执行的字节码进行
类加载子系统
类加载器
tomcat的自定义类加载器
为了进行类的隔离,如果Tomcat直接使用AppClassLoader类加载类,那就会出现如下情况:
- 应用A中有个com.example.Hello.class,应用B中也有个com.example.Hello.class,虽然都叫做Hello,但是具体的方法、属性可能不一样
- 如果AppClassLoader先加载了应用A中的Hello.class,那么应用B中的Hello.class就不可能再被加载了,因为名字是一样
- 就需要针对应用A和应用B设置各自单独的类加载器,也就是WebappClassLoader
- 这样两个应用中的Helo.class都能被各自的类加载器所加载,不会冲突
- 这就是Tomcat为什么用自定义类加载器的核心原因,为了实现类加载的隔离
- JVM中判断一个类是不是已经被加载的逻辑是:类名+对应的类加载器实例
运行时数据区
程序计数器
- 是物理寄存器的抽象实现
- 用来记录待执行的下一条指令的地址3.它是程序控制流的指示器,循环、if else、异常处理、线程恢复等都依赖它来完成
- 解释器工作时就是通过它来获取下一条需要执行的字节码指令的
- 唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域
虚拟机栈(java方法栈)
每个线程在创建时都会创建一个虚拟机栈,栈内会保存一个个的栈帧,每个栈帧对应一个方法。
- 虚拟机栈是线程私有的
- 一个方法开始执行栈帧入栈、方法执行完对应的栈帧就出栈,所以虚拟机栈不需要进行垃圾回收
- 虚拟机栈存在OutOfMemoryError,以及StackOverflowError
- 线程太多就可能会出现DutOfMemoryError,即线程创建时没有足够的内存去创建虚拟机栈
- 方法调用层次太多,就可能会出现StackOverflowError
- 可以通过
-Xss
来设置虚拟机栈的大小
栈帧结构
操作数栈(操作栈),栈帧中的一部分,用来在执行字节码指令过程中用来进行计算
public static void main(Stringp[] args){
int a = 10;
int b = 20;
int c = a + b;
}
- bipush,压栈10
- istore_1,从操作数栈存储到局部变量表1(a)
- iload_1,从局部变量读取变量1到栈
- iadd,加法操作
- return,方法执行完毕,栈帧清空
本地方法栈
- 本地方法:native method,在java中定义的方法,但由其他语言实现。
- 虚拟机栈存的是java方法调用过程的栈帧,本地方法栈存的是本地方法调用过程中的栈帧。
- 线程私有的,也可能会出现OOM和SOF
堆区
堆是VM中最重要的一块区域,JVM规范中规定所有的对象和数组都应该存放在堆中,在执行字节码指令时会把创建的对象存入堆中,对象对应的引用地址存入虚拟机栈中的栈帧中。当方法执行完之后,刚刚所创建的对象并不会立马被回收,而是要等JVM后台执行GC后,对象才会被回收。
-Xms
,ms(memory start),指定堆的初始化内存大小,等价于-XX:InitialHeapSize
-Xmx
,mx(memory max),指定堆的最大内存大小,等价于-XX:MaxHeapSize
- 一般会把
-Xms
和-Xmx
设置为一样,这样VM就不需要在GC后去修改堆的内存大小了,提高了效率 - 默认情况下,初始化内存大小=物理内存大小/64,最大内存大小=物理内存大小/4
新生代存储刚刚创建的对象,老年代存储经过垃圾回收之后仍然存活的对象
- 可以通过
-XX:NewRatio
参数来配置新生代和老年代的比例,默认为2,表示新生代占1,老年代占2,也就是新生代占堆区总大小的1/3 - 一般是不需要调整的,只有明确知道存活时间比较长的对象偏多,那么就需要调大NewRatio,从而调整老年代的占比
新生代又分为
- Eden:伊甸园区,新对象都会先放到Eden区(除非对象的大小都超过了Eden区,那么就只能直接进老年代)
- S0、S1:Survivor0、Survivor1区,也可以叫做from区、to区,用来存放MinorGC(YGC)后存在的对象
- 默认情况下(Eden区:S0区:S1区)的比例关系为(8:1:1),也就是Eden区占新生代大小的8/10,可以通过
-XX:SurvivorRatio
来调整
总体过程如下
- 新对象会创建在Eden区,当创建新对象无法在Eden存储时(已满),就会触发YGC(新生代垃圾回收)
- 经过一轮gc后仍然存活的对象会存放到s0区
- 新对象仍旧创建在Eden区,经过第二次垃圾回收后,仍旧存活的对象(包括Eden中的对象)被存放在s1区
- 下一次s1中没有经过gc的对象会存储到s0区,s0和s1之间不断交换
- 当超出一定gc次数的时候,对象被存储到老年代
如果Eden对象太大无法存储在s0和s1中,也会直接转移到老年代
对于超大对象,不会在Eden创建,直接存储到老年代
垃圾回收器
-
Young GC/Minor GC:负责对新生代进行垃圾回收
-
Old GC/Major GC:负责对老年代进行垃圾回收,目前只有CMS垃圾收集器会单独对老年代进行垃圾收集,其他垃圾收集器基本都是整堆回收(Full GC)的时候对老年代进行垃圾收集
-
Full GC:整堆回收,也会堆方法区进行垃圾收集
垃圾指在JVM中没有任何引用指向的对象,不清理就会导致持续的内存占用(无法给其他对象使用),最终OOM
垃圾回收方式
- 引用计数法
- 可达性分析法
引用计数法
每个对象都保存一个引用计数器属性,用户记录对象被引用的次数。
-
优点,实现简单,计数器为0则表示是垃圾对象
-
缺点,需要额外的空间来存储引用计数,以及需要额外的时间来维护引用计数,但是无法处理循环引用的问题
可达性分析法
以GC Roots作为起始点,然后一层一层找到所引用的对象,被找到的对象就是存活对象,那么其他不可达的对象就是垃圾对象。
GC Roots是一组引用,包括:
- 线程中虚拟机栈中正在执行的方法中方法参数、局部变量所对应的对象引用
- 线程中本地方法栈中正在执行的方法中方法参数、局部变量所对应的对象引用
- 方法区中保存的类信息中静态属性所对应的对象引用
- 方法区中保存的类信息中常量属性所对应的对象引用
- 等等
垃圾回收算法
标记清除(Mark-Sweep)算法
一种非常基础和常用的垃圾回收算法,针对某块内存空间,比如新生代、老年代,如果可用内存不足后,就会STW(Stop the world,在执行垃圾回收的过程冻结所有用户线程的运行,直到垃圾回收线程执行结束),暂定用户线程的执行,然后执行算法进行垃圾回收:
- 标记阶段:从GC Roots开始遍历,找到可达对象,并在对象头中进行记录
- 清除阶段:堆内存空间进行线性遍历,如果发现对象头中没有记录是可达对象,则回收它
缺点,效率不高,会产生内存碎片
优点,思路简单
复制(Copying)算法
将内存空间分为两块,每次只使用一块。在进行垃圾回收时,将可达对象复制到另外没有被使用的内存块中,然后再清除当前内存块中的所有对象,后续再按同样的流程进行垃圾回收,交替进行。
优点:
- 没有标记和清除阶段,通过GC Roots找到可达对象,直接复制,不需要修改对象头效率高
- 不会出现内存碎片
缺点:
- 需要更多的内存,始终有一半的内存空闲
- 对象复制后,对象存放的内存地址发生了变化,需要额外的时间修改栈帧中记录的引用地址
- 如果可达对象比较多,垃圾对象比较少,那么复制算法的效率就会比较低,所以垃圾对象多的情况下(Eden区),复制算法比较适合
标记-整理(Mark-Compact)算法
分为三个阶段
- 和标记清除算法一样,从GC Roots找到并标记可达对象
- 将所有存活对象移动到内存的一端
- 最后清理边界外所有的空间
标记整理算法相当于标记清除算法执行完一次之后再进行一次内存整理
优点:
- 不会出现内存碎片
- 不需要利用额外的内存空间
缺点:
- 效率要低于标记清除算法、复制算法
- 需要修改栈帧中的引用地址
垃圾回收算法的比较
Mark-Sweep | Mark-Compact | Copying | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(有碎片) | 少(无碎片) | 最多 |
移动对象 | 否 | 是 | 是 |
分代收集算法(理念)
不同对象的存活时长是不一样的,也就可以针对不同的对象采取不同的垃圾回收算法。
默认几乎所有的垃圾收集器都是采用分代收集算法进行垃圾回收的。
我们会把堆分为新生代和老年代:
- 新生代中的对象存活时间比较短,那么就可以利用复制算法,它适合垃圾对象比较多的情况。
- 老年代中的对象存活时间比较长,所以不太适合用复制算法,可以用标记-清除或标记-整理算法,比如:
- CMS垃圾收集器采用的就是标记-清除算法
- Serial Old垃圾收集器采用的就是标记-整理算法
常见垃圾收集器
CMS(并发标记清除算法)
下图中蓝色表示用户线程,橙色表示gc线程
CMS整个垃圾收集过程更长了,但是STW的时间变短了。
在垃圾收集过程中大部时间用户线程也还在执行,所以用户体验更好了,但是吞吐量更低了(单位时间内执行的用户线程更少了)
(1)初始标记:STW,暂停所有工作线程,然后标记出GC Roots能直接可达的对象。一旦标记完,就恢复工作线程继续执行,这个阶段比较短
(2)并发标记:从上一个阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象。耗时会比较长,但是不需要STW,用户线程与垃圾收集线程一起执行。三色标记
(3)重新标记:上个阶段标记的对象,可能有误差,需要进行修正需要STW,但是时间也不是很长。增量更新
(4)并发清除:重置标记方便下次gc,删除垃圾对象由于不需要移动对象,这个阶段也可以和用户线程一起执行,不需要STW
concurrent mode failure
如果在并发标记、并发清理过程中,由于用户线程同时在执行,如果有新对象要进入老年代,但是空间又不够,那么就会导致"concurrent mode failure”(垃圾回收速度跟不上创建对象的速度),此时就会利用Serial Old来做一次垃圾收集,就会做一次全局的STW。
在并发清理过程中,可能产生新的垃圾,这些就是“"浮动垃圾”,只能等到下一次GC时来清理
由于采用的是标记-清除,所以会产生内存碎片,可以通过参数-XX:+UseCMSCompactAtFullCollection
可以让VM在执行完标记-清除后再做一次整理,也可以通过-XX:CMSFullGCsBeforeCompaction
来指定多少次GC后来做整理,默认是O,表示每次GC后都整理。
G1垃圾回收算法
-
每一个方块叫做region,堆内存会分为2048个region,每个region的大小等于堆内存除以2048
-
仍旧区分了Eden区、S0区、S1区、老年代,只不过空间可以是不连续的了(逻辑连续,物理分离)
-
Humongous区是专门用来存放大对象的(如果一个对象大小超过了一个region的50%,那么就是大对象
(1)初始标记(同CMS的初始标记阶段):STW,暂停所有工作线程然后标记出GC Rootsi能直接可达的对象一旦标记完,就恢复工作线程继续执行,这个阶段比较短
(2)并发标记(同CMS的初始标记阶段):从上一个阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象耗时会比较长但是不需要STW,用户线程与垃圾收集线程一起执行。三色标记
(3)最终标记(同CMS的重新标记):上个阶段标记的对象,可能有误差,需要进行修正需要STW,但是时间也不是很长原始快照
(4)筛选回收(类似CMS的并发清除阶段)需要STW,来清除垃圾对象可以通过-X:MaxGCPauseMillis
:来指定GC的STW停顿的时间(并非按照垃圾对象的数量进行gc,而是按照指定的时间执行gc),所以可能并不会回收掉所有垃圾对象,默认200ms采用的复制算法。不会产生碎片(会把某个region.里的对象复制到另外空闲regionl区域,比如相邻的)
YoungGC:Eden区满,就会触发G1的YoungGC,对Eden区进行
GC MixedGC:老年代的占用率达到了-XX:InitiatingHeapOccupancyPercent
指定的百分比,回收所有的新生代以及部分老年代,以及大对象区
FullGC:在进行MixedGC:过程中,采用的复制算法,如果复制过程中内存不够,则会触发FullGC,会STW,并采用单线程来进行标记-整理算法进行GC,相当于用一次Serial GC