JVM面试37问

1入门部分

1.1为什么要学习JVM?
答:首先,JVM是Java平台的一部分,深入理解JVM可以帮助我们从平台角度提高解决问题的能力;
其次,在日常开发工作中,JVM是基本不用管的,程序只要编译没有问题就可以正常运行,但在实际敲代码中,我发现很多时候如果想解决一个问题通常都必须深入到字节码层次去分析,才可以得到准确的结论,字节码就是虚拟机JVM的一部分。Java中很多东西都是有关联的,如果Java项目崩溃了,就要学会看懂GC日志,如果想要看懂GC日志就必须了解学习Java虚拟机内存模型,如果要看懂内存模型,就得知道垃圾回收机制,Java虚拟机是如何进行垃圾回收的等等,同时我们还可以通过利用一些工具辅助观察当Java应用在运行时堆的布局情况,通过调整JVM相关参数来提高Java应用的性能。
所以我认为学习JVM的目的就是为了更加深入的理解Java语言,进行各种资源的分配、调优和故障排查等,为以后的工作打基础。
1.2你了解哪些JVM产品?
答:
(1)Sun Classic VM是最早期的虚拟机,其内部只提供解释器,为了解决Classic VM所面临的各种问题,提升运行效率,在JDK1.2发布了Exact VM,可以知道内存中某个位置的数据具体是什么类型;
(2)HotSpot VM,是由Sun公司研发,后在2010年由Oracle公司收购,是应用最官方,最主要,最广泛,最被人所熟知的虚拟机,顾名思义,热点代码探测,意味着HotSpot有着JIT(准时制)及栈上替换的能力;
(3)JRockit VM,是由BEA公司研发,后在2008年由Oracle公司收购,专注于服务端,没有解释器,全部靠即时编译,所以被称为最快的虚拟机;
(4)J9 VM,是IBM 内部使用,由于做了特殊的优化,性能非常出色,但是在IBM以外的Java产品可能会出现很多未知的bug;
(5)TaobaoJVM,是由AliJVM团队开发,基于OpenJDK开发了AlibabaJDK;
(6)Graalvm。是Oracle发布的下一代虚拟机,分为社区版和企业版,使用Graalvm可以高效执行Java;可以在使多语言并行,即在Java里面同时使用多种语言;通过把Java应用编译成机器码,使执行起来体积更小、启动速度更快。
(7)Dalvik是Google自己设计的用于Android平台的虚拟机,是Android移动设备平台的核心组成部分之一,可以支持已转换为.dex格式的Java应用程序的运行。Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik应用作为一个独立的Linux进行执行,这样独立的进程可以防止在虚拟机崩溃的时候所有的程序都被关闭。
1.3JVM的构成有哪几部分?
答:JVM的构成部分主要分为四大部分:类加载系统(ClassLoaderSystem)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地库接口(Native Interface);
类加载系统,即类加载器:负责加载类到内存,加载字节码文件,即编译后的.class文件;
运行时数据区:负责存储数据信息(对象、方法等),存放.class文件,分配内存;
执行引擎:负责解释执行字节码、执行GC操作等;
本地库接口:负责融合不同的编程语言为Java所用。

2类加载部分

2.1你知道哪些类加载器?
答:类加载就是将class文件读取到内存中的过程,而class文件的加载由ClassLoader完成,即类加载器,类加载器是在类运行时负责将类读到内存的一个对象,其类型为ClassLoader类型,是抽象类型,通常以父类形式存在。
类加载器主要有四种:
(1)获取系统类加载器(AppClassLoader),也叫应用类加载器,主要根据Java应用的类路径(classpath)来加载Java类,Java应用的类通常都是由它完成加载的,可以通过ClassLoader.getSystemClassLoader()来获取。
(2)扩展类加载器(ExtClassLoader):是用Java编写的,由BootStrapClassLoader加载,它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类。即主要负责加载%JAVA_HOME%/jre/lib/ext路径下的类;
(3)启动类加载器(BootStrapClassLoader):是基于C/C++实现的,由JVM在启动时加载初始化的,用来加载java核心类库,无法被java程序直接引用,主要负责加载%JAVA_HOME%/jre/lib路径下的类;
(4)用户自定义类加载器,即默认使用系统类加载器进行加载,通过继承java.lang.ClassLoader类的方式实现。
2.2什么是双亲委派类加载模型?
答:由于Java虚拟机对class文件采用的是按需加载的方式,即当需要使用该类的时候才会将它的class文件加载到内存生成class对象。当加载某个类的class文件时,Java虚拟机采用的就是双亲委派模式,即把请求交由父类类处理,是一种任务委派模式。当一个类加载器收到了类加载的请求,首先是把这个请求委派给父类加载器,所有的加载请求都应该传送到最顶层的启动类加载器,只要当父类加载器无法完成这个加载请求的时候,子加载器才会自己去完成加载,这就是双亲委派类加载模型。

