Bootstrap

JVM面试题

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

类加载器(ClassLoader)
运行时数据区(Runtime Data Area)组成部分:

方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。方法区的实现,对于JDK8之前的版本,我们都把他称为永久代,或者将两者混为一谈,其实两者并不是一个概念,使用永久代来实现方法区,可以像java堆一样去管理方法区的内存,而它会更容易导致内存溢出的问题(永久代有上限,参数:-XX:MaxPermSize,即使不设置也会有默认大小),JDK7将字符串常量池移到堆中,到了JDK8,就完全舍弃了永久代,改用元空间来实现。

java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的

java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。

程序计数器(PC Register):会记录将要执行的下一条指令的行号地址,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行,可见程序计数器也是线程私有的

本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
执行引擎(Execution Engine)
本地库接口(Native Interface)

执行流程:首先通过类加载器会把Java代码转换成字节码文件,运行时数据区再把字节码文件加载到内存中,加载时需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由 C PU去执行,执行的时候需要用到其他语言的本地库接口(Native Interface)来实现整个程序的功能。

2.如何判断对象是否可以被回收

(1)引用计数法

给一个对象添加引用计数器,被引用时,就把引用对象的值加1,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。但是有一个弊端,有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致垃圾回收器无法回收。

(2)可达性分析法

扫描堆中的对象,以根对象为起点进行搜索,如果有对象不可达的话,即是垃圾对象,可以作为 GC Root(根节点) 的对象:

方法区中类静态属性引用的对象,常量引用的对象

虚拟机栈(栈帧中的本地变量表)中引用的对象

本地方法栈中 JNI(即一般说的Native方法)引用的对象

3.四种引用

(1)强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。  ps:强引用其实也就是我们平时A a = new A()这个意思

(2)软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

(3)弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

(4)虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue ();

PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

(5)终结器引用(FinalReference)

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

  1. 垃圾回收算法
  1. 标记--清除算法

标记和清除。标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展出来的。

特点:

速度较快

会产生内存碎片

  1. 复制算法

把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中,复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。

特点:

不会有内存碎片

需要占用两倍内存空间

  1. 标记--整理算法

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把要清除的未标记对象和存活对象“压缩”到堆的其中一块,按顺序排放。

  1. 分代垃圾回收

分代收集的原则:

次数上频繁收集年轻代

次数上较少收集老年代

基本不动永久代

根据内存对象的存活周期不同,java虚拟机中一般将内存划分成新生代和年老,当新建对象时一般在新生代中分配内存,在新生代垃圾收集器回收几次后(最多15次)仍然存活的对象,将被移动到老年代,或者当大的对象在新生代中无法分配到足够连续的内存空间时也会直接分配到老年代。

1.新生代

新生类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),养老区进行内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

1、 Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

2、 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

采用复制算法收集垃圾,在新生代中存在大量短生命周期的对象,所以不需要将新生代容量等量化分,而是将新生代划分为Eden、survivor from、survivor to 三部分,其中新生代内存容量的默认比例如上图所示是8:1:1。survivor from和survivor to区域中总有一个是空白的,只有Eden和其中一个survior也就是总容量的90%会被用来为新对象分配内存。这样内存浪费就少了。当新生代的内存空间分配不足时,仍然存活的对象会被分配到空白的survior内存区域中。Eden和非空白的survivor会被标记回收,两个survivor交换使用。

jvm对新生代的垃圾回收称为Minor GC,次数频繁,每次回收时间也短。

-Xmn 设置新生代内存大小

2.老年代

老年代存活率一般比较高,所以采用标记-整理算法进行垃圾收集效率会比较高。

jvm对老年代垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。当新生代中无足够空间为对象分配内存,老年代内存也无法回收到足够的空间时,堆会产生OOM异常

相关 JVM 参数

含义     参数

堆初始大小 -Xms

堆最大大小 -Xmx 或 -XX:MaxHeapSize=size

新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

幸存区比例(动态)-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例 -XX:SurvivorRatio=ratio

晋升阈值 -XX:MaxTenuringThreshold=threshold

