满意offer之JVM虚拟机问答汇总

JVM是如何让java代码被机器执行的?

Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件, 而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。

JVM线程与系统线程的关系?

Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓 冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。 Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时, 会释放原生线程和 Java 线程的所有资源。

JVM系统线程主要有哪几个?

虚拟机线程 (VM thread)

这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当 堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the- world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。

周期性任务线程

这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。

GC 线程

这些线程支持 JVM 中不同的垃圾回收活动。

编译器线程

这些线程在运行时将字节码动态编译成本地平台相关的机器码。

信号分发线程

这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

JVM内存分为哪些区域?每个区域包含什么?

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区 域【JAVA 堆、方法区】、直接内存。

什么是程序计数器( 线程私有),它会 OutOfMemoryError吗?

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。不会OutOfMemoryError

什么是虚拟机栈( 线程私有)?

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

什么是本地方法栈( 线程私有)?

虚拟机栈为执行 Java 方法服务, 而本地方法栈则 为 Native 方法服务,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

简单介绍一下JVM中的堆和方法区

  1. 堆是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代。
  2. 方法区即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器。

JVM新生代 Eden 区、ServivorFrom、ServivorTo 三个区的意思

  1. Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直 接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
  2. ServivorTo:保留了一次 MinorGC 过程中的幸存者。
  3. ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

新生代 MinorGC采用什么算法?执行过程是怎么样的?

MinorGC 采用复制算法。首先,把 Eden 和 ServivorFrom 区域中 存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代 区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);然后,清空Eden 和 ServicorFrom 中的对象;最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为 下一次 GC 时的 ServicorFrom 区。

MajorGC采用什么算法?执行过程是怎么样的?

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(OutOfMemory)异常。

java8后JVM永久代做了哪些改变?

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

JVM中是如何确定对象数据是垃圾的?

  1. 引用计数法:一个对象如果没有任何与之关 联的引用,即他们的引用计数都为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
  2. 可达性分析:如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

简单介绍一下标记清除算法( Mark-Sweep )

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

简单介绍一下复制算法( copying )

为解决 Mark-Sweep 算法内存碎片化而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用 的内存清掉,这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原 本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

简单介绍一下标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

简单介绍一下分代收集算法

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。大部分 JVM 的 GC 对于新生代都采取复制算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代 划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块 Survivor 空间中。而老生代因为每次只回收少量对象,因而采用标记整理算法。当新生代的 Eden Space 和 From Space 空间不足时就会发 生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后 将 Eden Space 和 From Space 进行清理。如果 To Space 无法足够存储某个对象,则将这个对象 存储到老生代。在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。当对象 在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代 中。

JAVA 四中引用类型是哪四种,简单介绍一下

  1. 强引用:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。
  2. 软引用:软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
  3. 弱引用:弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
  4. 虚引用:虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

GC 垃圾收集器有几种,分别是什么?

  1. Serial 垃圾收集器,特点:CPU利用率最高,停顿时间即用户等待时间比较长。适用场景:小型应用通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
  2. ParNew 垃圾收集器,参数控制:-XX:+UseParNewGC ParNew收集器 -XX:ParallelGCThreads 限制线程数量
  3. Parallel Scavenge 收集器,采用多线程来通过扫描并压缩堆 特点:停顿时间短,回收效率高,对吞吐量要求高。 适用场景:大型应用,科学计算,大规模数据采集等。 通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
  4. Serial Old 收集器
  5. Parallel Old收集器
  6. Concurrent mark sweep(CMS)收集器,优点:并发收集、低停顿 缺点:产生大量空间碎片、并发阶段会降低吞吐量,特点:响应时间优先,减少垃圾收集停顿时间 适应场景:大型服务器等。 通过JVM参数 -XX:+UseConcMarkSweepGC设置
  7. G1收集器,特点:支持很大的堆,高吞吐量,通过JVM参数 -XX:+UseG1GC 使用G1垃圾回收器

JVM 类加载机制分为哪五个部分,都做些什么?

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这 五个过程。

  1. 加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)
  2. 验证:这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  3. 准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
  4. 解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的常量
  5. 初始化:初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。

简单介绍一下JVM三种类加载器?

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过- Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类
  2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通 过 java.ext.dirs 系统变量指定路径中的类库。
  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

什么是类加载?

  1. 类加载指的是将类 Class 文件读入内存,并为之创建一个 java.lang.Class 对象, class 文件被载入到了内存之后,才能被其它 class 所引用
  2. jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要去动态加载
  3. java 类加载器是 jre 的一部分,负责动态加载 java 类到 java 虚拟机的内存
  4. 类的唯一性由类加载器和类共同决定(双亲委派机制)

如何自定义实现自己的类加载器?

JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。

JVM 通过双亲委派模型进行类的加载,那么是如何加载的?

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

常见参数配置

-XX:+PrintGC 每次触发GC的时候 打印相关日志  
-XX:+UseSerialGC 串行回收   
-XX:+PrintGCDetails  更详细的GC日志 
-Xms 堆初始值   
-Xmx 堆最大可用值   
-Xmn 新生代堆最大可用值 
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例.  
-XX:NewRatio 配置新生代与老年代占比 1:2 
含以-XX:SurvivorRatio=eden/from=den/to  
**总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,**
**这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。**

谈谈你对JAVA 模块化开发的理解

什么是字节码?采用字节码的好处是什么?

  1. JVM可以理解的代码就叫做 字节码(即扩展名为 .class 的文件)
  2. Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

在Java语言里,可作为GC Roots对象的包括如下几种:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本地方法栈中JNI的引用的对象

为什么经常会说 Java 是编译与解释共存的语言。

JVM引进了 JIT 编译器,JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。

Java 语言为什么可以“一次编译,随处可以运行”?

JVM是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它都会给出相同的结果。字节码和不同系统的 JVM 实现,是 Java 语言“一次编译,随处可以运 行”的关键所在。

java GC发生在会么时候,对什么东西,做了什么事情?

  1. 程序员不能具体控制时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制进入oldObject的次数,使得oldObject 存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM的时间延迟,以延长对象生存期。
  2. 超出了作用域或引用计数为空的对象;从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。
  3. 如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等

常用的 JVM 调优的参数都有哪些?

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息。

调优总结: 初始堆值和最大堆内存内存越大,吞吐量就越高。 最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。 设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。减少GC对老年代的回收。

简述分代垃圾回收器是怎么工作的?

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

说一下 JVM 有哪些垃圾回收算法?

  • 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  • 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

说一下类装载的执行过程?

类装载分为以下 5 个步骤:

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。

说一下堆栈的区别?

  • 功能方面:堆是用来存放对象的,栈是用来执行程序的。
  • 共享性:堆是线程共享的,栈是线程私有的。
  • 空间大小:堆大小远远大于栈。

说一下 JVM 运行时数据区?

不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

说一下 JVM 的主要组成部分?及其作用?

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Native Interface)

组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券