什么是java虚拟机

java代码,程序员把java源码编译成字节码,再在JVM上解释执行(转成机器指令,在cpu上执行)。
不论底层操作系统和硬件的差异,只要支持JVM使用,就能运行代码。这样的机制让java代码做到一次编译,到处运行

面试必考三大考点

JVM内存区域划分

  1. 程序计数器
    占用内存极小,存储下一个要执行的字节码地址。
    作用:java是多线程的,当这一条线程从CPU上调走之后,再回来执行,因为程序计数器保存接下俩执行的地址,CPU就可以继续正常按照地址执行。
    特点:唯一一个不会垃圾回收和OOM的区域。
    例子:每次程序刚开始执行时,程序计数器会把main方法放到其中的内存里,而且会把main方法中第一条执行指令的地址放进去。

  • JVM Stacks 虚拟机栈
  • Native Method Stacks 本地方法栈
    作用:作为方法的存储空间,描述了方法的调用关系。
    特点:存储速度极快,空间小,生命周期短。栈溢出(StackOverflowError),一般是死递归。
    例子:当有方法A创建后,就会创建一个栈给他存储,如果这个方法A调用了方法B,那么方法A无法继续执行,就把方法A放入栈帧中,等待方法B执行完毕,如果方法B调用了方法C,那么方法B也放进栈帧,等待方法C执行完毕。
    方法进栈顺序:A->B->C 方法出栈顺序:C->B->A (先进后出)

  1. 作用:new的对象都会放入堆中。
    特点:作为JVM中内存占比最大的部分,会有垃圾回收机制处理,空间不够报OOM(内存溢出)。

  2. 方法区/元数据区
    存放程序固定不变的资源。
    包含:类结构、方法结构、常量、静态变量、接口、注解。
    专门给 Java 调用 C/C++ 底层原生方法用,比如系统底层调用,开发者几乎不用管。

总结
这里的面试问题通常会问:

  • JVM内存区域划分成几个部分,都是用来干什么的?
    可以回答有程序计数器(用来存储下一个执行地址),栈(用来存储方法),堆(用来存储大部分的内容,new对象),方法区/元数据区(用来存储固定不变的资源)。(简答)

  • 给你一段代码,其中的数据都存储在什么地方?

    • 局部变量存储在栈。
    • 成员变量,new对象,存储在堆中。
    • 静态成员变量,存储在方法区。
    • 不会出现程序计数器,程序内部执行 存储内部地址。
  • 上述内存区域中,哪些在一个进程中是存在“多份”的?
    栈和程序计数器是多份的,多个方法多个栈,多个线程多个程序计数器。
    堆和元数据区是共享一个的,多个线程之间都共用这一个。

JVM类加载机制

有三大步骤,第二步骤可以分成三步,所以一共有五个步骤。

  1. 加载
    找到和打开.class文件,打开文件,读取文件内容。
    具体说明:代码中会有import操作,全限定类名(带有包名)
    从入口类开始,找到所有的import的类名
    import的类可能还要import别的类
  2. 验证
    读取,class文件,还需要进行检验这个文件根式对不对。
    具体流程:根据java规范手册中约定的.class的格式,来相对比。
  3. 准备
    这里的是准备好需要的内存大小,把.class文件放进去。
    这里申请的内存中全是0值(包括静态变量)
  4. 解析
    把文件中的符号引用换成真实的内存地址。
  • 符号引用:原始的.class用“字面值”表示数据,单纯用文字标识import,方法名,变量名。而且有偏移量来表示其中的变量的值。
  • 直接引用:用内存地址表示方法和变量。因为CPU和JVM底层只能识别地址,把所有的方法和变量都标上地址,指向表示的值。
    .class文件不知道什么是地址,只是记录了文字,经过解析对应到地址,代码运行之前,可以根据方法和变量中地址放进对应的区域,而且可以被CPU执行。
  1. 初始化
    针对静态成员进行初始化,并且执行所有的静态代码块。

双亲委派模式

简答:在加载步骤中,找.class文件,从子类进,转到父类开始找,找不到一直向下子类找。

  • Bootstrap ClassLoader 负责加载标准库的类(标准库,是java规范文档,要求必须要包含的内容,任何一个jdk都需要能够支持)
  • Extension ClassLoader 负责加载拓展库的类(实现该jdk的厂商,加入的一些“私货”,现在已经很少使用)
  • Application ClassLoader 负责加载第三方库的类(第三方程序员自己写的类)
    这是加载类的三个类,前者都是后者的父亲。也可以从内容上看出区别,父亲的类是基础的类库,接下去是厂商拓展和程序员自己加进去的,需要找.class文件理应从基础的开始找,如果找不到,就不是标准的类,可以找子类中拓展的类。
    所有这里的双亲委派也能体现,父类的库优先级更高,当需要找.class文件时,正常下都是以子类为入口,但是从父类(最上层的父类)开始找,进行查询,如果在父类找到了,就开始类加载机制;如果没有找到,就往子类开始找;如果都没有找到,就会报错。
    (这样的机制也可以防止有人通过写相同内部类名来篡改内部类内容)
    这是在类加载机制之前找到对应类的方式,经过这样的流程,才能开始类加载机制。

