1、说一下 jvm 的主要组成部分?及其作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2、说一下 jvm 运行时数据区域?
JVM 运行时数据区域可以分为以下几个部分:
- 程序计数器:当前线程所执行的字节码的行号指示器。
- Java 虚拟机栈:描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- 堆:Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
3、 说一下堆栈的区别?
- 栈内存存储的是局部变量;堆内存存储的是实体;
- 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
- 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。
4、队列和栈是什么?有什么区别?
在 JVM(Java 虚拟机)中,队列(Queue)和栈(Stack)是两种常见的数据结构。
队列是一种先进先出(First-In-First-Out,FIFO)的数据结构。它类似于排队,新元素从队列的尾部添加,而元素从队列的头部移除。队列常用于需要按照顺序处理元素的场景,例如任务队列、消息队列等。
栈是一种后进先出(Last-In-First-Out,LIFO)的数据结构。它类似于一叠盘子,新元素总是添加到栈的顶部,而元素总是从栈的顶部移除。栈常用于函数调用、表达式求值等需要后进先出顺序的操作。
以下是队列和栈的一些主要区别:
- 数据访问顺序:队列按照先进先出的顺序访问元素,而栈按照后进先出的顺序访问元素。
- 添加和删除元素:在队列中,元素从尾部添加,从头部删除;在栈中,元素总是在顶部添加和删除。
- 应用场景:队列适用于需要按照顺序处理元素的情况,例如排队、任务调度等;栈适用于需要后进先出顺序的操作,例如函数调用、表达式求值等。
- 空间管理:队列的空间可以动态扩展,当队列已满时,可以增加队列的容量;栈的空间通常是固定的,当栈满时会发生栈溢出错误。
在 JVM 中,队列和栈的具体实现可能因不同的 JVM 实现而有所差异。例如,Java 中的Queue
接口和Stack
类分别提供了队列和栈的基本功能。此外,JVM 还可能使用内部的数据结构来实现队列和栈,以提高性能和效率。
5、什么是双亲委派模型?
双亲委派模型是 Java 中的一种类加载机制,用于确保类的加载过程的安全性和正确性。
在双亲委派模型中,类加载器被组织成一个层次结构,从顶部到底部依次是启动类加载器、扩展类加载器和应用程序类加载器。当一个类需要被加载时,JVM 会首先将加载请求委派给父类加载器,如果父类加载器无法找到或加载该类,则再由子类加载器尝试加载。
双亲委派模型的主要优点包括:
- 安全性:可以避免用户自定义的类覆盖 Java 核心类库中的类,从而保证了 Java 平台的安全性。
- 一致性:可以确保同一个类在不同的类加载器中都是同一个实例,从而保证了类的一致性。
- 可扩展性:可以方便地实现类的热部署和热替换,从而提高了系统的可扩展性。
总之,双亲委派模型是 Java 中非常重要的一个机制,它保证了类的加载过程的安全性、正确性和一致性。
6、说一下类加载的执行过程?
在 Java 中,类加载的执行过程主要包括以下几个阶段:
- 加载 :将类的二进制数据从指定的源(如磁盘文件、网络连接等)读取到内存中,并创建一个代表该类的
Class
对象。 - 验证:验证类的字节码是否符合 Java 虚拟机的规范,确保类的正确性和安全性。这一阶段主要包括检查类的结构、语法、语义等。
- 准备:为类的静态变量分配内存,并设置默认初始值。这些静态变量将在类的初始化阶段被赋值。
- 解析:将类中的符号引用转换为直接引用,以便在运行时能够正确地访问对象的方法和属性。
- 初始化:执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。这是类加载的最后一个阶段,也是类真正开始执行的阶段。
需要注意的是,类加载的执行过程是由 Java 虚拟机自动完成的,开发人员通常不需要直接干预。但是,在某些情况下,例如需要动态加载类或实现自定义类加载器时,开发人员可能需要了解类加载的机制和过程。
7、怎么判断对象是否可以被回收?
- 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
- 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
8、java 中都有哪些引用类型?
在 Java 中,有以下几种引用类型:
- 强引用(Strong Reference):这是最常见的引用类型。当一个对象被强引用引用时,垃圾回收器不会回收该对象,直到所有对该对象的强引用都被删除。
- 软引用(Soft Reference):软引用用于描述一些还有用但并非必需的对象。在系统内存不足时,垃圾回收器会回收软引用对象。
- 弱引用(Weak Reference):弱引用也是用于描述非必需的对象。与软引用不同的是,垃圾回收器会更积极地回收弱引用对象,只要垃圾回收器发现弱引用对象,就会将其回收。
- 虚引用(Phantom Reference):虚引用主要用于跟踪对象的垃圾回收状态。它不能单独使用,必须与引用队列(ReferenceQueue)一起使用。当垃圾回收器准备回收一个对象时,会将该对象的虚引用加入到引用队列中。
9、说一下 jvm 有哪些垃圾回收算法?
- 标记-清除算法:只回收,不整理
- 标记-整理算法:标记-清楚算法的优化,解决了内存碎片的问题
- 复制算法:解决内存碎片
- 分代回收算法(常用):年轻代以复制为主,老年代以标记-整理为主
10、详细介绍一下 CMS 垃圾回收器?
CMS(Concurrent Mark-Sweep),是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上"-XX:+UseConcMarkSweepGC"来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除算法,所以在 gc 的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
11、新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法。复制算法的优点是效率高,缺点是内存利用率低;
老年代垃圾回收器一般采用的是标记-整理算法。
12、说一下 jvm 有哪些垃圾回收器?
- Serial:最早的单线程串行垃圾回收器,新生代垃圾回收器,使用复制算法。
- Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案,老年代垃圾回收器,使用标记-整理算法。
- ParNew:是 Serial 的多线程版本,新生代垃圾回收器,使用复制算法。
- Parallel 和 ParNew 收集器类似是多线程的,但 Parallel 吞吐量优先,可以牺牲等待时间换取系统的吞吐量,新生代垃圾回收器,使用复制算法。
- Parallel Old 是 Parallel 老年版本,Parallel 使用的是复制算法,Parallel Old 使用的是标记-整理的内存回收算法,是老年代垃圾回收器。
- CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统,老年代垃圾回收器,使用标记-清除算法。
- G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项,是整堆回收器,使用标记-整理算法。
13、简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代。
新生代的默认空间是 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 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)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
14、说一下 jvm 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
15、常用的 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 详细信息。
16、内存溢出、内存泄露、GC的基本概念
内存溢出:out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露:memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。其实说白了就是该内存空间使用完毕之后未回收。
gc分为full gc 跟 minor gc(Young GC也就是Minor GC),当每一块区满的时候都会引发gc。