晋升详情 -XX:MaxTenuringThreshold=threshold

输出GC详情    -XX:+PrintGCDetails -verbose:gc

FullGC前MinorGC -XX:+ScavengeBeforeFullGC

3.永久代

永久存储区是一个常驻内存区域(方法区),用于存放JDK自身所携带的 Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,只有关闭 JVM 才会释放此区域所占用的内存空间

如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

Jdk1.6及之前: 有永久代, 常量池1.6在方法区
Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后:无永久代得说法,常量池在元空间

6.GC的分类

GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)

Minor GC和Full GC的区别

普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。

全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上

7.面试题

1、JVM内存模型以及分区,需要详细到每个区放什

2、堆里面的分区:Eden,survival from to,老年代,各自的特点

3、GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方

4、MinorGC与FullGC分别在什么时候发生

5、方法区会不会进行垃圾回收?

以下几种情况,方法区会进行垃圾回收:

首先该类的所有实例对象都已经从 Java 堆内存里被回收

其次加载这个类的 ClassLoader 已经被回收

最后,对该类的 Class 对象没有任何引用

7、对象在JVM内存中如何分配与流转

代码里创建出来的对象,一般有如下两种:

一种是短期存活的,分配在 Java 堆内存之后,迅速使用完就会被垃圾回收

另外一种是长期存活的,需要一直生存在 Java 堆内存里,让程序后续不停的去使用

前者的对象,是在 Java 堆内存的新生代里;而后者的对象,是在 Java 堆内存的老年代里。

8、什么情况下会触发新生代的垃圾回收

当程序创建了N多对象,然后导致 Java堆内存里囤积了大量的对象。在新生代的内存空间几乎被全部对象占满的情况下,如果要给新的对象分配内存,此时发现新生代里内存空间不够用。而此时新生代中有部分对象的局部变量没有人引用。就会触发一次新生代内存空间的垃圾回收。

9、关于新生代和老年代的对象分配,还存在哪些机制

新生代和老年代,在对象分配方面,还有很多的复杂机制,比如:

新生代垃圾回收之后,因为存活对象太多,导致大量对象直接进入老年代;

特别大的超大对象直接不经过新生代就进入老年代;

动态对象年龄判断机制;

空间担保机制

  1. 总结

理解默认情况下对象优先分配在新生代,特别大的超大对象不经过新生代直接就进入老年代