JVM垃圾回收机制

  • 垃圾回收(GC)的引入
    从JVM的内部结构入手,有资源计数器,栈,堆,元资源区,资源计数器和栈同线程的创建和销毁一样,不需要垃圾回收;类加载的结果一般不需要销毁,一个java进程加载的类是有限的,也不需要垃圾回收;但是堆中需要存放大量数据,而且清理数据的速度慢,而且程序员自行去删除一个不再使用的数据,需要大量的经验为基础,而且很容易因为删除错误导致进程无法正常执行,后果无法估计。
    所有大佬设计了一个垃圾回收机制去清理堆中不需要使用的数据,释放内存。
  • 如何判定垃圾
    既然这个机制引入,最核心的就是替代程序员去发现需要清理掉的垃圾,是如何判定的呢?
  1. 引用计数
    这里的机制是使用计数的方式表示一个数据有没有被使用,如果加入一个引用,就数字加一,如果减少一个引用,就数字减一。
    但是不是说没有引用,就执行清理,可能一个数据的一部分停止使用了,但是另一部分还在使用,就不能执行清理。
    不再引用,宁可放过,也不错杀,宁可释放这个对象的晚一点,也不能把一个可能之后使用的对象给释放掉,错杀的后果不能估计。
    空间存储问题
    既然每一个对象都需要引用来表示有没有使用,那么空间存储就有问题了,如果一个对象只有4个字节,再加入2个字节的引用来表示使用情况,这样的现象是很不可行的。
    循环引用
    循环引用
    现在给一个场景:有两个变量,各指向一个对象,再让其中a的地址指向b,让b的地址指向a,那么现在这两个对象都引用了两次。此时再把a和b的指向改成null,那么现在这两个对象都各有一个引用,并且互相引用,但是,这两个对象已经无法使用了,根据垃圾回收机制的引用机制,却不能清理他们,这就是循环引用的问题,引用机制是一个有漏洞的机制。
  2. 可达性分析(真正用来判定垃圾的机制)
    大佬们想到了树的结构来表示所有的对象(树的结构仅通过right和left的指向就能够到达所有的节点,可以看到所有对象的状态)
    但是也有缺点:当一个节点的连接断了之后,这个节点之后的叶子节点都是不可达状态。
    为了解决这个问题,于是引入可达性分析,JVM周期性的进行一次可达性分析的扫描。当扫描到断点,就视为垃圾,进行清理。
  • 如何释放垃圾对象
    • 标记清除
      利用标记垃圾的机制,清理时把带有标志的都清理掉。
      缺点:这样的方式会导致留下来的内存都是断的,申请的内存都是需要一连串的内存,这样无法满足。
    • 复制算法
      把需要删除的垃圾正常删除后,再把留存的数据转移到另一半内存中。
      优点:留下的内存片段连续。
      缺点:这样的方式导致内存利用率很低,而且如果未被清理的对象过多,复制的操作开销也很大。
    • 标记整理
      把需要删除的垃圾正常删除后,再把留存的对象搬运到内存的一端,留下长串的内存。
      优点:留下的内存片段是连续的,比较于复制算法内存利用率更大。
      缺点:如果删除的对象不多,留下的对象多,那么搬运的开销也很大。
    • 最终方案:分代回收(融合)
      这里的方案结合了上面的标记清理和复制算法和标记整理。
      先介绍分代回收的背景,大佬们把内存划分成伊甸区、幸存区和老年代,通过什么划分,又是为什么这样划分呢?
      大佬们发现垃圾回收中的垃圾大部分在第一次可达性分析时被清理掉,也有一些很多次可达性分析后都不会被清理的对象,于是进行划分成以下的样子:
      内存中存活次数划分

内存分成两半,前一半的前面80%表示新生代,两个10%表示幸存区,后面的一半表示老年代。
其中就是根据标志来划分的,每个对象的初始标志都是0,经过一次可达性分析没有判断成垃圾被清理,标志就加一。
新生代:new出来的对象都先放进这个内存区域
幸存区:第一次可达性分析后还存活,就放进幸存区;之后的可达性分析后还存活,就放进另一个幸存区,两个幸存区互相转移。
老年代:标志次数累加到15,从幸存区进入老年代。之后不再出来,直到可达性分析机制进行清理出去。
完整流程

  1. JVM进行第一次可达性分析,大部分new对象都被清理掉,留下的对象通过复制算法转移到幸存区中,没有被清理的对象的标志加一。
  2. 再进行可达性分析,垃圾被清理,未被清理的对象通过复制算法转移到另一个幸存区,并且标志加一。
  3. 直到其中的对象标志加到15,这样的对象通过复制算法转移到老年代区域。
  4. 在老年代的对象如果被清理了,剩下的对象通过标记整理转移到老年代的一端。

那为什么这里可以使用开销大的复制算法和标记整理呢?
因为大部分的对象已经在第一次可达性分析干掉了,剩下的对象进行转移的开销可以接受。

更多推荐