JVM面试

1、谈谈你对jvm的理解

java虚拟机,它是JRE的一部分,是java语言跨平台的关键,我们写的.java程序通过编译成.Class文件,.Class文件是不能够我们的具体操作系统平台所识别的,那jvm就可以把我们的.class文件解释成具体平台上的机器指令码去执行。

2、JVM 的主要组成部分及其作用

JVM包含两个子系统和两个组件,两个子系统为类装载、执行引擎;两个组件为运行时数据区、本地接口。

  • Classloader(类装载):类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
  • Execution engine(执行引擎):执行classes中的指令。
  • Native Interface(本地接口):是其它编程语言交互的接口。
  • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

作用 :首先通过编译器javac把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

3、Jvm内存模型

Jvm运行时数据区分为线程私有区和线程共享区
线程私有区分为虚拟机栈,本地方法栈,以及程序计数器;线程共享区分为方法区和java堆。

  • 程序计数器:当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java虚拟机栈(Java Virtual Machine Stacks):描述的是java方法执行的内存模型,每一个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息;每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈入栈到出栈的过程。
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的;
  • Java堆(Java Heap):Java虚拟机中内存大的一块,是被所有线程共享的,在虚拟机启动的时候创建,所有的对象实例以及数组都要在堆上分配内存。
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

4、堆,栈,队列的区别

  • 栈内存存储的是局部变量,8大基本数据类型,对象的引用
  • 而堆内存存储的是对象的实例和数组;所有引用类型的真实对象
  • 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
    栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。栈内存存放的变量生命周期一旦结束就会被释放,他的生命周期和线程相同。而堆内存存放的实体会被垃圾回收机制不定时的回收。
  • 队列是在队尾入队,队头出队。队是先进先出,而栈的进栈和出栈都是在栈顶进行的,是先进后出。

5、3种jvm

  1. Sun公司的HotSpot 是目前使用范围最广的Java虚拟机。
  2. BEA公司的JRockit
  3. IBM公司的J9 VM 是一个高性能的企业级 Java 虚拟机

6、堆分为几个区域?

默认情况下,分配堆的总内存是电脑内存的1/4,而初始化内存是电脑内存的1/64
分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念(JDK1.8之后为metaspace替代永久代),在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出放在了方法区。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。新生代默认的空间占比总空间的 1/3,老生代的默认占比是2/3

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

新生代使用的是复制算法,新生代里有3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor变From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

7、对象创建的几种方式?

  • 使用new关键字 调用了构造函数
  • 使用Class的newInstance方法 调用了无参构造函数
  • 使用Constructor类的newInstance方法 调用了构造函数
  • 使用clone方法 没有调用构造函数
  • 使用反序列化 没有调用构造函数

8、对象创建的主要流程,以及内存分配

虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,先执行相应的类加载。类加载之后,分配内存。为对象分配内存。类加载完成后,接着会在Java堆中划分一块内存分配给对象。

内存分配根据Java 堆是否规整,有两种方式:

指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的 放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。

空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲。 对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A分配内存,指针还没来得及修改,对象 B又同时使用了原来的指针来分配内存的情况。

解决这个问题有两种方案:

对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并 分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使 用TLAB。

9、oom内存溢出

加载了过多class或者创建了过多对象,给JVM分配的内存不够导致
比如一个启动类大量加载第三方jar包,Tomcat部署太多应用,大量动态生成的反射类不断被加载,直到内存满载,就会出现oom
可以把堆内存扩大,如果扩大了还有错误就是代码问题;分析内存,可以使用jpofiler工具分析

10、双亲委派模型的工作过程如下:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是包这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求最终都传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围内没有找到所需要的类)时,子加载器才会尝试自己去加载。

如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。

11、双亲委派模型的好处:

无论那个类加载器去加载这个类的话,它都会最终委派到处于模型最顶端的启动类加载器进行加载,因此这个类在程序的各种类加载器环境中都会是同一个类,如果没有这个双亲委派模型的话,由各个类去自行加载的话,比如用户编写的String类要去的加载的话,那系统中就会出现不同的String类,那应用程序就会变得混乱

