Java 虚拟机(JVM)

1、简述 JVM 垃圾回收机制

在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

2、JVM 的内存结构

image

  • Java 堆(Heap) java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
  • 方法区(Method Area) 存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享。
  • 程序计数器(Program Counter Register) 是一块较小的内存空间,当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
  • JVM 栈(JVM Stacks) 存放基本数据类型、对象的引用、方法出口等,线程私有。
  • 本地方法栈(Native Method Stacks) 和虚拟栈相似,只不过它服务于Native方法,线程私有。

线程私有是不会发生 gc,并且他们是随线程生随线程灭,即程序计数器、本地方法栈和虚拟机栈。

3、如何判断对象可以被回收?
  • 引用计数算法

    原理:通过一个计数器对对象进行计数,对象被引用时+1,引用失效时-1;当计数为 0 时则说明可以被回收;
    缺点:很难解决对象的相互循环引用问题

  • 可达性分析算法
    Java 虚拟机所采用的算法;

    原理:通过一些列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

    那么哪些对象可以被称为 gc roots 呢

    • 虚拟机栈(栈中的本地变量列表)
      方法区静态属性
      方法区常量引用
      本地方法栈中 JNI 所引用的的对象都是可以作为 gc roots 的
4、GC 回收机制–如何回收

1、标记清除算法
清除算法分成 2 个阶段–标记和清除; 标记阶段对所有存活的阶段进行标记,标记完成后,再扫描整个空间未标记对象,直接回收不存活的对象.

  • 优点: 大多数情况下比较高效,缺点是会造成内存碎片,碎片太多导致后面过程中对大内存的分配无足够空间时而提前猝发一次垃圾回收动作;

2、复制算法
将可用内存将容量划分成大小相等的 2 块,每次清理时将其中 A 内存还存活的对象复制到 B 内存里面,然后再把 A 中清理掉;

  • 优点:高效且并不产生碎片
  • 缺点:牺牲了一半的内存为代价适用存活对象少,回收对象多

3、标记整理算法

  • 该算法标记阶段和标记清除算法一样,完成标记后它不是直接清理可回收对象,而是将存活对象都向一端移动最后清理掉端边界意外的内存;
  • 适用于存活对象多,回收对象少的情况

4、分代回收算法
整合了复制算法和标记整理算法,根据新生代和老年代的不同特性采取上面的不同算法

  • 新生代 生命周期短,每次回收时都有大量垃圾对象需要回收 复制算法
  • 老年代 每次只有少量的对象需要回收 标记整理算法

深入理解分代回收算法

1、复制算法中内存划分其实并不是按照 1:1 来划分老年代和新生代,而是按照 8:1:1 分一个大的 Eden 区和两个小的 survivor 的空间
2、为什么需要 2 个 Survivor 区 新生代一般经历 15 次 Gc 就可以移到老年代.当第一次 gc 时,我们可以把 Eden 的存活对象放入 Survivor A 空间,第二次 Gc 时,Survivor A 也要使用复制算法,存活对象放到 Survivor B 上,第三次 gc 时,又将 Survivor B 对象复制到 Survivor A 上如此循环往复;
3、为什么 Eden 这么大,因为新生代中存活的对象,需要转移的 Survivor 的对象不多,算是缓解了复制算法的缺点;

5、JVM 的两个内存:栈内存和堆内存的区别是什么?

1、Java 把内存划分成两种:一种是栈内存,一种是堆内存。两者的区别是:

  • 栈内存:在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
  • 堆内存:堆内存用来存放由 new 创建的对象和数组。在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。
