JVM面试题

只挑重点问的,jvm其实最常问的是你对jvm的理解和jvm调优相关的东西

=======================================

1.什么情况下会发生栈内存溢出

  • 首先知道栈是干嘛的?栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候都会创建一个栈帧,他包含局部变量表 操作数栈 动态链接 方法出口等信息,局部变量表又包括基本数据类型和对象的引用

  • 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出内存溢出异常,方法的无限递归调用肯定会出现该问题

  • 调整参数-xss去调整jvm的栈的大小

    总结一下:栈栈,一般是方法栈,递归调用会溢出,可以通过调整参数来扩大栈的空间,这样就能防止栈帧溢出

2.详解JVM内存模型

首先5大区域:程序计数器 虚拟机栈 本地方法栈 java堆 方法区,怎么记住,死记就完事了,堆 栈,栈分为本地方法栈和虚拟机栈 方法区 程序计数器

  • 程序计数器:线程私有的,用于记录当前线程执行到哪里了

  • 虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于记录当前虚拟机正在执行的线程指令地址

  • 本地方法栈:线程私有的,保存的是native方法的信息,简单动态链接直接调用该方法,他其中是c++代码,相当于是用c++来执行java代码

  • 堆:所有线程共享的一块内存,几乎所有的对象实例和数组都要在堆上分配内存,因此该区域经常会发生垃圾回收的操作

  • 方法区:存放已被加载的类的信息常量 静态变量 ,即时编译的代码数据,即永久代,在jdk1.8不存在方法区了,被元数据区取代,原方法区被拆分成两部分:1.加载的类信息 2.运行时常量池:加载的类信息被保存在元数据区中,运行时常量池保存在堆中。

    其实这个里面最难记住的就是一个方法区,jdk1.8之后没有了方法区,变成了元数据区,元数据区被分为两部分:元数据区和运行时常量池,运行时常量池是保存常量,元数据区是保存加载的类的信息

3.jvm中一次完整的gc是什么样子的,对象如何晋升成老年代

java堆=新生代+老年代

新生代=eden+suivivor 当eden区空间满了的时候,就会触发一次GC,以收集新生代垃圾,存活下来的对象会被分配到sunvivor区大对象,会直接被分配到老年代,如果对象在eden中出生,并且在经历了一个gc之后仍然存活,年龄就+1,以后每次gc,年龄都会+1,当老年代满了,无法容纳更多对象的话,就会触发一个gc。收集老年代的对象

简而言之:eden这是新生代,满了就gc,没被收集年龄就+1,年龄到15就进入到老年代,

4.java中的垃圾回收算法

四种算法:标记清除算法 标记整理算法 复制算法 分代收集算法

  • 标记清除算法:利用可达性去遍历内存,把存活对象和垃圾对象进行标记,在遍历一遍,将所有的标记对象回收掉,特点:效率不行,标记和清楚的效率都不高,标记和清楚后会产生大量的不连续的空间分片,可能会导致之后的程序运行的时候需要分配大对象而找不到连续分片而不得不触发一次GC(对标记计数,会产生内存碎片,)

  • 标记整理算法:利用可达性去遍历内存,把内存对象和垃圾对象进行标记,第二步:在遍历一遍,将所有的标记对象回收掉:特点:将所有的存活对象向一段移动,将端边界以外的对象都回收掉:特点:、适用于存活对象多,垃圾少的情况,需要整理的过程,无碎片产生(对标记进行flag赋值,)

  • 复制算法:将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后再把使用过的内存空间移除,特点:不会产生空间碎片,内存的使用效率低

  • 分代收集算法:根据内存对象的存活周期不同,将内存划分成为几块,java虚拟机一般将内存分为新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收。

    总而言之:四种你能说出来吧:标记清理算法,标记清除算法,复制算法,分代收集算法

    标记清理算法,加一个标记,一般是数字,对于频繁使用的对象,不能用标记清理算法,而且清理效率不高,容易产生内存空间碎片,所以又有了标记整理算法,这个就是标记给一个flag来判断对象是否存活,解决了标记清楚算法的内存碎片问题,其次就又出来了复制算法,分成2块,每次使用1块,存活的放另1块,然后清空第一块,适合瞬间结束大量对象,不会产生空间碎片,还有就是分代收集算法,这个就简单,分代收集算法,就是根据内存对象的存活周期不同,划分成多块,java虚拟机中一般将内存分为新生代和老年代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,一下清理,敲黑板!!新生代大量对象用复制算法,对于老年代中因为对象的存活率极高,所以采用标记清理或者标记整理算法进行回收,建议使用标记整理算法,因为这样会减少碎片的产生

5.如何判断一个对象是否存活

引用计数法和可达性分析法

  • 引用计数法,相当于标记清理算法一样绑定一个计数器,有了+1,没有了-1,0的话就是没有,此时也就无法垃圾回收

  • 可达性分析,被称为gc roots对象向下搜索,没有任何引用链相连接时,说明该对象不可用,有了说明可用

6.有哪几种垃圾回收器,有哪些优缺点

  • serial:单线程收集器,收集垃圾时,采用复制算法

  • parnew:收集多线程,复制算法

  • 新生代垃圾收集器,复制算法收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量

  • 老年代收集器 标记整理算法

  • 还有一种标记清楚算法的收集器

7.什么是类加载

