Java面试题(不定期更新)面试

Java常见面试题(三、JVM)

2021-02-23  本文已影响0人  Batistuta9

三、JVM

1.说一下 jvm 的主要组成部分?及其作用?

2.说一下 jvm 运行时数据区?

(1)程序计数器

和堆一样所有线程共享,用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等。
jdk1.7使用永久代来实现方法区,在jdk1.8之后,使用元空间来实现方法区。
永久代:在运行时开辟空间实现方法区。
元空间:在本地内存区域开辟空间实现方法区。
永久代中的数据空间在每次fullgc的时候可能被收集,为永久代分配多少空间很难确定,超出指定空间容易造成内存泄漏。
元空间的特点:
1.类及相关的元数据的生命周期与类加载器的一致
2.每个加载器有专门的存储空间
3.只进行线性分配
4.不会单独回收某个类
5.省掉了GC扫描及压缩的时间
6.元空间里的对象的位置是固定的
7.如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
(6)运行时常量池

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

3.对象创建的过程?

  1. 检查指令的参数(即工作中我们New的对象),能否在常量池中找到它的符号引用。
  2. 如果存在,检查符号引用代表的类是否被加载、解析、初始化过。(如果没有则执行类的加载)。
  3. 加载通过后,虚拟机将为新生对象分配内存。(所需内存大小在类加载完成后便可确定)

4.内存分配的方式?

5.对象的内存分布?访问定位?

在HotSpot虚拟机中对象的内存布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

6.说一下堆栈的区别?

1.栈内存中保存的是局部变量,堆内存中保存的是对象的实例和数组。
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期短。
3.栈内存中保存的变量生命周期结束后会被释放,而堆内存中保存的对象实例和数组会被垃圾回收机制不定期回收。
4.栈是线程独享的,堆是线程共享的。但是TLAB例外。TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。TLAB在读取上确实是线程共享的,但是在内存分配上,是线程独享的。

7.队列和栈是什么?有什么区别?

8.说一下类加载的执行过程?

一个Java文件从编码到执行,一般经历两个过程。编译运行。编译是指把java文件通过javac命令编译成字节码,也就是.class文件。运行是指把编译生成的.class文件交给JVM去执行。
类加载就是指JVM把.class文件加载到内存中,生成class实例的过程。
类加载分为三个过程:

加载

将class字节码文件从各个来源通过类加载器加载到内存中。
字节码来源:本地路径下的文件、jar包、远程网络、动态代理编译等。
类加载器:一般包括启动类加载器、扩展类加载器、应用类加载器、用户的自定义类加载器。

验证

保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
文件格式验证。常量中是否有不被支持的常量?
元数据验证。该类是否继承了被final修饰的类?类中的字段和方法时候与父类冲突?是否有不合理的重载?
字节码验证。保证程序语义的合理性。
符号引用的验证。校验符号引用中是否可以通过全限定名找到对应的类?校验符号引用中的访问权限是否可以被当前类访问?

准备

为类变量(不是实例变量)分配内存,设置初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值。

解析

将常量池中的符号引用替换为直接引用。
符号引用:一个字符串,但是这个字符串给出了一些能够唯一确定识别一个变量、一个方法、一个类的信息。
直接引用:可以理解为一个内存地址或者一个偏移量。
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,JVM会把所有的常量名、方法名、类名这些符号引用,替换为具体的内存地址或者偏移量,也就是直接引用。

初始化

对类变量进行初始化,是执行类构造器的过程。
只有对static修饰的变量或者语句进行初始化。
如果初始化一个类的时候,其父类未被初始化,则先初始化其父类。

9.什么是双亲委派模型?

类加载器主要分为启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。


类加载器.png

当需要加载前一个类的时候,子类加载器并不会马上加载,而是一次去请求父类加载器,最终是到启动类加载器。当启动类加载器加载不了的时候,再依次往下让子类加载器去加载。当到达最底下还是加载不了的时候,就会抛出classnotfound异常。
好处:保证了程序的安全性。例子:比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。
有时会违反双亲委派模型的约束,比如jdbc。
Java提供了很多SPI(Service Provider Interface,服务提供者接口),允许第三方为这些接口提供实现。JDBC在获取connection时,调用DriverManager中的方法。但是因为DriverManager在rt.jar里面,它的类加载器时启动类加载器。而数据库的driver(com.mysql.jdbc.Driver)是放在classpath里面的,启动类加载器是不能加载的。所以,如果严格按照双亲委派模型,是没办法解决的。而这里的解决办法是:通过调用类的类加载器去加载。而如果调用类的加载器是null,就设置为线程的上下文类加载器:Thread.currentThread().getContextClassLoader()

