目录
JVM概念
- JVM(java Virtual Machine java虚拟机)
虚拟机:指通过软件模拟的具有完整的硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。 - 为啥Java可以实现一次编写,到处运行? 因为JVM可以帮助我们处理不同硬件的差异。
- JVM作用:
生成平台无关的字节码,在操作系统以上做一层隔离;
让编程语言设计的更灵活,比如支持垃圾回收等;
有更多的优化手段。
使用最广泛的JVM是 HotSpot
JVM布局组成部分(5部分)
- JVM内存区域
堆 【线程共享】
堆: 所有创建的对象信息都放在了这个区域,堆也是JVM中最大的一块内存。
堆里面的数据划分:
-
新生代:新创建的数据会在新生代,当经历了一定次数的GC(垃圾回收器),依然存活下来的数据,会移动到老年代(HotSpot 默认的垃圾回收次数是15次)。
新生代又有三个区域:Eden、S0、S1(使用的时候搭配使用:Eden+S0、Eden+S1) -
老年代:存放的是经过了一定次数还存活的对象和大对象。
-
堆空间大小的设置:-Xmx10m 设置堆空间的最大值为10mb
-Xms10m 设置堆空间的最小值为10mb -
面试题:为什么大对象会直接存到老年代?
答:因为大对象的创建和销毁所需要的时间比较多,所以性能比较慢,如果存到新生代,那么可能会导致频繁的创建和销毁大对象,从而使整个JVM运行效率降低,所以就直接将大对象存到老年代。
Java虚拟机栈【线程私有】
Java虚拟机栈(先进后出)【线程私有】
局部变量表:8大基础类型和对象的引用信息;
操作栈:每个方法都会生成栈;
动态连接:指向字符串常量池的引用;
方法返回地址:指向的是PC寄存器的地址;
程序计数器【线程私有】
是用来记录线程执行行号的。(为的是下一次执行的时候,能接着上一次执行)
本地方法栈【线程私有】
和JVM栈类似,只不过JVM栈是给Java程序使用的,而本地方法栈是给本地方法(c/c++)使用的。
方法区(永久代)【线程共享】
方法区存储的信息:字符串运行时常量池、常量final、类元数据
在JDK8以前的HotSpot虚拟机中,方法区也被称为"永久代"。JDK1.8后 称为元空间,并且将字符串常量池放到了堆中。
JVM类加载机制
类加载过程:
类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。 例如:JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
-
加载(Loading)
1.通过完整的类路径找到二进制字节流的类;
2.将静态的字节流转换为方法区的数据;
3.在内存中生成方法入口; -
验证
用来验证代码是否正确:1.文件格式的效验;2.字节码效验; -
准备
将类中的静态变量初始化到内存中。 (初始化为类型的默认值)
例如:有这样一行代码:public static int num = 123; 它是初始化num的int值为0(int默认值),而不是123。 -
解析
将常量池中的符号引用替换成方法区的直接引用的过程。 -
初始化
此步骤才将程序的执行权交给程序,也就是说此步骤就开始执行类的相应方法了(构造方法)。 -
使用
-
卸载(销毁)
双亲委派模型
JVM加载子类并不会直接进行类加载,而是将任务交给父类,一层一层的进行传递。如果父类能够加载就加载,不能加载则返回到子类进行加载。如果都不能加载则报错。ClassNotFoundException
- 双亲委派模型优势:
1.唯一性
2.安全性:保证 Java 核心库的类型安全。这种机制保证不会出现用户自己能定义java.lang.Object类等的情况。例如,用户定义了java.lang.String,那么加载这个类时最高级父类会首先加载,发现核心类中也有这个类,那么就加载了核心类库,而自定义的永远都不会加载。 - 破坏双亲委派模型的事件:
历史上有三次破坏了双亲委派模型:
第一次:JDK1.2 引入了双亲委派模型,为了兼容老代码出现了第一次破坏双亲委派模型。
第二次:它是双亲委派模型自身的缺点所导致的,比如当父类出现了调用子类方法时,就破坏了双亲委派模型。
第三次:人们对于热更新,热加载的追求(希望不更新代码就能实现功能),导致了第三次破坏了双亲委派模型。
JVM垃圾回收器
1. 判断死亡对象
- 如何判断对象的死亡?
- 引用计数器算法
给每个对象生成一个对应的技术器,每次在进行引用的时候这个计数器+1,如果撤销引用时计数器-1,GC(垃圾回收器)会根据计数器的值,当此值为0的时候就可以判定此对象为死亡对象。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
缺点 不能进行循环引用。
- 引用计数器算法
/**
- JVM参数 :-XX:+PrintGC
*/
public class Test {
public Object instance = null;
private static int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
Test test1 = new Test();
Test test2 = new Test();
test1.instance = test2;
test2.instance = test1;
test1 = null;
test2 = null;
// 强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
//结果:[GC (System.gc()) 6092K->856K(125952K), 0.0007504 secs]
GC日志包含" 6092K->856K(125952K)",意味着虚拟机并没有因为这两个对象互相引用就不回收他们。即JVM并不使用引用计数法来判断对象是否存活。
- 可达性分析算法
可达性算法是目前JVM使用的判断对象生死的算法。
概念: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索
走过的路径称之为"引用链"。
当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是可回收的。
在Java语言中,可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- 四种引用类型(JDK1.2后):
- 强引用 :xxx = new Object(); 当发生了OOM(内存溢出异常)时,垃圾回收器也不会回收此对象。
- 软引用 :比强引用稍弱,发生OOM之前会对软引用进行回收。
- 弱引用 :比软引用更弱,在下一次垃圾回收的时候就会清除。
- 虚引用 :创建即消亡,它的价值只是在垃圾回收的时候触发一个回调方法。
2. 垃圾回收算法
常见的垃圾回收算法:
- 标记-清除算法:标记:就是可达性算法标记处可回收的对象
不足:
效率问题 : 标记和清除这两个过程的效率都不高
内存碎片问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。 - 复制算法【新生代回收算法】:它将内存按容量分为大小相等的俩块,每次只使用其中的一块,当这块内存需要进行垃圾回收时,先将存活的对象复制到另一块没有使用的内存中,然后将此内存全部清除。
- 标记-整理算法【老年代回收算法】:可以解决内存碎片问题
3. 垃圾回收器
图中并行和并发的解释:
并行(Parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态
并发(Concurrent) : 指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个CPU上。
CMS收集器
CMS收集器目前应用最广泛的垃圾回收器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前应用集中在互联网站或者B/S系统的服务端上。
CMS收集器是基于“标记—清除”算法实现的。优点:并发收集,低停顿。
它的运作过程分为4个步骤:
- 初始标记(CMS initial mark)
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 - 并发标记(CMS concurrent mark)
并发标记阶段就是进行GC Roots Tracing的过程。 - 重新标记(CMS remark)
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。 - 并发清除(CMS concurrent sweep)
并发清除阶段会清除对象。
G1收集器:JDK11默认的垃圾回收器,唯一一个全区域的垃圾回收器。