附:双亲委派模型进行类加载的具体过程:
如果一个类加载收到了类加载请求,首先是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,就进一步向上委托,依次递归,直到请求最终到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。
举个例子:
在JVM中存在三个默认的类加载器:AppClassLoader、ExtClassLoader、
BootstrapClassLoader;
它们的关系是:AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会先使⽤ExtClassLoader的loadClass⽅法来加载类,同样的,ExtClassLoader的loadClass⽅法中会先使⽤
BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会尝试加载该类,如果还是没有加载到,那么就会由AppClassLoader来加载这个类。
2.3双亲委派方式加载类有什么优势、劣势?
答:优势:避免重复加载Java类型,确保了一个类的全局唯一性。Java类随着其类加载器一起具备了带有优先级的层次关系,通过这种关系可以避免类的重复加载,当父类已经加载了该类,就没有避免让子ClassLoader再加载一次。既保护了程序的安全,防止核心API被随意篡改,又避免了类的重复加载;
劣势:由于检查类是否加载的委派过程是单向的,虽然结构清晰,但是顶层的ClassLoader无法访问底层的ClassLoader所加载的类。由于双亲委派是按照BootstrapClassLoader-》ExtClassLoader-》AppClassLoader的顺序逐层尝试是否可加载,其中,启动类加载器BootstrapClassLoader中的类为系统核心类,所以在这种情况下,应用类访问系统类没有问题,但系统类如果想要访问应用类,就可能会出现问题,可能会造成无法创建由应用类加载器加载的应用实例的问题。
2.4描述一些类加载时候的基本步骤是怎样的?
答:类加载的过程大致可以分为加载、验证、准备、解析、初始化几个阶段,具体的步骤是:
(1)通过一个类的全限定名(类全名)类获取其定义的二进制字节流;
(2)将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2.5什么情况下会触发类的加载?
答:通常当我们第一次需要使用类信息的时加载,就称为类加载;
触发类加载的几种情况:
(1)第一次创建类的实例的时候,new对象的时候加载,如果第二次new的是同一个类,就不需要再加载;
(2)初始化一个类的子类,当加载子类会先加载父类,覆盖父类方法时所抛出的异常不能超过父类定义的范围;
(3)访问类的静态方法;
(4)访问某个类或接口的静态变量,或者给静态变量赋值;
(5)虚拟机启动时被标明为启动类的类;
(6)反射机制,Class.forName(“java.lang.String”);
2.6类加载时静态代码块一定会执行吗?
答:不一定,类的加载采用懒加载模式,能不加载就不加载,因此当一个类是被动加载时,并不会执行静态代码块。类加载的方式有很多种,并不是每一种方式都能执行静态代码块。例如,当显式加载的时候,通过ClassLoader对象的loadClass方法加载类不会执行静态代码块。
2.7如何理解类的主动加载和被动加载?
答:
主动加载一定会发生类的初始化,执行静态代码块,而被动加载时,类的静态代码块不会被执行;创建类的实例对象,调用类的静态方法,访问类或接口的静态变量的行为都属于主动加载。
2.8为什么要自己定义类加载器,如何定义?
答:因为在日常应用程序开发中,类的加载几乎是由JDK默认提供的类加载器互相配合来完成类的加载的;例如,打破类的双亲委派模型从而修改类的加载方式;从数据库中加载类从而扩展加载源;对字节码文件进行加密,用的时候再通过自定义类加载器对其进行解密,从而防止源码泄露;不同框架有相同全限定名的类从而隔离类的加载。所以我们可以自定义类加载器,从而定制类的加载方式。
实现自定义类加载有以下两步:
(1)继承ClassLoader;
(2)重写findClass,在findClass里获取类的字节码,并调用ClassLoader中的defineClass方法来加载类,获取class对象。
其中,如果要打破双亲委派机制,需要重写loadClass方法,实现类的加载。

3字节码增强部分