9.怎么判断对象是否可以被回收?

1.引用计数法(已被淘汰)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
之间相互循环引用的问题。尽管该算法执行效率很高。
2.可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连接时,则证明此对象不可用。
Java中可作为GC Roots的对象:
1.虚拟机栈(栈帧中的局部变量表)中引用的对象。
2.方法区中静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
方法区存储内容是否需要回收的判断不一样。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

10.被GC判断为”垃圾”的对象一定会回收吗?

如果对象在进行了可达性分析后发现没有与GC Roots相连的引用链,那么会对这些对象进行第一次标记并且做一次筛选。筛选的条件是这些对象是否有必要执行finalize方法。如果执行过或者没有覆盖finalize方法,则被认为没有必要执行,直接回收。
如果有必要执行finalize方法,这些对象会放置在一个F-Queue的队列中,并在稍后由一个JVM建立的低优先级的finalizer线程去执行。执行时JVM不会等待方法执行结束,因为会出现有的对象finalize方法执行慢或者出现死循环,导致队列中其他对象永久处于等待状态的情况。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

11.java 中都有哪些引用类型?

12.说一下 jvm 有哪些垃圾回收算法?

13.说一下 jvm 有哪些垃圾回收器?

14.GC是什么时候触发的?

15.详细介绍一下 CMS 垃圾回收器?

因为在Hotspot JVM的世代回收过程中,新生代的空间会比较小,而老生代的空间会比较大。基于老生代空间大,变更小的特点,为了尽量减少GC引起的停顿时间,采用了停顿时间最短的CMS收集器。在CMS的并发标记的过程中,它会将整个老生代的空间切割为一个个block,每个block对应一个card。并且对整个老生代加上write barrier。从而在并发的标记过程中,用card来记录堆内发生写操作的区域。

CMS垃圾回收的特点

1.cms只会回收老年代和永久带(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻带;
2.cms是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%;

CMS垃圾回收步骤

  1. 标记老年代中所有的GC Roots对象。
  2. 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)

CMS需要注意的问题

  1. UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
  2. 用户调用了System.gc(),而且DisableExplicitGC没有开启。
  3. young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
    上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
    CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。

总结

优点:
1).将stop-the-world的时间降到最低,能给电商网站用户带来最好的体验。
2).尽管CMS的GC线程对CPU的占用率会比较高,但在多核的服务器上还是展现了优越的特性,目前也被部署在国内的各大电商网站上。
缺点:
1).对CMS在单核和多核机器上做测试。发现CMS在收集过程中会大量占用CPU的时间。所以在第二个阶段会比较漫长,所以一般将其设置在多核机器上。并且对于CMS在单核机器上的表现设计了一套启发式控制。这种控制将收集器看作一个掠夺者,而收集器会尽量赶在用户线程分配新的对象之前完成收集的工作。同样也有可能会出现用户线程希望分配对象,但目前空间不够,则需要停下收集器,这样会让整个收集时间大大加长。所以这时候一搬会选择扩张堆的大小。
2).Mark Sweep算法一直令人诟病的碎片问题,造成了堆空间的浪费以及利用率的下降。
3).需要较大的内存空间去运行,因为在很多并行的阶段,要考虑到用户程序运行时也要分配空间。所以一般选择在堆利用率达到一个常数的时候就开启CMS的收集。可以在VM argument里来设置这个阀值。(–XX:CMSInitiatingOccupancyFraction =n,n=0~100)
4).会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随程序自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理。

16.常用的 jvm 调优的参数都有哪些?

jvm配置

XX比X的稳定性更差,并且版本更新不会进行通知和说明。

1、-Xms

s为strating,表示堆内存起始大小

2、-Xmx

x为max,表示最大的堆内存

(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)

3、-Xmn

n为new,表示新生代大小

(-Xss:规定了每个线程虚拟机栈(堆栈)的大小)

4、-XX:SurvivorRator=8

表示堆内存中新生代、老年代和永久代的比为8:1:1

5、-XX:PretenureSizeThreshold=3145728

表示当创建(new)的对象大于3M的时候直接进入老年代

6、-XX:MaxTenuringThreshold=15

表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代

7、-XX:-DisableExplicirGC

表示是否(+表示是,-表示否)打开GC日志
上一篇 下一篇

猜你喜欢

热点阅读