1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
2)同时也避免了类的重复加载。

12、自定义类加载器

自定义的类加载器只需要继承ClassLoader,把自己的类加载器的逻辑写到findClass方法。

13、垃圾回收GC

清理那些没有被任何引用的对象,加到要回收的集合中,进行回收
垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行。

14、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当我们创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
可以。程序员可以手动执行System.gc(),通知GC运行,但是并不保证GC一定会执行。

15、怎么判断对象是否可以被回收?

一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1, 当计数器为0时就可以被回收。它有一个缺点不能解决循环引用的问题;

  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。 当一个对象到 GC Roots没有任何引用链相连时,则证明此对象是可以被回收的

16、在Java中,对象什么时候可以被垃圾回收,JVM中的永久代中会发生垃圾回收吗

该对象不再被引用
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)

17、java8中虚拟机不同

Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区

18、说一下 JVM 有哪些垃圾回收算法?

  • 标记-清除算法(Mark-Sweep):首先标记出需要回收的对象,标记完成后统一回收被标记的对象
    缺点:效率问题,标记和清除效率不高,另一个问题是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大的对象时,无法找到足够连续的内存而不得不提前触发一次垃圾收集动作。

  • 复制算法:它把内存空间划为两个相等的区域,每次只使用其中一个区域。当一块用完的时候将活着的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。
    优点:分配时不需要考虑内存碎片,只要移动堆顶指针,按顺序分配内存即可,实现简单、运行高效,没有内存碎片。缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制,成本高

  • 标记–整理算法:标记的过程和标记清除算法一样,让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

  • 分代收集算法:据对象的存活周期将内存划分为新生代、老年代。在新生代中每次都有大批对象死去,只有少量存活,可使用复制算法,老年代中对象存活率高,使用标记-清除算法或者标记-整理算法。

19、说一下 JVM 有哪些垃圾回收器

Serial(单线程收集器):它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束!
ParNew收集器:是Serial的多线程版本,新生代收集器,可以与CMS收集器工作!
Parallel Scavenge(并行收集器):新生代收集器,使用复制算法,并行的多线程收集器,目标是打到一个可控制的吞吐量(吞吐量 = 用户线程时间/(用户线程时间+GC线程时间)),高效利用CPU。
Serial Old(Serial的老年代版本),使用标记-整理算法!
Parallel Old(Parallel Scavenge(并行收集器)的老年代版本):使用多线程和标记-整理法
CMS:老年代并行收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1:Java堆并行收集器,整堆回收器

20、详细介绍一下 CMS 垃圾回收器?

CMS是以最短回收停顿时间为目标的垃圾回收器。对于要求服务器响应速度,希望系统的停顿时间最短,以给用户带来较好的体验的应用上,这种垃圾回收器非常适合。在启动JVM 的参数加上“- XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。 CMS 使用的是标记-清除的算法实现的, 所以在 gc的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,不得不触发一次full gc。CMS收集器会有启动阈值,要是运行期间预留的内存无法满足程序需要,就会系统将会出现 Concurrent Mode Failure,临时 CMS会采用 Serial Old 回收器进行老年代垃圾清除,此时的性能将会被降低。

21、简述java内存分配与回收策率以及Minor GC和Major GC

对象优先在Eden区分配,当Eden区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次GC后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。对于大对象直接进入老年代,需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发GC以获取足够的连续空间来安置新对象。
对于长期存活对象将进入老年代,如果对象在 Eden 区出生, 并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬 过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

22、Minor GC 和Full GC (Major GC)

Minor GC是指发生在新生代的GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;Full GC 是指发生在老年代的 GC,出现了 full GC 通常会伴随至少一次 Minor GC。Full GC 的速度通常会比 Minor GC 慢 10 倍以上。

23、类装载方式,有两种 :

1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