6、类加载器相关
  • 启动类加载器:Bootstrap ClassLoader,负责加载存放在 JDK\jre\lib(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库
  • 扩展类加载器:Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 DK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
7、Dalvik 和 Java 虚拟机(JVM)有什么区别?

-Dalvik 基于寄存器
JVM 基于栈
基于寄存器的虚拟机对于更大的程序来说,在它们编译的时候,花费的时间更短。

  • JVM 字节码中,局部变量会被放入局部变量表中,继而被压入堆栈供操作码进行运算,当然 JVM 也可以只使用堆栈而不显式地将局部变量存入变量表中。
  • Dalvik 字节码中,局部变量会被赋给 65536 个可用的寄存器中的任何一个,Dalvik 指令直接操作这些寄存器,而不是访问堆栈中的元素。
8、GC 会造成什么影响?
  • 在开始学习 GC 之前你应该知道一个词:stop-the-world。不管选择哪种 GC 算法,stop-the-world 都是不可避免的。
  • 也就是说,当垃圾回收开始清理资源时,其余的所有线程都会被停止。所以,我们要做的就是尽可能的让它执行的时间变短。如果清理的时间过长,在我们的应用程序中就能感觉到明显的卡顿。
9、什么情况下 GC 会执行?

-总的来说,有两个条件会触发主 GC:

  • 当应用程序空闲时,即没有应用线程在运行时,GC 会被调用。因为 GC 在优先级最低的线程中进行,所以当应用忙时,GC 线程就不会被调用,但以下条件除外。
  • Java 堆内存不足时,GC 会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM 就会强制地调用 GC 线程,以便回收内存用于新的分配。若 GC 一次之后仍不能满足内存分配的要求,JVM 会再进行两次 GC 作进一步的尝试,若仍无法满足要求,则 JVM 将报“out of memory”的错误,Java 应用将停止。
  • 由于是否进行主 GC 由 JVM 根据系统环境决定,而系统环境在不断的变化当中,所以主 GC 的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主 GC 是反复进行的。
10、为什么需要程序计数器?

1、Java 是多线程的,意味着线程切换;
2、确保多线程情况下的程序正常执行;

11、你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。

思路: 一定要记住典型的垃圾收集器,尤其cms和G1,它们的原理与区别,涉及的垃圾回收算法。

我的答案:

1)几种垃圾收集器:

  • Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
  • ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
  • Parallel Scavenge收集器: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
  • Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
  • CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
  • G1收集器: 标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

2)CMS收集器和G1收集器的区别:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用;
  • G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用;
  • CMS收集器以最小的停顿时间为目标的收集器;
  • G1收集器可预测垃圾回收的停顿时间
  • CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
  • G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
12、JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代

思路: 先描述一下Java堆内存划分,再解释Minor GC,Major GC,full GC,描述它们之间转化流程。

我的答案:

  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1
  • 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
  • 大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态;
  • 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即长期存活的对象进入老年态。
  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
  • Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
13、VM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。

思路: 先讲一下JAVA堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。

我的答案:

1)共享内存区划分

  • 共享内存区 = 持久带 + 堆
  • 持久带 = 方法区 + 其他
  • Java堆 = 老年代 + 新生代
  • 新生代 = Eden + S0 + S1

2)一些参数的配置

  • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。
  • 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
  • Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold)

3)为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
14、简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

思路: 先说明一下什么是类加载器,可以给面试官画个图,再说一下类加载器存在的意义,说一下双亲委派模型,最后阐述怎么打破双亲委派模型。

我的答案:

  1. 什么是类加载器?

类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
  • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

2)双亲委派模型

双亲委派模型工作过程是:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

双亲委派模型图:

在这里插入图片描述

3)为什么需要双亲委派模型?

在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码

4)怎么打破双亲委派模型?

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。

15、说说你知道的几种主要的JVM参数

思路: 可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。

我的答案:

1)堆栈配置相关

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k 
-XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
  • -Xmx3550m: 最大堆大小为3550m。
  • -Xms3550m: 设置初始堆大小为3550m。
  • -Xmn2g: 设置年轻代大小为2g。
  • -Xss128k: 每个线程的堆栈大小为128k。
  • -XX:MaxPermSize: 设置持久代大小为16m
  • -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
  • -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
  • -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。

2)垃圾收集器相关

-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC 
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:
  • -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
  • -XX:ParallelGCThreads=20: 配置并行收集器的线程数
  • -XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
  • -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
  • -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
16、JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。

思路: 先画出Java内存模型图,结合例子volatile ,说明什么是重排序,内存屏障,最好能给面试官写以下demo说明。

我的答案:

1)Java内存模型图:
在这里插入图片描述

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

2)指令重排序。

在这里,先看一段代码

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
 
public static void main(String[] args) throws InterruptedException {
    Thread one = new Thread(new Runnable() {
        public void run() {
            a = 1;
            x = b;
        }
    });
 
    Thread other = new Thread(new Runnable() {
        public void run() {
            b = 1;
            y = a;
        }
    });
    one.start();other.start();
    one.join();other.join();
    System.out.println(“(” + x + “,” + y + “)”);
}

运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。

3)内存屏障

内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

4)happen-before原则

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用。

————————————————
原文链接:https://blog.youkuaiyun.com/qq_41701956/article/details/100074023
原文链接:https://blog.youkuaiyun.com/jaynm/article/details/107854707

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值