JVM速记
什么是JVM
JVM:java虚拟机,是一个虚拟出来的计算机,支持跨平台可以运行在linux、windows、Mac Os等平台上,用于运行java源码编译成的字节码文件(.class文件),不同平台的jvm是不同的,jvm需要把java字节码文件解释成不同平台的机器指令从而达到跨平台使用,做到java代码一次编译处处运行。
JVM体系结构
类加载子系统
加载
- BootstrapClassLoader:jre/lib下的的jar包的类
- ExtClassLoader:加载jre/lib/Ext下的jar包的类
- AppClassLoader:加载当前应用classpath指定的类加载路径
双亲委派
避免类的重复加载,与核心api被篡改
通过源码可知道AppClassLoader去加载类时会先让parent(ExtClassLoader)去加载这个类,ExtClassLoader的parent不存在所以会让去BootstrapClassLoader去加载类。即自己先不加载,不断向上层请求加载直到顶层,如果顶层能够加载或者已经加载过了,那底层则不再需要重复加载并且可以避免底层核心api被篡改,比如自己去定义一个java,lang.String,由于上层类加载器已经加载过了,所以这个自定义的String类不会再被加载
加载
- 验证:验证待加载的字节码文件是否正确
- 准备:为static变量分配内存并赋初始化值(即零值)
- 解析:将符号引用解析为直接引用
初始化
运行时数据区
程序计数器
用来记录待执行的下一条记录的地址,是唯一一个jvm规范中没有规定任何内存溢出OutOfMemoryError情况的区域。是物理机寄存器的抽象实现,程序的控制流循环、if…else…、异常处理都是依靠他来完成,解释器工作时就是通过他来获取下一条需要执行的字节码指令的
虚拟机栈(java栈)
每个线程再创建时都会创建一个虚拟机栈,栈内会保存一个个栈帧,每个栈帧对应一个方法。虚拟机栈是线程私有的。会存在存在内存溢出和栈溢出
OutOfMemoryError(OOM)内存溢出的情况:线程太多,每个线程都会一个独立的虚拟机栈,线程太多可能导致内存溢出情况。
StackoOverflowError(SOF):每个方法都会有一个栈帧,当某一个虚拟机栈方法的嵌套太多层,导致栈帧太多超过堆栈大小限制。
一个方法开始执行就会入栈,方法执行完成后就会出栈,所以虚拟机栈不需要GC进行垃圾回收
本地方法区
本地方法:native method,再java定义的方法,但由其他语言实现的。
虚拟机栈存的是java方法调用过程的栈帧,本地方法栈寸的是本地方法调用过程的栈帧,也会出现OOM和SOF
堆(heap)
JVM中最重要的一块区域,JVM规定所有的对象和数组都应该存放在堆中,再执行完字节码指令时会把创建的对象放入堆中,对象的引用地址则存入虚拟机栈(java栈)中的栈帧。不过当方法执行完之后创建的对象并不会马上被回收,而是要等JVM后台执行GC后,对象才会被回收。
-Xms:ms(memory start)指定堆的初始化内存大小,等价于-XX:InitialHeapSize
-Xmx:mx(memory max)指定堆的最大内存大小,等价于-XX:MaxHeapSize
一般会把-Xms和-Xmx设置成一样大这样JVM就不需要再执行完GC后去修改堆内存大小,提高了效率
默认情况下,初始化大小=物理内存大小/64,最大内存大小=物理内存大小/4
分为新生代和老年代可以通过 -XX:NewRatio参数配置新生代和老年代的比例,默认值是2,表示新生代占1,老年代占2,一般不需要调整,只有再明确知道存活时间较长的对象偏多的时候才需要调整这个比例。
新生代又分为Eden区、Survivor0、Survivor1,默认占比是8:1:1,可以通过-XX:SurvivorRatio调整。
Eden区:新对象创建会先放到Eden区,除非对象大小超过Eden区,即会直接放入老年代。
Survivor0、Survivor1:也可以叫from区、to区,用来存放被MinorGC(YoungGC)后存在的对象
对象如何在堆上存放的:新创建的对象存放再Eden区,经历一次MinorGC后还存活的对象存入Survivor0、Survivor1,并记录经历MinorGC次数,经历过MinorGC后还存活的对象每次经历MinorGC后还存活会在Survivor0、Survivor1之间存放。当对象经历了15次MinorGC后再经历一次MinorGC后仍然存活则会进入老年代。如果对象创建时,因为对象太大无法放入Eden区也会直接进入老年代。如果Eden区上的对象太大经历一次MinorGC还存活但无法放入Survivor0、Survivor1则也会直接进入老年代
MinorGC/YoungGC:负责堆新生代进行垃圾回收
MajorGC/OldGC:负责堆老年代进行垃圾回收
FullGC:对整堆进行垃圾回收
目前只有CMS垃圾回收器会单独对老年代进行垃圾回收,其他垃圾回收器基本都是整堆回收的时候才会对老年代进行垃圾回收。
垃圾回收
垃圾是指再JVM中没有任何引用指向他的对象,java代码中一个方法执行完后对应的栈帧就会从该线程的虚拟机栈出栈,栈帧中引用指向的对象人让存在,但是没有引用指向这些对象则是垃圾。如果不清理这些对象,那么就会一直占用这内存,不断堆积,导致没有足够的空间给其他对象使用,从而导致OOM
垃圾标记阶段
也就是找到JVM中的垃圾对象(主要是堆中)
一、引用计数法
每个对象保存一个引用计数器属性,用户记录对象被引用次数
优点:实现简单,计数器为0则表示是垃圾对象
缺点:
- 需要额外的空间来存储引用计数
- 需要额外的时间来维护引用计数
- 无法处理循环引用的问题
二、可达性分析法
可达性分析法会以GCRoot作为起始点,然后一层一层找到引用对象,被找到的对象则是存活对象,其他则是垃圾对象。
GCRoot可以是:
- 线程中虚拟机栈中正在执行的方法的参数、局部变量对应的对象引用
- 线程本地方法栈中正在之心的方法的参数、局部变量对应的对象引用
- 方法区中保存的类信息的静态属性对应的对象引用
- 方法区中保存的类信息的常量属性对应的对象引用
- 等等
标记-清除算法
一种非常基础常用的垃圾回收方法,针对某块空间;比如新生代、老年代如果内存不够时就会STW,暂停用户线程的执行然后执行算法进行垃圾回收。
- 标记阶段:从GCRoot开始便利找到可达对象,在对象头中进行标记
- 清除阶段:堆内存空间进行线性便利,如果发现对象头中没有标记是可达对象,则清除他
缺点:
- 效率不高
- 会产生内存碎片,清楚后内存空间不是连续的。
复制算法
把内存分成两块,只使用其中一块,在清除时,先从GCRoot找到可达对象,直接复制到另一块空间,然后把原先空间整个清楚。
优点:
- 没有标记清除阶段,找到可达对象,直接复制,不需要修改对象头,效率高。
- 不会出现内存碎片。
缺点:
- 需要更多内存,始终有一半内存空闲
- 对象复制后,对象存放引用地址发送改变,需要额外的时间去修改栈帧中的引用地址。
- 如果可达对象比较多的情况下,垃圾对象比较少那么复制算法效率会比较低
标记-整理算法
-
第一阶段:从GCRoot开始便利找到可达对象,在对象头中进行标记
-
第二阶段:把可达对象移动到一端
-
第三阶段:清理边界空间
优点:
- 不会出现内存碎片
- 也不需要额外的内存空间
缺点:
- 效率要低于标记-清除算法和复制算法
- 也需要改变对象引用地址
不同的对象存活时间不一样也就可以针对不用的对象采用不同的垃圾回收算法
默认激活所有垃圾收集器都是采用分代收集算法
把堆分为新生代和老年代
- 新生代中的对象存活比较短,可以利用复制算法,适合垃圾对象比较多的情况
- 老年代对象存辉时间比较长,不适合复制算法,可以使用标记-清除或者标记整理算法比如:CMS垃圾收集器采用的就是标记-清除算法,Serial Old垃圾收集器采用的是标记-整理算法