虚拟机吧描述类的数据加载到内存里,并对数据进行校验解析 初始化,最终编程虚拟机直接使用的class对象

8.类加载的过程

加载--连接--初始化

连接中又包含了验证 准备 解析

加载 验证 准备 解析 初始化 加载 加载分为三步

  • 通过类的全限定性类名获取该类的二进制流

  • 将该二进制流的静态存储结构转为方法区中的运行时数据结构

  • 在堆中为该类生成一个class对类

  • 验证:验证该class文件找那个字节流是否符合虚拟机的要求,不会威胁到jvm的安全

  • 准备:为class对象的静态变量分配内存,初始化其初始值

  • 解析:该阶段主要完成符号引用转换成直接引用

  • 初始化:到了初始化阶段,才开始执行类中定义的java代码,初始化阶段是调用类构造器的过程

9.什么是类加载器,常见的类加载器有哪些

类加载器:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器,类加载器分为以下四种:启动类加载器,用来加载java核心类库,无法被java程序直接引用

扩展类加载器:用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类

系统类加载器:用于架子啊java扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类

自定义类加载器:由java语言实现,继承自classloader

启动类加载器classloader 扩展类加载器 extensions classloader 系统类加载器 syatem class loder 用户自定义类加载器

10.什么是双亲委派机制

当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载器去加载,只有福来加载器在自己搜索范围类查找不到该类时,子加载器才会尝试自己去加载该类

11.为什么需要使用双亲委派机制

为了防止内存中出现多个相同的字节码,如果有双亲委派机制,只让同一个类去读,重复的就不进行加载了

12.怎么打破双亲委派模型

自定义类加载器,继承classloader类,重写loadclass方法和findclass方法

13.内存泄漏和内存溢出的区别

内存泄漏

内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。内存泄漏不好理解,举个例子,就相当于你租了个带钥匙的柜子,但是钥匙丢了,你打不开柜子了,自己也用不了,别人也用不了,这个就是叫内存泄漏

内存溢出

当程序申请内存时,没有足够的内存供使用者使用,内存溢出,假如你申请的是int,存的数据都是long或者递归调用方法等,都会造成内存溢出,内存泄漏的堆积也会造成内存溢出。通常用—xss来扩大栈内存容量,来防止内存泄漏

14.jvm调优

jvm调优目标,使用较小的内存占用来获得较高的吞吐量或较低的延迟、

  • 内存占用:程序正常运行需要的内存大小

  • 延迟:由于垃圾收集引起的程序停顿时间

  • 吞吐量:用户程序运行时间占用用户程序和垃圾收集占用总时间的比值

jvm优化工具:系统运行日志,堆栈错误信息,gc日志,线程快照,堆转储快照等,系统巡行日志就是在程序代码中打印出日志,描述了代码级别的系统运行轨迹堆栈错误信息,当系统出现异常后,可以根据堆栈信息初步定为问题,线程快照,堆转储快照,jps可以查看虚拟机启动的所有进程,执行主类的全名,jvm启动参数,比如执行了maain方法后,执行jps

或者用jstat监视虚拟机信息,jmap查看堆内存信息 jconsole jvisualvm分析内存信息

JVM调优经验

jvm配置方面,一般情况可以使用默认配置,在测试中根据系统运行状况,结合gc日志,内存监控,使用的垃圾收集器进行合理的调整,当老年代内存过小时可能引起频繁的gc,当内存过大时gc时间会特别长

那么jvm的配置比如新生代 老年代应该配置多大啊最合适呢,

  • -xms 和xmx的值设置成相等,堆大小默认为xms指定的大小,默认空闲堆小于40%时,jvm会扩大堆到-xmx指定的大小,空闲堆内存大于70%时,jvm会减小堆到-xms指定的大小,如果gc满足不了内存需求会动态调整,这个阶段比较耗费资源。初始堆和变化的堆大小相等。

  • 新生代尽量设置大一些。让对象在新生代中多存活一段时间,

  • 老年代如果使用cms收集器,新生代不用太大,因为cms的冰箱收集速度很快,收集过程中比较耗时的并发标记和并发清楚阶段都可以和用户并发执行

  • 方法区大小的设置,1.7只要差不多能装下启东市和斗气动态加载的类信息就行

  • 如果是堆内存不够: 尝试调整-Xmx,–Xms选项,这个值代表最大堆内存和初始化堆内存的大小

  • 如果是想提高系统的并发性能: 可以尝试降低–Xss的值,这个值代表每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。别调太小了,太小了栈溢出了。

  • 调整对象在年轻代存活的时间: -XX:MaxTenuringThreshold 默认值15,这个值代表垃圾最大年龄,对于老年代比较多的应用,减少这个值可以提高效率。对于年轻代比较多的应用,增加这个值可以增加数据在年轻代即被回收的概率。这个值调整需要尤其注意,设置小了可能引发老年代频繁full GC,设置大了可能导致某些数据长期存活于新生代,每一次Minor GC都要拷贝它,很影响性能的。

  • 调整CMS垃圾回收器并行线程数: -XX:ConcGCThreads=4 CMS垃圾回收器并行线程线,推荐值为CPU核心数。记得把最小值和最大值设置成同一个: 应尽量把永久代的初始值与最大值设置为同一值,因为永久代的大小调整需要进行FullGC才能实现。设置为同一个就可以防止内存抖动。