24、类加载器有哪些

  • 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,用来加载Java_HOME/lib/目录中的,或者被-Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库
  • 扩展类加载器(extensions class loader): 它用来加载 Java 的扩展库,负责加载\lib\ext目录或Java. ext.dirs系统变量指定的路径中的所有类库 。
  • 应用程序类加载器(system class loader ):负责加载用户类路径(classpath)上的指类库,一般情况,如果我们没有自定义类加载器,一般情况下,这个就是程序中默认的类加载器。
  • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

25、说一下类装载的执行过程?

  • 加载:通过类的全限定名来获取类的二进制字节流,并加载到运行时数据区的方法区中,并在内存中生成 class 对象
  • 验证:然后进行验证这个 class 文件的字节流是否符合当前虚拟机的要求,包括文件格式校验、元数据验证,字节码校验等
  • 准备:给类中的静态变量分配内存空间,设置类变量的初始化值;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为 一个标示,用一组符号来描述所引用的目标,而在直接引用直接指向内存中的地址,如果有了直接引用,那引用的目标必定在内存中存在。
  • 初始化:到了这个阶段,才真正开始执行类中定义的java代码(或者字节码)。

26、说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
jconsole:用于对JVM中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
jProfile:分析dump内存文件,快速定位内存泄漏,获取堆中的数据,获得最大的对象。

  • 线程详细信息:查看线程内部运行情况;
  • 死锁检查 ;查看堆内类、对象信息查看:数量、类型等
  • 线程信息监控:系统线程数量。
  • 线程状态监控:各个线程都处在什么样的状态下
  • CPU热点:检查系统哪些方法占用的大量CPU时间
  • 内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
  • 内存泄漏检查

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

  • -Xms2g:初始化推大小为 2g;
  • -Xmx2g:堆最大内存为 2g;
  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
  • -XX:+PrintGC:开启打印 gc 信息;
  • -XX:+PrintGCDetails:打印 gc 详细信息

28、深入理解垃圾收集器的选择策略

同时,先解释几个名次:
1,并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态
2,并发(Concurrent):用户线程和垃圾收集线程同时执行
3,吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

29、JVM中最大堆大小有没有限制?

1、一般初始堆和最大堆设置一样,但是如果不一样,从初始堆到最大堆的过程会有一定的性能开销,所以一般设置为初始堆和最大堆一样。64位系统理论上可以设置为无限大,但是一般设置为4G,因为如果再大,JVM进行垃圾回收出现的暂停时间会比较长,这样全GC过长,影响JVM对外提供服务,所以不能太大。一般设置为4G。
2、-XX:NewRaio和-XX:SurvivorRatio这两个参数,都是设置年轻代和年老代的大小的,设置一个即可,第一是设置年轻代的大小,第二个是设置比值,理论上设置一个既可以满足需求

30、什么是内存泄露

内存泄露的定义:对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的时候,就产生了内存泄露。要理解这个定义,我们需要理解对象在内存中的状态。
如下图所示,对象A引用对象B,A的生命周期(t1- t4)比B的生命周期(t2-t3)要长,当B在程序中不再被使用的时候,A仍然引用着B。在这种情况下,垃圾回收器是不会回收B对象的,这就可能造成了内存不足问题,因为A可能不止引用着B对象,还可能引用其它生命周期比A短的对象,这就造成了大量无用对象不能被回收,且占据了昂贵的内存资源。同样的,B对象也可能引用着一大堆对象,这些被B对象引用着的对象也不能被垃圾回收器回收,所有的这些无用对象消耗了大量内存资源。

31、怎样阻止内存泄露

1.使用List、Map等集合时,在使用完成后赋值为null
2.使用大对象时(比如那种很长的字符串以及数组),在用完后赋值为null
3.目前已知的jdk1.6的substring()方法会导致内存泄露
4.避免一些死循环等重复创建或对集合添加元素,撑爆内存
5.简洁数据结构、少用静态集合等
6.及时的关闭打开的文件,socket句柄等
7.多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘了注销该listener,可能就会产生内存泄露

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值