面试题二:JVM

  1. JVM垃圾回收的时候如何确定垃圾?

    有2种方式:

    1. 引用计数

      每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收;

      缺点:无法解决对象循环引用的问题;

    2. 可达性分析

      从GC Roots开始向下搜索,搜索所走过的路径称为引用链;

      当一个对象到GC Roots没有任何引用链时,表示当前对象是不可用的,可以进行垃圾回收;

  2. GC Roots是什么?

    在java中,可以作为GC Roots的对象包含以下几种:

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    2. 方法区中类静态属性引用的对象;
    3. 方法区中常量引用的对象;
    4. 本地方法栈(即一般所说的native方法)中引用的对象;
  3. 方法区能否被回收?

    方法区可以被回收,但是价值很低,主要回收废弃的常量和无用的类。

    如何判断无用的类?

    1. 该类所有实例都被回收;
    2. 加载该类的ClassLoader已经被回收;
    3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方利用反射访问该类。
  4. 如果A和B两个对象循环引用,是否可以被回收?

    可以,因为在java中使用的是可达性分析的判断方式。

  5. 对象分配规则?

    1. 对象优先分配到Eden区;

    2. 大对象直接进入老年代(大对象是只需要大量连续内存空间的对象)

      避免大对象在E区和S区频繁复制(新生代一般使用复制算法)

    3. 长期存活的对象进入老年代

      虚拟机为每个对象都定义了一个年龄计数器,每经历一次minor GC年龄就+1,直到-XX:MaxTenuringThreshold参数配置的值时,对象被移入老年代

    4. 动态判断对象的年龄

      一般情况下,对象的年龄需要大于等于-XX:MaxTenuringThreshold参数配置的值时才会进入老年代,但是如果S区中相同年龄的对象的大小的总和大于S区空间的一半,那么年龄大于或者等于该年龄的对象可以直接进入老年代。

    5. 空间分配担保

      每次minor GC时,jvm会计算S区移入老年代的对象的平均大小,然后和老年代剩余值大小进行比较,如果老年代剩余值小于要移入的对象大小则进行一次Full GC,否则检查HandlePromotionFailure 设置,为true则只进行Major GC,为false则进行Full GC(jdk1.6之后已经弃用了).

  6. JVM调优和参数配置?

    JVM的参数主要分为三大类:

    1. 标配参数

      • -version
      • -help
      • -showversion
    2. X参数(了解)

      • 2.1 -Xint 解释执行
      • 2.2 -Xcomp 第一次使用就编译成本地代码
      • 2.3 -Xmixed 混合模式(默认)
    3. XX参数

      • 3.1 布尔类型: +/-XX:某个属性值,+表示开启,-表示关闭

        例如,+XX:PrintGCDetails表示开启打印GC收集信息的功能

      • 3.2 KV设值类型:-XX:属性key=值value

        例如,-XX:MetaspaceSize=128m表示设置元空间的大小为128M

        -Xms/-Xmx是哪种类型的参数?

        -Xms以及-Xmx属于XX参数,全称是Xx:InitialHeapSize/-Xx:MaxHeapSize

  7. 如何查看JVM系统默认值?

    1. -XX:+PrintFlagsInitial

      查看JVM根据系统设定的初始默认值,例如,

      java -XX:+PrintFlagsInitial
      
    2. -XX:+PrintFlagsFinal

      查看JVM更改后的参数值,例如,

      java -XX:+PrintFlagsFinal -Xss128k javaClass
      
    3. -XX:+PrintCommandLineFlags

      查看一些常见的参数,比如所使用的垃圾回收器,例如,主要用来查看最后的垃圾回收器

      java -XX:+PrintCommandLineFlags -version
      //运行结果
      -XX:InitialHeapSize=263050112 -XX:MaxHeapSize=4208801792 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
      
  8. 如何查看当前程序运行的配置?

    使用jps+jinfo的组合命令查看当前程序运行的配置

    jps -l //查看对应的进程ID
    jinfo -flag [某个参数] pid  //查看参数对应的值
    jinfo -flag pid //查看进程全部的参数值
    
  9. JVM常用的基本配置参数?

    1. -Xms

      初始内存大小,默认是物理内存的1/64,等价于-XX:InitiaHeapSize

    2. -Xmx

      最大分配内存,默认是物理内存的1/4,等价于-XX:MaxHeapSize

    3. -Xss

      单个线程栈的大小,默认是512k-1024k,等价于-XX:ThresholdStackSize

    4. -Xmn

      新生代大小

    5. -XX:MetaspaceSize

      设置元空间大小,元空间和永久代都是对于方法区的实现,到那时元空间和永久代的区别是元空间使用直接内存,而永久代是存在于虚拟机中的

      -Xms10m -Xmx10m -Xx:MetaspaceSize1024m 
      
    6. -XX:PrintGCDetails

      输出GC日志详细信息

    7. -XX:SurvivorRatio

      新生代Eden和两个Survivor区的大小比值,默认是8:1:1

    8. -XX:NewRatio

      设置新生代和老年代在堆区的占比,默认值是2,表示新生代占堆区的1/3,老年代占堆区的2/3,该参数的值就是老年代占有的比例,剩余的给新生代

    9. -XX:MaxTenuringThreshold

      设置垃圾最大年龄,默认是15次,也就是需要经过15次YGC后,对象才会进入老年代。

      如果将该值设置为0,表示对象不进入S区,直接进入老年代,对于老年代比较多的应用,可以提高效率。

      如果将该值设置的比较大,那么对象就会多次在S区进行复制,增加对象在年轻代存活的时间,增加在年轻代回收的概率。

  10. 运行时数据区域?

    1. 程序计数器

      没有规定任何OutOfMemoryError的区域

    2. java虚拟机栈

      会出现OutOfMemoryErrorStackOverflowError

    3. 本地方法栈

      会出现OutOfMemoryErrorStackOverflowError

    4. 会出现OutOfMemoryError

    5. 方法区-元空间

      线程共享区域,用来存放虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据;

      jdk1.7将字符串常量池,静态变量移入到堆中了,jdk1.8彻底废除永久代,改为元空间

      会出现OutOfMemoryError

    6. 运行时常量池

      方法区的一部分

      Class文件中除了有类的版本、字段、方法、接口等描述信息,还有常量池表用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。

      运行期间也可以将新的常量放入运行时常量池中,如String.intern();

      会出现OutOfMemoryError

    7. 直接内存

      会出现OutOfMemoryError

  11. 谈谈OOM的认识?

    1. 出现OOM的区域

      • 1.1 java堆溢出

        Java 堆溢出的原因,有可能是内存泄露,可以使用 MAT 进行分析;

      • 1.2 虚拟机栈和本地方法栈溢出

        在Hotspot虚拟机中,虚拟机栈和本地方法栈是合一的,因此只有-XSS参数有效,可以用来设置栈容量。

        该部分出现的异常主要有以下2种:

        • 如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError
        • 如果虚拟机在扩展栈时无法申请到足够的内存,会跑出OutOfMemoryError异常;
      • 1.3 运行时常量池溢出

        JDK7将运行时常量池和静态变量迁移到堆中,因此要测试,需要回退到JDK6版本

      • 1.4 方法区的内存溢出

        java8中方法区变成了元空间,因此也就无法重现方法区的内存溢出,如果要测试,需要回退到JDK7的版本

      • 1.5 元空间内存溢出

      • 1.6 本机直接内存溢出

    2. 实例

      • 2.1 java.lang.StackOverflowError

        虚拟机栈或者本地方法栈抛出的异常

      • 2.2 java.lang.OutOfMemoryError:Java heap Space

        该异常表示java堆内存溢出。

        要解决该异常,一般是先通过内存映像分析工具对Dump出来的内存快照进行分析,重点是确认内存中的对象是否是必要的,也就是需要先分析出来是内存泄漏还是内存溢出。

        内存泄漏:

        内存溢出:

      • 2.3 java.lang.OutOfMemoryError:GC overhead limit exceeded

        GC回收时间过长,并且回收的内存较少,如98%的时间都在回收内存,但是只回收了2%的内存,并且连续多次都只回收不到2%的内存的情况下就会抛出该异常。此时CPU的使用率一直是100%,但是GC却没有任何成果。

      • 2.4 java.lang.OutOfMemoryError:Direct buffer memory

        NIO程序使用ByteBuffer来读取或者写入数据,它可以直接使用Native函数直接操作堆外内存,此时由于堆内内存没有发生明显变化导致GC减少,从而导致DirectByteBuffer对象不会被回收,当本地内存不足时,就会出现OutOfMemoryError异常。

      • 2.5 java.lang.OutOfMemoryError:unable to create new native thread

        导致原因:

        • 应用创建了太多的线程,一个应用创建多个线程,超过系统承载极限;
        • linux默认允许单个进程创建的线程数是1024个,应用程序创建的线程数超过这个数据就会导致该异常。

        解决方案:

        • 降低应用创建的线程数;
        • 实在需要创建超过linux限制的线程数量时,可以修改服务器的配置;
      • 2.6 java.lang.OutOfMemoryError:Metaspace

        metaspace是jdk8引入的,用来取代永久代对方法区的实现,区别在于metaspace使用的是native memory,用来存储虚拟机加载的类信息、即时编译后的代码。

        不断生成类数据灌入元空间中就会导致该异常。

  12. 什么时垃圾回收机制?

    • java中对象都是通过new或者反射的方式生成的,这些对象的创建都是在堆中进行的,所有对象的回收都是通过虚拟机的垃圾回收机制完成的;
    • GC为了能够正确释放对象,会监控每个对象的运行情况,对他们的申请、引用、被引用、赋值等状况进行监控;
    • java程序员不需要担心内存管理,因为垃圾收集器会自动管理;
    • 可以通过System.gc()方法触发垃圾回收,但是jvm也可以屏蔽掉显示的调用,也即可能不执行。
  13. 为什么不建议使用System.gc()触发垃圾回收?

    1. 显示调用垃圾回收进行的是Full GC,对应用很大可能存在影响;
    2. 调用System.gc()后,垃圾回收不是立即执行,而是由jvm来决定何时执行;
  14. finalize()方法什么时候被调用?作用是什么?

    finalize()方法是在释放对象内存前由垃圾回收器调用;

    作用

    • 在该方法中释放对象持有的资源,如持有的堆外内存、远程服务的长连接;
    • 一般情况下,不建议重写该方法;
    • 对于一个对象,该方法仅且只会调用一次。
  15. GC垃圾回收算法?

    1. 标记-清除

      缺点:

      1.执行效率不稳定,如果java堆中存在大对象,那么标记和清除两个阶段的效率随着对象数量增加而降低;

      2.内存空间的碎片化问题,空间碎片太多可能会导致连续触发垃圾收集动作;

    2. 标记-复制

      解决的问题:解决标记-清除算法在大量对象需要回收的情况下的效率问题以及内存碎片的问题;

      实现的方式:根据年轻代对象98%会被第一轮回收的情况,将新生代分为Eden和2个Survivor区域,每次只使用Eden和一个S区,只浪费了10%的新生代内存空间

      担保机制:

      当10%的新生代空间不足以容纳一次Minor GC之后存活的对象时,这些存活的对象将通过分配担保机制直接进入老年代;

      优点:

      1.分配内存时不需要考虑空间碎片的问题,顺序移动指针分配即可;

      2.针对大多数对象可以被回收的情况下,算法需要复制的就是较少数的存活对象;

      缺点:

      1.对象存活率较高时要进行较多次复制操作,效率降低;

      2.为了不浪费50%的空间,需要有额外的空间进行分配担保,以应对被使用的内存中的对象存活率为100%的情况,不适合老年代使用;

    3. 标记-整理

      解决的问题:为了解决老年代对象存活率比较高导致的对象大量复制的问题;

      实现的方式:标记阶段和标记-复制算法是一样的,在整理阶段,是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

    4. 分代收集

  16. 垃圾收集器?

    1. Serial收集器

      作用范围:新生代

      并行/串行:串行

      优点:简单、高效

      缺点:Stop The World

      应用场景:桌面应用或者部分微服务应用中,在收集少量的新生代内存时,停顿时间相对较短,可以达到用户接受的地步。因此该收集器更适用于客户端模式下的虚拟机

      垃圾回收算法:标记-复制

    2. ParNew收集器

      作用范围:新生代

      并行/串行:并行

      优点:Serial的并行版本,同时存在多个线程进行垃圾回收,提高垃圾回收的效率,其他的如控制参数、回收策略、对象分配规则等都完全相同。

      缺点:单核心处理器的情况下,效率未必比Serial收集器效率高。

      应用场景:JDK9版本以后,ParNew收集器只能和CMS收集器搭配使用,ParNew+Serial Old以及Serial +CMS的组合已经被取消,且取消了-XX:+UseParNewGC参数

      垃圾回收算法: 标记-复制

    3. Parallel Scavenge收集器

      作用范围: 新生代

      并行/串行:并行

      目标:Parallel Scavenge收集器的目标是达到一个可控制的吞吐量

      适用场景:在后台运算而不需要太多交互的任务;开启-XX:+UseAdaptiveSizePolicy参数后,只需要设置基本内存数据如-Xmx之后,Parallel Scavenge收集器配合自适应策略,根据-XX:MaxGCPauseMillis更关注停顿时间或者-XX:GCTimeRadio更关注吞吐量参数,自动调节-Xmn-XX:SurvivorRatio等参数的值。

      垃圾回收算法:标记-复制

    4. Serial Old收集器

      作用范围:老年代

      并行/串行: 串行

      适用场景:1.jdk5之前和Parallel Scavenge收集器搭配使用;2.CMS收集器发生失败时的后备预案;

      对应的新生代收集器:Serial 或者Parallel Scavenge

      垃圾回收算法:标记-整理

    5. Paraller Old收集器

      作用范围: 老年代

      并行/串行:并行

      适用场景:与新生代Parallel Scavenge收集器搭配使用,吞吐量优先

      垃圾回收算法:标记-整理

    6. CMS收集器

      作用范围:老年代

      并行/串行: 并行

      四个阶段:①初始标记;②并发标记;③重新标记;④并发清除;

      优点:低停顿时间

      缺点:

      1.资源敏感。因为并发标记和并发清除阶段,回收线程和用户线程并行执行,会占用部分CPU资源,导致用户程序的吞吐量降低,默认的CMS线程数是(NCpu+3)/4,随着cpu核心数增加影响逐渐减小,但是cpu数量小于4核时影响较大;

      2.浮动垃圾导致的Full GC。并发标记和清除阶段用户线程也在运行,此时就会重新产生垃圾,由于此时已经标记完成,这些垃圾只能在下一次垃圾收集时回收,因此这些垃圾就被称为浮动垃圾。此外,由于并发标记清除时用户线程也在执行,因此不能等到老年代完全占满之后再去执行垃圾回收,因此需要预留一部分空间给用户线程使用,jdk6之前是68%,jdk6之后是92%的空间占用时就会触发老年代回收。一旦预留空间无法满足对象的分配,那么就会出现并发失败的问题,此时就需要Serial Old收集器作为后备方案,执行一次停顿时间更长的单线程收集。

      3.标记-清除算法导致的大量内存碎片,虽然提供了一些参数进行修正,但是jdk9以后这些参数都废弃了。

      垃圾回收算法:标记-清除

    7. G1收集器

      作用范围:新生代+老年代

      并行/串行:并行

      适用范围:在延迟可控的情况下,尽可能的提高吞吐量,未来用来替代CMS

      步骤:①初始标记;②并发标记;③最终标记;④筛选回收;

      垃圾回收算法:标记-整理 + 复制算法

      和CMS的比较:

      1.CMS适用标记-清除算法,容易产生内存碎片;G1适用标记-整理+复制算法,不会产生内存碎片;

      2.G1能够控制停顿时间;

      3.分Region的内存分布布局;

      4.优先回收价值最大的Region;

      缺点:

      1.用户程序执行过程中,用户程序运行过程中,G1为了垃圾收集产生的内存占用以及额外负载比CMS高;

    8. Shenandoah收集器

      低延迟垃圾收集器

    9. ZGC收集器

      低延迟垃圾收集器

  17. 垃圾回收算法和垃圾收集器的关系?

    参考垃圾收集器

  18. 如何查看服务器上默认的垃圾收集器?

    java -XX:+PrintCommandLineFlags -version
    
  19. 生产上如何选择垃圾收集器?谈谈对垃圾收集器的理解?

    参考垃圾收集器

  20. Minor GC?Major GC?Full GC?

    1. Partial GC

      指目标收集不是收集整个java堆的垃圾收集,又分为以下几个部分:

      • 1.1 新生代收集

        Minor GC/Young GC,目标只是新生代的垃圾收集

      • 1.2 老年代收集

        Major GC/Old GC,目标是老年代的垃圾收集。

        目前只有CMS*收集器会有单独收集老年代的行为。

      • 1.3 混合收集

        Mixed gc,目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这样的行为。

    2. Full GC

      目标是整个堆以及方法区的垃圾收集

  21. 生产环境服务器变慢,诊断思路和性能评估?

    1. 查看整机的负载情况

      top -c
      //或者uptime查看负载
      uptime
      
    2. 查看CPU的情况

      vmstat -n 2 3 //每2秒采样一次,采样3次
      //或者
      mpstat -P ALL 2
      //或者
      pidstat -u 1 -p [pid] //查看某个进程的cpu占用
      
    3. 查看内存情况

      free -m
      //查看某个进程的内存占用
      pidstat -p [pid] -r 2 //每2秒采样一次
      
    4. 查看存储情况

      df -h
      
    5. 查看磁盘IO

      iostat -xdk 2 3  //查看最后一项%util,值越大,负载越大
      //
      pidstat -d 2 -p [pid]
      
    6. 查看网络IO

      ifstat 1 //每秒1次
      
  22. 生产环境CPU占用过高,分析思路和定位?

    1. top命令查找占用cpu较高的进程;

    2. ps -ef | grep java | grep -v grep 或者jps命令进一步查找具体的进程

    3. 定位到具体的线程或者代码

      1. ps -mp [进程ID] -o THREAD,tid,time 查看线程id对应的占用的cpu时间
      2. 参数解释: -m 显示所有的线程 | -p pid 进程使用cpu的时间 | -o 该参数后是用户自定义格式
    4. 将需要的线程ID转换成16进制格式(英文小写)

    5. jstack 进程ID | grep tid(16进制进程ID英文小写) -A60

  23. 如何排查频繁Full GC?

    Full GC出现的原因有以下几点:

    1. 显示调用Syetem.gc()

    2. 执行jmap -histo:live [pid]命令,显示堆中对象统计信息;

    3. 执行minor GC时的一系列检查;

      具体过程参考对象分配规则题目

    4. 使用了大对象,大对象直接进入老年代;

    5. 在程序中长期持有了对象的引用;

    1,2两点可以很好的避免,主要分析从345点分析,可以使用 -XX:HeapDumpBeforeFullGC参数或者jconsole等工具分析具体的原因。

  24. JDK自带的JVM监控和性能分析工具用过哪些?怎么使用的?

    1. jps

      列出所有的虚拟机进程,主要用来查询进程ID

      jps -l
      

      参数:

      • -q 只输出lvmid,省略主类名称
      • -m 输出虚拟机进程启动时传递给主类的参数
      • -l 输出主类的全名
      • -v 输出虚拟机进程启动时的jvm参数
    2. jinfo

      java配置信息工具

      // 查询具体参数
      jinfo -flag Xmx [pid]
      // 查询全部参数
      jinfo -flags [pid]
      
    3. jstat

      虚拟机统计信息监视工具

      jstat -gc [pid] 250 20 //每250毫秒输出一次gc情况,输出20次
      
    4. jstack

      java堆栈跟踪工具

      // 除了堆栈外,显示关于锁的附加信息
      jstack -l [pid]
      
    5. jmap

      java内存影像工具-生成堆转储快照

      // 生成堆转储快照,指定生成的文件
      jmap -dump:[live]format=b,file=<filename> [pid]
      // 显示堆详细信息
      jmap -heap [pid]
      
    6. jhat

      虚拟机转储快照分析工具

      不建议使用,建议使用Eclipse Memory Analyzer等工具

    7. 可视化分析工具

      • jconsole
      • VisualVM
      • JHSDB
  25. 强、软、弱、虚引用?

    • 强引用

      当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会回收强引用的对象,例如,

      A a = new A();
      
    • 弱引用

      弱引用是比软引用具有更短声明周期的引用,需要使用java.lang.ref.WeakReference类来实现。

      一旦垃圾回收开始,不管内存是否充足,具有弱引用的对象都会被回收掉

      WeakReference<Person> weakReference = new WeakReference<>(new Person("李四","30"));
      

      使用场景:

      假设一个服务需要读取大量的本地图片,如果每次从磁盘读取比较耗费性能,但是整个读取到内存中又容易造成内存溢出,此时可以选择使用弱引用。

    • 软引用

      软引用是一种相对于强引用弱化了一些的引用。需要用java.lang.ref.SoftReference类来实现。软引用可以让对象豁免一些垃圾收集。

      对于软引用而言,当JVM执行垃圾收集时,如果内存够用就不回收,如果内存不够用就会被回收

      主要用于内存敏感类型的程序中,比如高速缓存

      SoftReference<Person> softReference = new SoftReference<>(new Person("张三","20"));
      
    • 虚引用

      从虚引用的源码可知,它的get()不管何时都返回null,所以单独引用虚引用没有任何意义,需要和引用队列类联合使用。

      当执行GC时,如果一个对象只有虚引用,就会把这个对象加入到与之相关的引用队列中,程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否要被垃圾回收。

      如果程序发现虚引用已经加入到引用队列,那么就可以在所引用的对象被回收之前采取必要的行动。

      使用场景:

      ThreadLocalMap中的键就是使用的虚引用。

    • 弱引用和软引用的区别:

      弱引用和软引用都有利于提高GC和内存的效率,区别:

      1. 弱引用一旦失去最后一个强引用会被立刻回收;
      2. 软引用虽然逃脱不了被回收的命运,但是可以延迟到内存不足时才会被回收。
    • 为什么需要使用不同的引用类型/

      java不像C语言,可以手动控制内存的释放,在java中有时候需要适当的控制内存回收的时机,因此就诞生了不同的引用类型,例如,

      • 使用软引用和弱引用解决OOM的问题;
      • 使用软引用实现java对象的高速缓存。
原文地址:https://www.cnblogs.com/ybyn/p/14547618.html