3.1为何要学习字节码?
答:
(1)了解字节码对于开发人员来说,可以更准确、直观地理解Java语言中比源码更深层次的东西,更好地理解Java中各种语法和其背后的原理。加上字节码增强技术在SpringAOP、各种ORM框架、热部署中的应用越来越多,深入理解其原理对于开发人员来说有很多好处。
(2)由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM升运行,这样就可以扩展Java所没有的特性或者实现各种语法糖。
理解字节码,学习字节码,从字节码视觉看设计思路,这样可以更好的利于学习。
3.2如何解读字节码内容?
答:
方法一:
(1)编写一段代码;
(2)当代码的源代码编译后,可以通过notepad++打开该代码,对于是一个遵循一定书写格式的二进制流文件,文件内容默认是一种16进制的格式,直接解读;
方法二:通过javap指令应用,在代码目录下使用javap -verbose对类进行反编译;
方法三:通过idea插件jclasslib,在代码编译后,可以在菜单栏view中选择Show Bytecode With jclasslib,就可以直观地看到当前字节码文件的类信息、常量池、方法区等信息。
3.3字节码内容由哪几部分构成?
答:魔数、版本号、常量池、访问标识、类型引用、字段表集合、方法表集合、属性表集合
3.4什么是字节码增强?
答:字节码增强技术相当于是一把打开运行时JVM的钥匙,利用字节码增强可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署。也可以跟踪JVM运行中程序的状态,进行性能诊断等。
3.5为什么要进行字节码增强?
答:字节码增强可以高效地定位并快速修复一些问题,例如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题,字节码增强可以让开发人员减少冗余代码,大大提高开发效率。
3.6你了解哪些字节码增强技术?
答:
(1)ASM技术,对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产.class字节码文件,也可以在类被加载入JVM之前动态修改类行为。
(2)Javassist技术,是一个用于分析、编辑和创建Java字节码的类库,相比ASM在指令层次上操作字节码会更加简单直观。
(3)Java Agent技术,是Java InstrumentationAPI的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口,可以侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。
3.7什么是热替换以及如何实现?
答:在一个持续运行,并已经加载了所有类的JVM中,还能利用字节码增强技术,对其中的类行为做替换并重新加载,这个过程就是热替换,即在运行的Java系统中进行对象的替换升级。
实现:如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重复定义这个类。因此,在Java中实现这一功能的一个可行方法就是灵活运用ClassLoader
(1)动态编译,将.java文件编译成.class文件;
(2)动态加载,编写一个自定义的类加载器,将编译好的类加载到运行环境中去;
(3)建立两个服务器JVM的通讯连接;
(4)由JVM将修改后的内容通过连接传输至正在运行的服务器虚拟机中,修改其原本的字节码文件的内容。

4JVM运行内存部分

