面试问题

谈谈对JVM的理解?java8虚拟机和以前有什么区别

什么是OOM?什么是栈溢出StackOverFlowError?

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

内存快照如何抓取,怎么分析Dump文件?

谈谈JVM中,类加载器的认识?

在一个项目中突然出现了OOM故障,该怎么排除?

JVM的内存模型和分区-详细到每个区放什么?

堆里面的分区有哪些?有什么特点?

GC的算法有哪些?

轻GC和重GC

1
2
3
4
5
首先 三者之间存在包含关系

JVM + 核心类库 = JRE

JRE + java开发工具(javac.exe/jar.exe) = JDK

什么是JVM?

Java Virtual Machine 即Java虚拟机

我们知道Java语言有一个独特的优点就是可以跨平台

像其它语言,比如C,我们要针对不同操作系统windos,mac……各出一套应用程序

而Java则可以做到一个软件在任何的操作系统中都能执行,这就是JVM的功劳

如下图 Alt text

图片来自高新强老师JAVA课程 本来我们编写的Java代码计算机还是不认识的,但是我们在每一个操作系统上都会配置一个与之相对应的JVM,会帮我们把我们的Java代码翻译成对应操作系统可以识别的内容。

JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

Java语言中的反编译一般指将class文件转换成java文件。

所以说我们在第一次写Java程序时都要先把JVM给装好。

什么是JRE?

Java Runtime Environment 即Java运行环境

JVM + 核心类库 = JRE

刚才不是说只需要装JVM吗?那这个JRE是个什么鬼东西?

是因为只有JVM不能运行,它还需要核心类库,才能保证Java运行

由于JRE包含JVM 因此我们只要直接安装JRE 就顺便把JVM安装了

Alt text 图片来自高新强老师JAVA课程

什么是JDK?

Java Development Kit 即Java开发工具包

JRE + java开发工具(javac.exe/jar.exe) = JDK

前面不是说安装了JRE以后,Java程序就可以运行了吗?那为啥子还要安装这个JDK?

这是因为我们是开发人员,我们是写软件的,软件光能运行不行啊,得给我们一个地方让我们来写代码吧?所以就需要java开发工具给我们腾出一个地儿来,好让我们coding

由于JDK包含JRE 因此我们只需要安装JDK就都有了

Alt text

类加载机制

Alt text 类装载器(ClassLoader)(用来装载.class文件并初始化)

执行引擎(执行字节码,或者执行本地方法) 运行时数据区(方法区、堆、java栈、PC寄存器、本地方法栈)

堆(Heap)

它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。

元空间逻辑上存在,物理上不存在,不放在虚拟机内存,而存储在本地内存中

运行时常量池(Runtime Constant Pool)

存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

本地方法堆栈(Native Method Stacks)

JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

PC寄存器

PC寄存器是用于存储每个线程下一步将执行的JVM指令,如该方法为native的,则PC寄存器中不存储任何信息。

方法区

方法区:static、final、Class、常量池

不止是存“方法”,而是存储整个 class文件的信息,JVM运行时,类加载器子系统将会提取 class文件里面的类信息,并将其存放在方法区中。例如类的名称、类的类型(枚举、类、接口)、字段、方法等等。

方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

栈:栈内存,主管程序的运行,生命周期和线程同步

  • 线程结束,栈内存就释放,对于栈来说,不存在垃圾回收问题
  • 一旦线程结束,栈就over
  • 栈:JVM栈中存放的为当前线程中局部8大基本类型(boolean、char、byte、short、int、long、float、double)、对象引用、实例的方法、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
  • 栈的运行原理:栈帧
  • 栈满了:StackOverFlowError
  • 栈+堆+方法区:交互关系
  • JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈

双亲委派机制

工作原理:

1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;

2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达项层的启动类加载器;

3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

作用:

1)避免核心API被篡改

2)避免类的重复加载

1
2
3
AppClassLoader
ExtClassLoader
BootstrapClassloader  #java获取不到因为底层是c/c++

image-20210614221246852

字节码执行机制

GC垃圾回收

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

(1)对新生代的对象的收集称为minor GC; (2)对旧生代的对象的收集称为Full GC; (3)程序中主动调用System.gc()强制执行的GC为Full GC。

不同的对象引用类型, GC会采用不同的方法进行回收,JVM对象的引用分为了四种类型:

(1)强引用:默认情况下,对象采用的均为强引用(这个对象的实例没有其他对象引用,GC时才会被回收) (2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC) (3)弱引用:在GC时一定会被GC回收 (4)虚引用:由于虚引用只是用来得知对象是否被GC

不同的区

垃圾收集算法

标记-清除算法

适用场合

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)

缺点

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
  • 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

复制算法

适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少

缺点:

  • 需要一块儿空的内存空间
  • 需要复制移动对象

标记-整理算法

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

步骤:

  • 当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,每次Minor GC,存活下来的对象年龄加+1
  • 那什么时候会进入老年代呢?从上面看到,如果对象在GC过程中没有被回收,那么它的对象年龄(Age)会不断的增加,对象在Survivor区每熬过一个Minor GC,年龄就增加1岁,当它的年龄到达一定的程度(默认为15岁),就会被移动到老年代,这个年龄阀值可以通过-XX:MaxTenuringThreshold设置。

谁空谁是to

大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

详情 垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。

JVM性能监控与故障定位

JVM调优

**对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。**1.Full GC

会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。

2.导致Full GC的原因

1)*年老代(Tenured)被写满*

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。

2)持久代Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例

3)System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制

1.监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。

举一个例子: 系统崩溃前的一些现象:

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
  • 年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。

4.分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。

注:如果满足下面的指标,则一般不需要进行GC:

  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

5.调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6.不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

JVM调优参数参考

1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;

2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。

比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

3.年轻代和年老代设置多大才算合理

1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC

2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率

如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根 据以下两点:

(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。

(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法-XX:+UseParallelOldGC

5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

JMM

java 内存模型