JVM
JVM设计的初心,是为了让java程序员感知不到系统层面的一些内容,让程序员只关注业务逻辑,不关注底层的实现细节。后来有一本书叫《深入了解java虚拟机》,这本书里面讨论了一些jvm话题,于是就掀起了jvm潮流。
1.JVM 简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM 是一台被定制过的现实当中不存在的计算机
2.JVM 运行流程
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过类加载器(ClassLoader) 把文件加载到内存中 [运行时数据区(Runtime Data Area)] ,而字节码
文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 **执行引擎(Execution Engine)**将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部
分的职责与功能。
3.java中的内存区域划分
jvm其实是一个java进程,java进程会从操作系统这里申请一大块内存区域,给java代码使用,这一大块内存区域,进一步划分出:
1.堆 new出来的对象和成员变量放这里
2.栈 维护方法之间的调用关系和局部变量存这里
3.方法区(旧叫法)/元数据区(新叫法) 放的是类加载之后的类对象和静态变量
这三个是最最核心的区域了
这里的面试题就是给你一段代码,问你某个变量处于内存的哪个区
判断方法是就看这个变量是局部变量还是成员变量还是静态变量
这个在哪个区跟变量的类型是没关系的,网上有的说内置类型的变量是在栈上,引用类型的变量是在堆上,这种说法是错的
堆和元数据区,在一个jvm进程中,只有一份
栈(本地方法栈和虚拟机栈)和程序计数器是存在多份的,每个线程都有一份
因为不同的线程调用的方法的顺序是不一样的,而且不同的线程下一个要执行的指令也不一样
java的线程和操作系统的线程是一对一的关系,每次在java创建出来的线程,必然会在系统中有一个对应的线程,这个线程在java的部分要有一个虚拟机栈来描述它的调用关系,在虚拟机里面,就要有一个本地方法栈来描述这个线程在被虚拟机调用之后,在虚拟机内部的本地方法调用的关系了
4.类加载
类加载简单来说就是把.class文件加载到内存,得到类对象这样的一个过程
程序员要想运行程序,就需要把程序依赖的指令和数据加载到内存中
类加载的步骤,非常复杂,但是把类加载的过程概括出了五个词,这五个词必备!!!!
1.加载阶段(Loading)
加载就是:找到.class文件,并且读文件内容
这个找.class文件,涉及到一个经典考点(双亲委派模型,下面解释)
在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证阶段(Verification)
.class文件有明确的数据格式(二进制的),jvm就是要验证一下,打开读到的这个.class文件内容,是不是符合这个格式要求,要符合才能继续下一步,不符合就失败了
3.准备阶段(Preparation)
类加载,最终是为了得到类对象,因此准备阶段就是给类对象分配内存空间(这个空间是未初始化的内存中的数据全是0的,类对象中的静态成员啥的也是0)
相当于我租了个写字楼,但是是毛胚房,没装修
4.解析阶段(Resolution)
针对类字符串常量进行初始化
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
5.初始化阶段(Initialization)
针对类对象进行初始化(初始化静态成员,执行静态代码块,如果有父类还需要加载父类…)
初始化阶段,Java 虚拟机真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
类加载这个动作,不是jvm一启动,就把所有的.class文件都加载了,整体是一个“懒加载”的策略(懒汉模式),非必要,不加载
什么叫做“必要”
1.创建了这个类的实例
2.使用了这个类的静态方法/静态属性
3.使用子类,会触发父类的加载
下面我们来看一个类加载中最常见的考点
双亲委派模型
双亲委派模型出现在类加载的第一个步骤,也就是加载中的找.class文件的这个过程,它的工作就是在第一个步骤中找.class文件这个过程
这个双亲委派模型,并非是类加载中的一个很重要的步骤,但是确实是考察频率最高的东西,俺猜是因为名字好听
首先我们要知道
JVM中,加载类,需要用到一组特殊的模块,类加载器
在JVM中,内置了三个类加载器
BootStrap ClassLoader 负责加载 Java标准库中的类
Extension ClassLoader 负责加载一些非标准的但是Sun/Oracle扩展的库的类
Application ClassLoader 负责加载项目中自己写的类 以及 第三方库 中的类
Extension 是Application 的爹,BootStrap 是Extension 的爹
这个就是双亲委派模式,看图只有父亲,没有母亲,为啥叫双亲呢?
是翻译问题,双亲好听!
双亲委派模型可以被打破的,如果你自己实现了一个,你可以遵守双亲委派模型,也可以不遵守
5.JVM垃圾回收(GC)
帮助程序员自动释放内存空间的
C语言中,malloc的内存必须手动free,否则就容易出现内存泄露(光申请内存,不施放,内存逐渐用完了,导致程序崩溃),于是java等后续编程语言引入了GC来解决上述问题,能够有效的减少内存泄露的出现概率
其实内存释放时一种很纠结的事情
申请的时机是明确的=>使用到了必须要申请
释放的时机是模糊的=>彻底不使用了才能释放
C/C++做法是完全让程序猿来决定,比较不靠谱的,特别依赖程序猿的水平
Java通过JVM自动判定基于一系列策略就可以让这个准确性比较高,但是也会付出一些代价
JVM中的内存分为好几个区域,GC主要释放的是哪个空间呢?
堆!!!(new出来的对象)
程序计数器,就是一个单纯存地址的整数,不需要随着线程一起销毁.
栈,也是随着线程一起销毁.方法调用完毕,方法的局部变量自然随着出栈操作就销毁了
元数据区/方法区,存的类对象,很少会“卸载”
GC说是释放内存,其实是释放对象,GC是以对象为单位进行释放的
GC主要分为两个阶段
1.找到谁是垃圾
2.把垃圾对象的内存给释放掉
针对这两个阶段,有了一些垃圾回收算法
这里提到的垃圾回收算法,是基本的思想,不代表jvm真实的实现方式,jvm的真实实现方式是基于这些思想方法,但是又做出了很多细节上的调整和优化,我们只需要掌握最基本的思想就足以应付面试了
先说第一步
1.找,确认垃圾( 死亡对象的判断的算法)
一个对象,如果后续再也不用了,就可以认为是垃圾
Java中使用一个对象,只能通过引用!!如果一个对象,没有引用指向他,此时这个对象一定是无法被使用的(妥妥的是垃圾),就释放,如果一个对象已经不想用了,但是这个引用可能还指向着呢,就不释放,Java中只是单纯通过引用没有指向这个操作,来判定垃圾的
Java对于垃圾对象的识别是比较保守的,最大程度的避免“误杀”,释放不及时,是小事。误杀是大事
那么具体来说,怎样知道一个java对象是否有引用指向呢?
引用计数算法
给对象里安排一个额外的空间,这个空间保存一个整数,表示该对象有几个引用指向(实际java没有用这个算法,但是py,php采取了,也是一种典型的算法)
两个缺陷:
1.浪费内存空间(假如我这个对象很小,只有5个字节,但是我还要空出来4个字节来计数)
2.存在循环引用的情况,会导致引用计数的判定逻辑出错
可达性分析算法
(这个是java采取的)把对象之间的引用关系,理解成了一个树形结构.从一些特殊的起点出发,进行遍历.只要能遍历访问到的对象,就是“可达”.再把“不可达的”当做垃圾即可
可达性分析,克服了引用计数的两个缺点,但是也有自己的问题
1.消耗更多的时间,因此某个对象成了垃圾,也不一定能第一时间发现,因为扫描的过程,需要消耗时间
2.在进行可达性分析的时候,要顺腾摸瓜,一旦这个过程中,当前代码的对象的引用关系发生变化了,就还麻烦了,因此,为了更准确的完成这个摸瓜的过程,需要让其他的业务线程暂停工作(STW问题—stop the world)
(java这里发展这么多年,垃圾回收这里也在不断优化,STW这个问题,现在已经能够比较好的应付了,不能完全消除,但是已经可以让STW的时间尽可能的短了)
2.把垃圾对象的内存给释放掉(垃圾回收算法)
标记清除算法
直接把内存中的垃圾对象释放
虽然说简单,但是太粗暴了,因此为了避免内存碎片,又引入了复制算法
复制算法
万一你要的内存空间一开始就大于这个申请内存的一半,复制算法就用不了了
复制算法,解决了内存碎片问题,但是也有缺点
1.内存利用率比较低
2.如果当前的对象大部分都是要保留的,垃圾很少,此时复制成本就比较高了
用复制算法的合适场景是垃圾占大多数
针对复制算法也有部分局限性,于是就引入了第三种算法
标记整理算法
解决了内存碎片问题,但是搬运开销也比较大
这么一看上述三种的方法好像都不咋地
因此JVM实际上的实现思路,是结合了上述几种思想方法,又搞出来了一个分代回收思想
分代回收算法
针对不同的情况使用不同的策略,取长补短
给对象设定了“年龄”这样的概念,描述了这个对象存在多久了.如果一个对象刚诞生,认为是0岁.每次经过一轮扫描(可达性分析),没被标记成垃圾,这个时候对象就涨一岁,通过年龄来区分这个对象的存活时间
经验规律:如果一个对象存活时间很长了,他将继续存在更长的时间(要死早死了)