4.1JVM运行内存是如何划分的?
答:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
4.2JVM中的程序计数器用于做什么?
答:程序计数器(Program Counter Register),即PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
4.3JVM虚拟机栈的结构是怎样的?
答:每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
每个栈帧中存储着:
(1)局部变量表(Local Variables);
(2)操作数栈(Operand Stack)(或表达式栈)
(3)动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
(4)方法返回地址(Return Address)(或方法正常退出或者异常退出的定义);
(5)一些附加信息
4.4JVM虚拟机栈中局部变量表的作用是什么?
答:局部变量表也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息。在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。局部变量表的大小相当程度决定了栈帧的大小。
4.5JVM虚拟机栈中操作数栈的作用是什么?
答:每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈(Operand Stack)。操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)。
4.6JVM堆的构成是怎样的?
答:JVM堆分为年轻代和老年代。年轻代分为Eden(伊甸园)和两个Survivor(幸存者)区。
4.7Java对象分配内存的过程是怎样的?
答:
(1)编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配;
(2)如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配;
(3)如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区);
(4)如果Eden区无法存储对象,则执行Yong GC(Minor Collection);
(5)如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代;
(6)新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象;
(7)GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代;
(8)GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区;
(9)GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推;
(10)当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。
4.8JVM年轻代幸存区设置的比较小会有什么问题?
答:伊甸园区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
4.9JVM年轻代伊甸园区设置的比例比较小会有什么问题?
答:伊甸园设置的比较小,会增加GC的频率,可能会导致STW的时间边长,影响系统性能。
4.10JVM堆内存为什么要分成年轻代和老年代?
答:为了更好的实现垃圾回收。
(1)年轻代:主要存放新创建的对象,内存大小相对比较小,垃圾回收会比较频繁;
(2)老年代:主要存放JVM认为生命周期比较长的对象,内存大小相对比较大,垃圾回收相对没那么频繁。
4.11如何理解JVM方法区以及它的构成是怎样的?
答:方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据;
(1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次;
(2)方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的;
(3)方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展;
(4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace。
构成:
(1)类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息;
(2)常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
4.12什么是逃逸分析以及可以解决什么问题?
答:逃逸分析是一种数据分析算法,基于此算法可以有效减少Java对象在堆内存中的分配。Hotspot虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。例如:
(1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸;
(2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
问题解决:
(1)栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引用不会发生逃逸,对象可能是栈上分配的候选;
(2)同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步;
(3)分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
4.13何为内存溢出以及导致内存溢出的原因?
答:内存中剩余的内存不足以分配给新的内存请求就会内存溢出,内存溢出可能直接导致系统崩溃。
原因:
(1)内存泄漏;
(2)创建的对象太大导致堆内存溢出;
(3)创建的对象太多导致堆内存溢出;
(4)方法出现了无限递归调用导致栈内存溢出;
(5)方法区内存空间不足导致内存溢出。
4.14何为内存泄漏以及内存泄漏的原因是什么?
答:动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束,这个现象称之为内存泄漏。因此良好的代码规范,可以有效地避免这些错误。
原因:
(1)大量使用静态变量(静态变量与程序生命周期一样);
(2)IO/连接资源用完没关闭(记得执行close操作);
(3)内部类的使用方式存在问题(实力内部类或默认引用外部类对象);
(4)缓存(Cache)应用不当(尽量不要使用强引用);
(5)ThreadLocal应用不当(用完记得执行remove操作)。
4.15JAVA中的四大引用你知道多少?
答:Java中的四大引用分别是强引用、软引用、弱引用、虚引用;
(1)强引用是最常见的引用赋值操作,只要引用关系还在,GC就不会回收;
(2)软引用,GC发现软引用关联的对象时不会立刻进行回收,而是在内存不足时才会对软引用对象进行回收;
(3)弱引用,不管内存空间是否充足,GC发现弱引用关联的对象时就会对其进行回收;
(4)虚引用,一个对象是否被虚引用指向,完全不影响它的生存空间,无法通过虚引用来获取对象的实例,虚引用的价值就是在这个对象被回收之前获得系统的一个通知。

5JVM垃圾回收部分

5.1何为GC以及为何要GC?
答:GC(Garbage Collection)称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作。比方说,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收。
程序在运行过程中,会产生大量的垃圾对象,如果内存被垃圾占满不回收的话,就会导致内存泄漏甚至内存溢出,导致应用后续运行出现问题,GC可以使内存循环利用,运行更流畅,将无用的对象清理掉,释放出内存来给应用继续使用。
5.2你知道哪些GC算法?
答:
(1)标记清除(Mark-Sweep)算法分为“标记”和“清除”阶段,它首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象。
(2)标记复制(Mark-Copy)算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。
(3)标记整理清除(Mark-Sweep-Compact)算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。第三阶段清理掉存活边界以外的全部内存空间。
5.3JVM中有哪些垃圾回收器?
答:
(1)Serial收集器
Serial GC是最古老也是最基本的收集器,但是现在依然广泛使用,JAVA SE5和JAVA SE6中客户端虚拟机采用的默认配置。
(2)Parallel收集器
Parallel 收集器为并行收集器,它可利用多个或多核CPU优势实现多线程并行GC操作,其目标是减少停顿时间,实现更高的吞吐量(Throughput)。
(3)CMS 收集器
CMS的官方名称为 “Mostly Concurrent Mark and Sweep Garbage Collector”,其设计目标是追求更快的响应时间。
(4)G1收集器
G1(Garbage-First )收集器是一种工作于服务端模式的垃圾回收器,主要面向多核,大内存的服务器。G1 在实现高吞吐的同时,也最大限度满足了GC 停顿时间可控的目标。在Oracle JDK7 update 4 后续的版本中已全面支持G1 回收器功能。
5.4服务频繁fullgc,younggc次数较少,可能原因?
答:fullgc触发的条件是老年代空间不许,如果服务频繁fullgc,younggc次数较少,说明大量对象频繁进入老年代,导致老年代的空间不足,需要频繁得进行GC释放内存空间。
导致原因可能:
(1)系统并发高、执行耗时过长或者数据量过大,导致younggc频繁,同时gc后存活对象太多,但是幸存者区存放不下,导致对象快速进入老年代并迅速填满;
(2)一次性加载过大对象,或者创建的对象过大,年轻代内存空间较小无法存放,导致直接被分配到了老年代;
(3)老年代中的部分内存空间出现了泄露,随着内存泄露的增多,内存空间势必会受到挤压。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值