JVM-面试考点

JVM

1.请简单描述一下JVM加载class文件的原理是什么?

答:首先类加载分为以下三个步骤:

  • 第一:装载:根据查找路径找到相对应的class文件,然后导入

  • 第二:链接:链接又可以分为3个小的步骤,具体如下

    • 检查.检查待加载的class文件的正确性
    • 准备.给类中的静态变量分配存储空间
    • 解析.将符号引用转换成直接引用
  • 初始化:对静态变量和静态代码块执行初始化工作.

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。

**Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。**在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

类装载方式,有两种
(1)隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
(2)显式装载,**通过class.forname()**等方法,显式加载需要的类 ,隐式加载与显式加载的区别:两者本质是一样的。

提高性能上:Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

2.什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

答:Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。

Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

3.jvm最大内存限制多少?

答:分为堆内存分配和非堆内存分配和VM最大内存

  • 堆内存分配:JVM初始分配的内存由**-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小 于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、 -Xmx相等以避免在每次GC后调整堆的大小**。
  • 非堆内存分配:JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。
  • VM最大内存:JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽 然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系 统下为2G-3G),而64bit以上的处理器就不会有限制了。

4.jvm是如何实现线程的?(了解)

答:实现线程主要有三种方式:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现。

主要借鉴实现线程主要有三种方式

  • 使用内核线程实现
    内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核。
  • 使用用户线程实现
    广义上,内核线程以外,就是用户线程。轻量级也算用户线程,但轻量级进程的实现始终是建立在内核上的,许多操作都要进行系统调度,效率会受到限制。
    狭义上,用户线程指完全建立在用户空间的线程库上。这种线程不需要切换内核态,效率非常高且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
  • 用户线程加轻量级进程混合实现
    内核线程与用户线程混合使用。可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低整个进程被完全阻塞的风险。用户线程与轻量级进程比例是N:M。

5.请列举一下,在JAVA虚拟机中,哪些对象可作为ROOT对象?

答:主要有以下四个对象

  • 虚拟机栈中的引用对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用对象
  • 本地方法栈中JNI引用对象

6.GC中如何判断对象是否需要被回收?

答:GC主要做三个事情:1、什么对象可以回收2、什么时候进行回 3、如何回收

对于什么对象可以回收,jvm通过以下算法来判断一个对象能否回收:

  • 引用计数算法

    方式:给对象维护一个引用计数器,当有一个地方引用到它,则计数器加1,当有引用失效,则计数器减1,当为0时,说明没有地方引用到这个对象。 优点:实现简单、效率高缺点:无法解决循环引用。

  • 可达性分析算法:

    方式:从一系列被称为GC ROOT的对象开始,向下搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT之间没有引用链,说明这个对象不可用。(Root4个对象在第五题有讲到) 这个算法可以轻松的解决循环引用的问题

  • 可达性分析算法之后判断finalize方法

    当一个对象被判定为不可达对象后,也并不是非死不可。在通过可达性分析算法判断没有引用链使之与GC ROOT相连,会判断该对象是否有必要执行finalize方法。

    假如重写了finalize,并且未调用过,则说明有必要执行。判断有必要执行finalize的对象,会被放入一个队列,有jvm建立的低优先级的Finalizer线程去执行。当在finalize中自救(把自己与引用链上的一个对象关联起来)成功的对象,就会在第二次标记时移除即将回收的集合。当在finalize中自救成功的对象,就会在第二次标记时移除即将回收的集合。自救失败的就会被回收,不会在执行finalize。

7.请说明一下eden区和survial区的含义以及工作原理?

答:目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成Eden 空间、 From Survivor 和 To Survivor 三块区域。

  • 我们把Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。
  • 一次 gc 发生后:
  • 1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;
    1. 清空 Eden 和 From Survivor ;
    1. 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。
  • 可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。

8.请简单描述一下JVM内存分区都有哪些?

答:java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。

JVM-面试考点_第1张图片

9.请简单描述一下类的加载过程

答:先上图

JVM-面试考点_第2张图片

  • **加载:**加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
  • 连接:
    • **验证:**这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    • **准备:**准备阶段是正式为类变量分配内存并设置类变量的初始值(初始为0,因为没有被编译,没调用构造方法)阶段,即在方法区中分配这些变量所使用的内存空间。
    • **解析:**解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
      • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
      • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
  • **初始化:**初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。

10.请简单说明一下JVM的回收算法以及它的回收器是什么?

答:垃圾回收算法有标记清除、复制算法、标记整理、增量算法

垃圾收集器有Serial收集器、ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器、CMS收集器、G1收集器

  • **标记清除:**标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段

    • 标记阶段:在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。
    • 清除阶段:清除所有未被标记的对象。

    带来的问题:会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发full gc。

  • **复制算法:**将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  • 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM研究表明新生代中的对象98%是朝夕生死的,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1(可以通过-SurvivorRattio来配置),也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

  • 标记整理:复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是**大部分对象都是存活对象。**如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

    • 标记-压缩算法:是一种老年代的回收算法它在标记-清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • **增量算法:**增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

  • Serial收集器:Serial收集器是最古老的收集器,它的缺点是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即stop the world。到现在为止,它依然是虚拟机运行在client模式下的默认新生代收集器,与其他收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。

  • **Serial Old收集器:**是Serial收集器的老年代版本,它同样是一个单线程收集器,使用”标记-整理“算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scanvenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。

    • 通过指定-UseSerialGC参数,使用Serial + Serial Old的串行收集器组合进行内存回收。
  • **ParNew收集器:**在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

    • -UseParNewGC: 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。
  • **Parallel Scavenge收集器:Parallel是采用复制算法的多线程新生代垃圾回收器似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。**所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

  • **Parallel Old收集器:**是Parallel Scavenge收集器的老年代版本,采用多线程和”标记-整理”算法。这个收集器是在jdk1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合”给力“。直到Parallel Old收集器出现后,”吞吐量优先“收集器终于有了比较名副其实的应用祝贺,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

    • -UseParallelGC: 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行垃圾回收
  • **CMS收集器:**CMS(Concurrent Mark Swep)收集器是一个比较重要的回收器,现在应用非常广泛,我们重点来看一下,CMS一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。它的收集过程分为四个步骤:

    • 初始标记(initial mark)
    • 并发标记(concurrent mark)
    • 重新标记(remark)
    • 并发清除(concurrent sweep)
    • 注意初始标记和重新标记还是会stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。
    • -XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
  • **G1收集器:**G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

    • 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
    • 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。
    • 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。
    • 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

你可能感兴趣的:(JavaSE面试题基础学习,jvm,java,编程语言,算法)