JVM是Java Virtual Machine的简称,翻译过来就是java虚拟机。
虚拟机是指通过软件模拟具有完整硬件功能的、运行在一个完全隔离环境中的完整计算机系统。
java能够那么火热,JVM功不可没。java的宣传口号是“一次编写,处处运行”,依靠的就是JVM,那么这个功能是怎么实现的呢?因为当你在IDE上写完代码编译后,JVM会为其生成一个.class文件,也叫字节码。当你把这个字节码放到另外一个系统上,只要都安装了JVM,那么这个java程序也能在另一个系统中运行。可以把JVM比作翻译机,如下图

JVM有很多的版本,也就是说有很多厂商都会有自己的JVM,比如阿里巴巴的Taobao JVM,HotSpot等,目前HotSpot占领着绝对的主场,有着广泛的用户。即使这样,那如果我们想支持国产的Taobao JVM,会不会造成写的代码就因为虚拟机的不同而造成bug呢?答案是不会,因为句各大厂商在开发JVM的时候都会遵循《Java虚拟机规范》,即使是不同的虚拟机,运行起来也不会有问题。(这就好比五菱宏光和劳斯莱斯,虽然引擎不一样,但不也是加油就能动嘛~~)。
我们已经知道了java代码为什么能够一次编写,处处执行。那么JVM具体是怎么做的呢?
程序在执行之前会先将java代码转换成字节码(class文件),JVM需要先将字节码通过一定的方式通过类加载器(ClassLoader)把文件加载到内存中运行时数据区(Runtime Data Area),字节码是Java虚拟机的一套指令规范,不能与底层的操作系统直接交互,所以此时需要**执行引擎(Execution Engine)将字节码翻译成对应的底层系统指令交由CPU处理,这个过程中需要用其他语言的接口(一般是C/C++)的接口本地库接口(Native Interface)**来实现整个程序的功能。
总的来看JVM主要包括以下4个部分:

如图,JVM内存布局分为5大部分:
其中堆和方法区是线程共享的,JVM栈区、本地方法栈和程序计数器都是线程私有的。
程序中创建的所有对象都保存在堆中,堆区分为老年代和新生代(老年代占堆空间的2/3,新生代占堆空间的1/3),新生代里面又包括Eden区和两个Survivor区(Eden区占80%,S0和S1分别占10%)。

一般当创建一个对象时,会将该对象放在Eden区,由于大部分的对象是朝生夕死的,当经历过垃圾回收的折磨后,将还存活的对象放在Survivor中的其中一个(S0区,或称为From区),当Eden区再次进行垃圾回收Mirror GC(采用复制算法)时,会扫描Eden区和From区,并将还存活的对象复制到S1区(也叫To区),并清空From区,如此进行反复,所以From区和To区是不断变换的,且有一个区域一直为空。那老年代呢?闲着?当然不是!
老年代的情况比较特殊,因为老年代一般存储的都是一些老油条,不容易被垃圾回收机制清理,所以我上面说的是一般情况下,那么在哪些情况创建的对象会进入老年代呢?
这段大家可以先理解堆区都有哪几部分,关于垃圾回收的部分后面还会讲到。
Java虚拟机栈的生命周期和线程相同,Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧存储局部变量表、操作数栈、动态连接、方法出口等信息。

