我们平常开发编写的java语言代码都是是需要JVM虚拟机来进行编译,解析,运行的.JVM会把我们的java源码编译成Class字节码文件,然后就可以在安装了JVM的不同操作平台去运行,也就是java语言”一次编译,到处运行”的跨平台性,这是因为JVM虚拟机屏蔽了与操作平台相关的信息,为程序运行构建了相同的运行环境.
JVM内存模型
JVM在运行时涉及到的存储区域称为运行时数据区,主要包括: 堆(heap)、方法区、程序计数器(pc寄存器)、虚拟机栈(JVM Stacks)、本地方法栈.
其中堆和方法区是所有线程共享的一块内存区域,是线程不安全的;而程序计数器,虚拟机栈,本地方法栈是每个线程私有的内存区域,生命周期与线程相同,是线程安全的.
程序计数器:是一块较小的内存空间,每一个线程都有自己的程序计数器,当cpu切换执行线程时,就记录当前线程执行到的位置,也看做是当前线程所执行的字节码的行号指示器,当cpu切换回来时,就按照之前记录的位置继续执行. (对于Native方法:则为undefined,因为底层已经不是JAVA语言了). 该区域是唯一不会有OOM情况产生的.
虚拟机栈:描述的是java方法执行的内存模型:虚拟机栈中存放着栈帧(Stack Frame),每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、帧数据区等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈:其实它和虚拟机栈功能很类似,只不过它是为虚拟机使用到的底层native方法服务,而虚拟机栈则是为虚拟机执行java方法(也就是字节码)服务.
方法区:它用于存放已被虚拟机加载的类信息,常量,静态变量,以及即时编译器编译后的代码等数据.方法区中有一个运行时常量池,用于存放编译器生成的各种字面量(字符串、final变量)和符号引用(类/接口、方法和字段的名称和描述符等). 方法区的大小决定了系统可以保存多少个类。(JDK8之前该区域叫永久代, JDK8及之后叫元空间, 元空间并不在虚拟机中,而是使用堆外的直接内存)。
堆:堆内存随着JVM 启动而创建.它的唯一目的就是用来存放对象和数组(特殊的对象),几乎所有的对象实例都在这里分配内存。堆中会自动垃圾回收, 回收不需要的对象进而释放内存。堆是垃圾收集器进行GC的最重要的内存区域.
内存溢出: 是指程序在申请内存时,没有足够的内存给申请者使用,比如:给了你一块存储int类型数据的存储空间,但是你却用来存储long类型数据,那么内存肯定不够用,这时候就会报内存溢出,也就是OOM错误.
内存泄漏:是指程序在申请内存后,无法释放已经申请过的内存空间,也许一次内存泄漏不会有大的影响,但是内存泄漏不断堆积,就会造成上面说的内存溢出. 内存泄漏的实质其实就是长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要了,但是因为长生命周期的对象持有它的引用不能被回收。
类加载机制:
JVM的类加载机制分为五步,加载(Load)——验证——准备——解析——初始化(initialize),其中验证、准备、解析统称为链接(Link).
加载: 通过类的全路径名称,读取类的二进制数据流。解析类的二进制数据流,转化为方法区(永久代or元空间)内部的数据结构。创建java.lang.Class类的实例对象,表示该类型。
验证: 保证加载进来的字节码是合法且符合规范的。 大致分为4部分:
- 1.格式检查:检查魔数、版本、长度等等。
2.语义检查:抽象方法是否有实现类、是否继承了final类等等编码语义上的错误检查。
3.字节码验证:跳转指令是否指向正确的位置,操作数类型是否合理等。
4.符号引用验证:符号引用的直接引用是否存在.
准备: 正式为类变量分配内存并设置类变量的初始值阶段,即:在方法区中分配这些变量所使用的内存空间。
// 实际上变量v在准备阶段过后的初始值为0而不是8080,
// 将v赋值为8080的指令是程序被编译后,存放于类构造器<clinit>方法之中。
public static int v = 8080;
// 但是注意,如果声明为:public static final int v = 8080;
// 在编译阶段会为v生成ConstantValue属性,
// 在准备阶段虚拟机会根据ConstantValue属性将v赋值为 8080。
public static final int v = 8080;
解析: 是指虚拟机将运行时常量池中的符号引用替换为直接引用的过程。
初始化: 该阶段类就可以顺利加载到系统中。此时,类才会开始执行J行Java字节码。初始化阶段是执行类构造器<clinit>方法的过程。
3种类加载器:
启动类加载器(Bootstrap ClassLoad)(负责加载 JAVA_HOME\lib rt.jar)
扩展类加载器(Extension ClassLoad)(负责加载 JAVA_HOME\lib\ext)
应用程序类加载器(Application ClassLoad)(负责加载用户路径(classpath)上的类库)
双亲委派机制:
当类加载器收到了一个类加载的请求,它并不会尝试自己先去加载,而是把这个请求委托给父类加载器去执行,如果当前父类加载器还存在父类加载器,则依次向上委托,请求最后到达顶层的启动类加载器,如果父类能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成加载请求(在他的加载路径下找不到所需加载的class文件),子类加载器才会尝试自己去加载,如果所有类加载器最终都无法完成加载请求,就会报错ClassNotFound.
这里说的父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码.
垃圾回收
如何定义垃圾
垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,释放内存空间。两种算法进行定义对象是否可回收: 引用计数法和可达性算法(可触及算法/引用链法/根搜索算法/追踪性垃圾收集).
引用计数法:是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0 时,那么该对象就可以被回收。
它的缺陷就是无法解决循环引用问题,比如:当对象A引用对象B,对象B又引用对象A,那么此时对象A、B的引用计数器都不为零,即使这2个对象已经不可能再被访问了,也永远无法通知GC收集器回收它们,所以主流的虚拟机都不会采用这种算法。
可达性算法:从作为根节点(GC Roots)的对象开始向上搜索,当一个对象到 GC Roots 之间没有任何引用链相连时(即从GC Roots节点到该节点不可达/不可触及),则证明该对象是不可用的,就可以进行回收.
可作为GC Roots的对象包括以下4种:
1.虚拟机栈(栈帧中的局部变量表)中引用的对象(常用)
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中(Native方法)引用的对象
可触及性分为3种状态:
① 可触及:从根节点开始,可以到达某个对象。
② 可复活:对象引用被释放,但是可能在finalize()函数中被初始化复活。
③ 不可触及:由于finalize()只会执行一次, 所以错过这一次复活机会的对象,则为不可触及状态
四种引用级别和垃圾回收关系(基本常见强引用):
垃圾回收算法
垃圾收集算法有多种,我们常见的几种垃圾收集算法比如:标记清除算法、复制回收算法、标记整理算法、分代算法、分区算法。目前JVM一般使用后两者较多.
标记清除算法(Mark-Sweep):它是最基础的一种垃圾回收算法。先标记,再清除。
缺点是内存碎片问题:当我们回收完成,释放出的内存就成了很多段的碎片空间。我们知道开辟内存空间时,需要的是连续的内存区域,比如我们需要一个2M的内存区域,其中有2个1M 是没法用的。这样就导致本身还有这么足够的内存,但因为是不连续的却用不了。
复制回收算法(Copying):是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效.
缺点是:始终只能使用一半的内存,付出的内存空间代价太大.
但是新生代中的内存划分为8:1:1,也就是可使用90%的空间,而且对象大多为朝生夕死, 所以新生代使用的就是复制算法, 只需要复制少量对象就可完成垃圾回收.
标记整理算法(Mark-Compact):标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉该端边界以外的内存区域.
标记整理算法一方面在标记清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。但是它对内存变动更频繁,需要整理几乎所有存活对象的引用地址,在效率上比复制算法要差很多。
而老年代中的对象基本都是存活较久或者比较大的对象,对象存活率高,每次GC只清除少部分对象,而且频率没年轻代那么高,因此适合标记整理算法.
分代收集算法:严格来说并不是一种具体算法,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合算法。也就是针对新生代和老年代(内存默认为1:2)分别使用最为合适的回收算法.
STW(Stop the World): 是指在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集器线程之外的线程都被挂起。此时系统只允许GC线程运行,等待GC线程执行完毕后才能再次运行。内存越大,STW机制的时间也越长,所以内存也不仅仅是越大就越好.
分区算法: 将堆空间划分成连续的不同小区间,每个区间独立使用、回收。由于当堆空间大时,一次GC的时间会非常耗时,那么可以控制每次回收多少个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
JVM垃圾收集器
串行回收器 - Serial
串行回收器也叫Serial收集器,是最古老收集器。它在JDK1.3之前是虚拟机新生代收集器的唯一选择。它是单线程执行回收操作的。它的特点就是,在单核或内核少的计算机来说,有更好的性能表现。它的优点就是简单高效。
ParNew
ParNew是一个新生代的回收器,也是一个独占式的回收器,它与串行回收器唯一不同的,就是它采取并发方式执行GC。但是在cpu核数少的机器,它的性能很可能比串行回收器差。
默认情况下,当CPU数量<=8个的时候,并行线程数为8个。如果CPU数量>8,并行线程数量为:3+(5*cpu_nums/8)
ParallelGC
ParallelGC也是新生代的回收器,也采用的复制算法执行GC回收任务。它与ParNew有一个不同点就是,它提供了一些设置系统吞吐量的参数用来控制GC行为。
-XX:MaxGCPauseMillis:最大的垃圾收集暂停时间
-XX:GCTimeRatio:设置吞吐量大小,可设置的值为0~100之间的整数
-XX:+UseAdaptiveSizePolicy:如果你不倾向手动设置上面的参数,可以采用把参数调整交由虚拟机自动设置
Parallel Old GC
它跟ParallelGC相似,也是关注于吞吐量的收集器。是一个应用于老年代的回收器。可以与ParallelGC搭配使用,即:ParallelGC(新生代收集器)+ ParallelOldGC(老年代收集器)。 它采用标记压缩算法进行GC操作。也可以使用-XX:ParallelGCThreads来指定并行GC的线程个数。
启用指定收集器参数:
CMS
CMS全称为Concurrent Mark Sweep,即:并发标记清除。 它采用的是标记清除算法。也是多线程并发执行器。分为如下6个步骤
① 初始标记(STW):标记根对象
② 并发标记:标记所有对象
③ 预清理:清理前的准备以及控制停顿时间(可以采用-XX:-CMSPrecleaningEnabled关闭,不进行预清理)
④ 重新标记(STW):修正并发标记数据
⑤ 并发清理:清理垃圾(真正的执行垃圾回收)
⑥ 并发重置:重置状态等待下次CMS的触发
为什么要有预清理?
因为第4步重新标记是独占CPU的,如果YoungGC发生后,立即触发一次重新标记(新生代使用ParNew回收器),那么一次停顿时间可能很长,为了避免这种情况,预处理时,会刻意等待一次新生代GC的发生,然后根据历史数据预测下一次YoungGC的时间,在当前时间和预测时间取中间时刻执行重新标记操作,目的就是尽量避免YoungGC与重新标记重叠执行,从而减少一次停顿时间。
G1
G1全称Garbage First Garbage Collector,优先回收垃圾比例最高的区域。G1收集器将堆划分为多个区域,每次收集部分区域来减少GC产生的停顿时间。一般为3个阶段
1.新生代GC
新生代GC的主要工作就是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。回收后,所有的eden区都应该被清空,而survivor区会被收集一部分数据,但是应该至少仍然存在一个survivor区。如下图所示:
2.并发标记周期
初始标记(STW):标记从根节点直接可到达的对象。这阶段会伴随一次Young GC,会产生STW(全局停顿),应用程序会停止执行。
根区域扫描:由于Young GC的发生,所以初始标记后,eden被清空,存活对象放入Survivor区。然后本阶段,则扫描survivor区,标记可直达老年代的对象。本阶段应用程序可以并行执行。但是,根区域扫描不能和YoungGC同时执行(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),因此如果恰巧在此时需要进行YoungGC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次YoungGC的时间就会延长。
并发标记:用来再次扫描整个堆的存活对象,并做好标记。与CMS类似,该阶段可以被一次Young GC打断。
重新标记(STW):本阶段也会发生STW,应用程序会停止执行。由于并发标记阶段中,应用程序也是并发执行的,所以本阶段,对标记结果进行最后的修正处理。
独占清理(STW):本阶段也会发生STW,应用程序会停止执行。它用来计算各个区域的存活对象和GC回收比例,然后进行排序,从而识别出可以用来混合收集的区域。该阶段给出了需要被混合回收的区域并进行了标记,那么在混合收集阶段,是需要这些信息的。
并发清理:本阶段会去识别并清理那些完全空闲的区域。
3.混合收集
在第二步的并发标记周期过程中,虽然有部分对象被回收,但是总体回收比例还是比较低的。由于G1已经明确知道哪些区域含有比较多的垃圾比例,所以就可以针对比例较高的区域进行回收操作。
JVM监控工具
jps: 用于列出Java的进程。执行语法: jps [-options]
jstat: 用于查看堆中的运行信息。执行语法:jstat –help jstat -options
jinfo: 用于查看运行中java进程的虚拟机参数。执行语法:jinfo [option] <pid>
jstack: 命令用于导出指定java进程的堆栈信息。执行语法:jstack [-l] <pid>
jcmd: 命令用于导出指定java进程的堆栈信息,查看进程,GC等。