(一)认识JVM
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统。
常⻅的虚拟机:JVM、VMwave、Virtual Box。
(二)JVM运行流程
程序在执行前要把java代码转为.class文件,JVM首先要将.class文件通过类加载器加载到运行时数据区,但是.class文件不可以直接给底层操作系统执行,需要特定的解析器将字节码翻译成底层系统指令再给CPU去执行,这个过程需要调用其他语言的接口---本地库接口
(三)JVM的内分划分
JVM的内存由五部分组成:
1.堆
堆用来存储我们new出来的对象,并且堆是所有线程共享的
堆里面分为两个区域:新生代和老生代,新生代存放新建的对象,当个经过数次GC后还存活的对象会放入老生代,具体我们会在JVM垃圾回收机制中说明
2.栈
栈分为两种,一种时虚拟机栈,一种是本地方法栈,栈是线程私有的,每个线程都有自己的虚拟机栈,且生命周期和线程是相同的。
栈具体描述的是JAVA方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用来存储局部变量等信息
java虚拟机中的栈叫虚拟机栈,本地方法的栈叫本地方法栈
3.程序计数器
程序计数器也是线程私有的,用来存放下一条指令的地址,如果当前线程正在执⾏的是⼀个Java⽅法,这个计数器记录的是正在执⾏的虚拟机字节码指令的地址;如果正在执⾏的是⼀个Native⽅法,这个计数器值为空。
4.元数据区(方法区)
用来存储虚拟机加载的类信息,常量和静态变量等数据的
JDK 1.8 元空间的变化
1. 对于 HotSpot 来说,JDK 8 元空间的内存属于本地内存,这样元空间的⼤⼩就不在受 JVM 最内
存的参数影响了,⽽是与本地内存的⼤⼩有关。
2. JDK 8 中将字符串常量池移动到了堆中。运⾏时常量池
运⾏时常量池是⽅法区的⼀部分,存放字⾯量与符号引⽤。
字⾯量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引⽤ : 类和结构的完全限定名、字段的名称和描述符、⽅法的名称和描述符。
(四)JVM的类加载机制
对于一个类来说,我们需要经过加载->验证->准备->解析->初始化这五个步骤
接下来我们看看具体每个步骤要如何执行内容
1.加载
加载阶段是整个类加载中的一个过程,我们需要在这个阶段完成:
1)通过⼀个类的全限定名来获取定义此类的⼆进制字节流。
2)将这个字节流所代表的静态存储结构转化为⽅法区的运⾏时数据结构。
3)在内存中⽣成⼀个代表这个类的java.lang.Class对象,作为⽅法区这个类的各种数据的访问⼊。
2.验证
我们加载阶段生成了一个.class文件,我们要确保这个.class文件是正确的,包含的信息符合我们的要求,验证就是为了确保.class文件正确性的,我们需要验证:文件格式,字节码,符号引用等。
3.准备
准备是为类中定义的2变量分配内存并且设置类变量的初始值的,我们定义了变量在这个阶段,还没有被赋值,只有初始值,我们这时候看,就全为0
4.解析
解析阶段是 Java 虚拟机将常量池内的符号引⽤替换为直接引⽤的过程,也就是初始化常量的过程
5.初始化
初始化阶段,我们JVM才开始真正执行类中编写的程序代码,将主导权交给程序
双亲委派模型
自从JDK1.2以来,java就一直保持三层类加载器,双亲委派的类加载架构器
那我们来说一下他的工作过程:如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。
由上到下我们查找的顺序是:先去标准库下查找,查找不到再去扩展库中查找,如果仍然查不到就会去第三方库或者我们自己写的方法中查找
优点:1.避免类的重复加载,如果a和b都有一个父类c,那么当a启动时就会把c加载起来,那么b类在进行加载时就不需要再重复加载c了
2.安全性:使⽤双亲委派模型也可以保证了 Java 的核⼼ API 不被篡改,如果没有使⽤双亲委派模
型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object
类的话,那么程序运⾏的时候,系统就会出现多个不同的 Object 类,⽽有些 Object 类⼜是⽤⼾⾃
⼰提供的因此安全性就不能得到保证了
(五)JVM的垃圾回收
上面说程序计数器,栈的生命周期是和线程有关的,当线程结束时,内存需要跟着线程一起挥手,所以我们现在就来说一下JVM时如何进行垃圾回收的。
Java堆中存放着⼏乎所有的对象实例,垃圾回收器在对堆进⾏垃圾回收前,⾸先要判断这些对象哪些还存活,哪些已经"死去"。判断对象是否已"死"有如下⼏种算法
如何判断对象不再需要
1)引用计数器
就是跟名字一样,给对象加一个计数器,如果有地方引用他就会计数器+1,如果引用失效了计数器就-1,任何时刻计数器为0,就说明对象可以被挥手了
优点:实现简单,方便理解,同时效率较高 缺点:无法解决对象循环引用(类似多线程中的死锁问题)
2)可达性分析
我们JVM就是通过可达性分析来检测对象是否存活
核心思想就是遍历,通过根对象作为起始点,从这结点开始向下遍历,走过的路径称为引用链,当一个对象没有任何引用链可以到达时,证明这个对象是不可用的
比如上图中的object 5和object6 7,虽然她们之间有关联,但是无法由根对象遍历到,所以我们认为它们是不可达的,就会被判定成可回收对象
在java中可以被当作根对象的有:
1. 虚拟机栈(栈帧中的本地变量表)中引⽤的对象;
2. ⽅法区中类静态属性引⽤的对象;
3. ⽅法区中常量引⽤的对象;
4. 本地⽅法栈中 JNI(Native⽅法)引用的对象
垃圾回收算法
1)标记-清除算法
标记清除就是标记出所有需要回收的对象,在标记完成后统一回收
但是这种算法会导致内存碎片化,也会有效率问题
内存碎片化:标记清除后会产生大量不连续的内存碎片,内存碎片过多会导致我们之后要分配一块连续的空间时,可能无法找到足够的内存
2) 复制算法
复制算法就是对上述的标记清除算法进行优化,他是将内存分成大小相等的两块,每次只使用其中一边,当这块内存需要垃圾回收时,我们就会将这块区域仍然存活的对象复制到另一边,然后再将这里的内存区域一次性清理,这样既可以提升效率,也可以避免内存碎片化。
但是仍然有一定的问题,这会导致我们无法充分利用我们的内存,每次都会损失一般的内存空间来复制
3)标记整理算法
复制算法在面对对象存活率较高的情况下,每次都需要复制很多对象,效率也不会很高,所以在面对对象存活率较高时,我们可以是以哦那个标记整理算法,在刚开始标记无用对象时与标记清除一直,但是之后我们却不是直接删除,而是让所有存活的对象向一端移动,再清理掉边界以外的内存即可
但同时,如果存活元素过多也会有效率问题
4)分代算法
分代算法是通过区域划分:新生代和老年代,实现不同区域不同策略的垃圾回收机制(当前我们的JVM都是使用分代算法解决垃圾回收机制)。
新生代中,每次垃圾回收都会有很多对象死去,只有少量存活,我们才用复制算法,而老年代存活对象较多,我们可以采用标记整理或者标记清理算法。
那什么对象是新生代,什么对象是老年代?
• 新⽣代(Minor GC):⼀般创建的对象都会进⼊新⽣代;
• ⽼年代(Full GC):对象经历了 N 次(⼀般情况默认是 15 次)垃圾回收依然存活下来的对象会从新⽣代移动到⽼年代。