1.局部变量表
存放了8大基本数据类型,对象引用。其中在编译期间完成内存大小的分配,且在运行期间不会改变,基本数据类型存放的为值,对象引用存放的为引用指针。
2.操作栈
每个方法会生成一个先进后出的操作栈,主要用于存储计算的临时数据。(这可以参考https://blog.csdn.net/z318913/article/details/123004876 图画的很详细)
3.动态连接
由于OOP的思想中多态的概念使得编译器在编译源代码时无法确定其对象类型,只有在运行是才能确定对象,指向常量池中方法的引用。
4.方法出口
记录方法结束时的出栈地址(正常执行结束时的返回地址或由于报错结束时的异常地址)。
本地方法栈与虚拟机栈相似,不同的地方是本地方法栈是为虚拟机的Native方法提供服务;Hostpot将虚拟机栈与本地方法栈合二为一。
由于CPU在执行的时候会存在时间片切换的概念,所以CPU执行指令是可能会中断的,这时候程序计数器会记录当前线程执行停止的字节码指令位置(行号) 以便于再次切换到该线程之后能够恢复到正确的执行位置而避免重新执行。
程序计数器需要注意以下两点
1.如果执行的是Native方法,计数器值为空。
2.程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域!
方法区主要用于存储以下几部分:
对于HotSpot来说,jdk1.8之后,字符串常量池被移动到了堆区;此时的方法区被称为元空间,元空间的内存属于本地内存,这样元空间的大小就不再受JVM最大内存参数的影响,而是与本地内存有关。
类加载顺序是这样的:

我们挨个看看每一步都做了什么:
1.加载
加载是类加载中的第一步,不要把概念混淆了。
加载主要做了以下几个内容:
2.验证
验证是连接阶段的第一步,主要目的是为了确保Class文件的字节流中包含的信息符合《Java虚拟机》的全部约束要求,保证这些信息被当成代码运行后不会危害虚拟机自身的安全。
这步主要验证以下几点:
3.准备
准备阶段是正式为类中定义的变量(既静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
比如有一个静态变量和成员变量
public static int value = 88;
public boolean flag = true;
此时value的值为0,flag的值为false
4.解析
因为多态的原因此时要将常量池内的符号引用替换为直接引用
5.初始化
此时Java虚拟机真正开始执行类中编写的Java代码,将主导权交给程序。初始化阶段就是执行类构造其方法的过程。
以上就是类加载的主要过程,刚开始记得话记主要步骤,要是主要步骤会记混建议结合做车这一现实中的场景来记忆。
不知道大家有没有想过一个问题:当类加载时如果一个父类有两个子类A和B,当A启动时会将A的父类也加载起来,那么当启动B时,还会加载这个父类,相当于这个父类被重复加载了两次。这还只是这个父类只有两个子类,我们都知道Java里面Object是所有类的父类,那Object需要被加载多少次?这种浪费资源的做法在程序员前辈面前是非常碍眼的,所以又了双亲委派模型。
1.什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,首先不会加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是这样,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当加载器反馈无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会去尝试自己去完成加载。
2.双亲委派模型的优点
小知识:SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。
我们在进行JDBC编程时需要调用DriverManager,DriverManager位于rt.jar下的sql包里(感兴趣的同学去jdk源码找),所以DriverManager由BootStrap类加载器加载,但是DriverManager提供的Driver接口的实现类出现在服务商提供的Jar包中,由子类加载器(线程上下文加载器Thread.currentThread().getContextClassLoader )来加载的。这样就破坏了双亲委派模型(因为双亲委派模型需要将所有类交给父类加载)

通过上面的学习我们知道,Java中创建的对象和数组都存放在堆里面,堆的大小是有限的,如果创建的所有对象一直呆在堆里面,那么满了就会报异常。所以我们要将无用的对象进行回收。JVM的垃圾回收机制做的就是这么一件事,这里面我们需要知道3个主要内容:(1)找到无用对象(死亡对象);(2)使用什么方法回收?;(3)使用什么工具?
常见的判断死亡对象的算法有两种:引用计数算法和可达性分析算法,通过这些算法我们就可以找到死亡对象,完成第一步。
引用计数器算法就是给对象添加一个引用计数器,当有一个地方引用他,那么计数器+1;当引用失效时,计数器-1 。任何时候计数器为0的对象都被认定为死亡对象。
引用计数器的实现简单,也很高效,Python就采用的这种算法。但是JVM并没有使用,而是使用可达性分析算法。为什么呢?因为当两个对象相互引用,即使两个对象都被置空,也不会被认定为是死亡对象,这就是循环引用问题。
引用计数器算法是直接判断对象是否已死,但是可达性 分析是用于判断对象是否存活,然后进行排除。
引用计数器的实现原理就是通过一些“GC Roots”的对象作为起始点,从这些结点向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots 没有任何引用链相连时,说明此对象是不可用的,也就被认为是死亡对象。
问题来了,总不能所有对象都能是GC Roots吧,如果真是这样,那这个算法将没有意义。所以一般将以下几种对象作为GC Roots:
知道哪些对象是死亡对象之后,接下来就要想着用什么方法回收这些对象了。
标记清除算法很简单,就是将死亡对象标记出来,垃圾回收的时候跳过没有被标记的区域,清理被标记的对象所在的区域。

这种算法有两个问题:
复制算法是为了解决标记清理算法的效率问题的,它将内存分为相等大小的两块,每次只使用其中的一块,当对一块内存进行垃圾回收时,会将这一块的存活对象复制到另一块内存中,然后将这一块内存全部对象回收,也就不会产生内存碎片。
在HotSpot中新生代使用的就是这种算法。因为新生代的大多数对象是朝生夕死的,所以使用这种算法时就更快。

在老年代中,由于对象的存活率更高,所以不适合使用复制算法,而是使用标记-整理算法。
标记-整理算法就是将死亡对象标记出来,然后将存活的对象向一端移动,然后清理另一端的死亡对象。


这幅图总结了各个GC回收器使用的算法和显著特点,值得注意的是CMS是支持并发的,G1是可单独使用,不必搭配其它GC回收器。各位同学感兴趣可以去了解每一个回收器具体细节。