新生代如果对象满了,会触发普通GC( Minor GC 回收掉没有人引用的垃圾对象

如果有对象躲过了15次垃圾回收,就会放入老年代里

如果老年代也满了,那么也会触发垃圾回收(full GC),把老年代里没人引用的垃圾对象清理掉

11、每个线程都有 Java 虚拟机栈,里面也有方法的局部变量等数据,这个 Java虚拟机栈需要进行垃圾回收吗?为什么?

JVM里垃圾回收针对的是新生代,老年代,还有方法区(永久代),不会针对方法的栈帧。方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉了。

12、JVM内存相关的几个核心参数

 -Xms:Java堆内存的初始值大小
 -Xmx:Java堆内存的最大能扩展的内存大小,建议设置为一样的大小,减少内存提升的性能消耗


 -Xmn:Java堆内存中的新生代大小,(新生代中伊甸园区,幸存者0区,幸存者1区,比例为8:1:1)扣除新生代大小剩下的就是老年代的内存大小了


-XX:PermSize:永久代初始值大小
-XX:MaxPermSize:永久代最大内存大小,通常这两个数值也是设置为一样的


-Xss:每个线程的栈内存大小

13、在线上部署系统应该如何设置 JVM参数

比如采用 “java -jar” 的方式启动一个 jar 包里的系统,然后采用类似下面的格式:

java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar

  1. 详细分析一下如何配置jvm参数

1.支付系统,在系统架构层面会包括:高并发访问、高性能处理请求、大量的支付订单数据需要存储,假设每天100万个支付订单,100万平均分配到几个小时里,那么大概是每秒100笔订单左右。假设支付系统部署了3台机器,每台机器实际上每秒大概处理了30笔订单。

2.通过实体类包含的实例变量来计算数据量,支付订单这种核心类,可以按照 20个实例变量来计算,应该只多不少,Integer类型的是 4个字节,Long类型是 8 个字节,大概一个对象就在几百字节左右。往大了去算,可以算一个支付订单对象占据 500字节的内存空间,每秒30个支付订单,大概占据的内存空间是 30 * 500 字节 = 15000 字节,大概就是 15kb,每秒30个支付请求,创建 30个支付订单对象,最多占据 kb级别的内存空间。一秒之后这30个对象就没有人引用了,就成了新生代里的垃圾了。下一秒请求过来,系统持续去创建支付订单对象,不停在新生代里放入30个支付订单,然后新生代里的对象会持续的累积和增加。直到发现新生代里都有几十万个对象,并且占据了几百MB的内存空间,可能导致新生代空间快满了,然后就会触发Minor GC,就把新生代里的垃圾对象都给回收掉了,腾出内存空间,然后继续在内存里分配新的对象。

3.支付订单对象以外的其他对象也会被大量创建,估算的话可以把内存扩大10倍到20倍,差不多内存空间有1M左右了。

4.空间分配:如果用2核4G的机器来部署,那么刨去机器本身要用的内存空间,最后JVM进程最多就是2G内存。而这2G内存还得分配给方法区、栈内存、堆内存几块区域,那么堆内存最多可能只有1G多的内存空间,而堆内存又分为新生代和老年代,老年代少说也要几百MB的内存空间,那么新生代可能也就几百MB的内存了。结合上述业务流程,如果针对整个支付系统的预估,大致每秒会占据1MB左右的内存空间。意味着,如果新生代只有几百MB的内存空间,就会导致运行几百秒之后,新生代内存空间就满了,从而触发Minor GC。而频繁的触发 Minor GC,是会影响性能的。因此至少考虑采用4核8G的机器来部署支付系统,那么JVM进程至少可以给4G以上内存,新生代至少可以分配到2G以上内存空间。

15、JVM调优/安全监控工具

JDK 自带的工具:

  1. JConsole,在jdk安装目录的bin目录下找到jconsole.exe直接启动就可以,可以选择本地jvm,也可以监控远程服务,连接成功后 可以看到概览图,内存、线程、类、VM、MBean 等板块
  2. VisualVM,几乎0配置,可直接选择本地 JVM,也可以远程连接 JVM。连接成功后,有概述、监视、线程、抽样器等板块,还可以支持安装其他功能插件。
  3.  jmap结合jStat, jmap导出堆内存,然后使用jhat来进行分析

第三方调优工具

Arthas(阿尔萨斯):阿里开源的一款线上监控诊断工具,可以查看应用负载、内存、gc、线程等信息,可以直接下载jar包的形式,通过java -jar就可以运行,可以使用相应的命令来查看具体情况。

我们统一购买的阿里云服务器,数据库也是买的阿里提供的mysql,消息队列也是阿里的,文件存储也是阿里云oss存储服务。有相应的页面可以查看内存,线程监控等等,运维人员配置警告内容,可以直接发邮件的形式。收到邮件后,查看具体的详细警告内容,在进行具体处理。

1.垃圾回收是否涉及栈内存?

不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

2.栈内存分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

3.方法的局部变量是否线程安全

如果方法内部的变量没有逃离方法的作用访问,它是线程安全的,如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

4.线程运行诊断

案例1:cpu 占用过多(ps:死循环)

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

top 命令,查看是哪个进程占用 CPU 过高

ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高

jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

案例2:程序运行很长时间没有结果(ps:死锁)

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出

可以使用 -Xmx8m 来指定堆内存大小。

堆内存诊断

jps 工具

查看当前系统中有哪些 java 进程

jmap 工具

查看堆内存占用情况 jmap - heap 进程id

jconsole 工具

图形界面的,多功能的监测工具,可以连续监测

jvisualvm 工具

方法区内存溢出

1.8 之前会导致永久代内存溢出

使用 -XX:MaxPermSize=8m 指定永久代内存大小

1.8 之后会导致元空间内存溢出

使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

;