# 说一下 jvm 的主要组成部分?及其作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
作用:首先通过类加载器会把 java 代码转化为字节码文件,运行时数据区再把字节码加载到内存中,而字节码文件只是 jvm 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令器执行引擎,将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程需要调用其他语言的本地库接口来实现整个程序的功能。
# 说⼀下 jvm 运行时数据区?
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- 本地方法栈(Native Method Stacks):与虚拟机栈的作用一致,只不过虚拟机栈是服务 java 的,而本地方法栈是为了虚拟机调用 Native 方法服务的
- Java 堆(Java Heap):Java 虚拟机内存中最大的一块,是被所有线程共享的,几乎所有的对象实例都是在这里分配内存的
- 方法区(Method Area):用于已被虚拟机加载的类信息,常量、静态变量、及时编译后的代码等数据
# 说一下堆栈的区别?
- 性能方面:堆是用来存储对象的,栈是用来执行程序的
- 共享性:堆是线程共享的,栈是线程私有的
- 空间大小:堆大小远远大于栈
# 队列和栈是什么?有什么区别?
队列和栈都是用来预存储数据的
队列允许先进先出检索元素,但也有例外的情况,Deque 接口允许从两端检索元素
栈和队列很相似
# 说⼀下类装载的执行过程?
- 加载:根据查找的路径找到 lclass 文件然后导入
- 检查:检查加载 class 文件的正确性
- 准备:给类中的静态变量分配内存空间
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标识,而直接引用直接指向内存中的地址
- 初始化:对静态变量和静态代码块执行初始化工作
# 怎么判断对象是否可以被回收?
- 引用计数法:没每个引用对象创建一个计数对象,有对象引用时计数器 + 1,引用被释放时计数器 - 1,当计数器为 0 时就可以被回收,它的缺点是不能解决循环引用的问题
- 可达性分析法:从 GC Roots 开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以回收的
# java 中都有哪些引用类型?
- 强引用:发生 gc 时不会被回收
- 软引用:有用但不是必须的对象,在发生内存溢出时之前会被回收
- 弱引用:有用但不是必须的对象,在下一次 gc 时会被回收
- 虚引用(幽灵引用 / 幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知
# 说一下 jvm 有哪些垃圾回收算法?
- 标记 - 清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片
- 标记 - 整理算法:标记无用对象,让所有存活的对象向一端移动,然后清除端边界以外的内存
- 复制算法:按容量大小划分为两个大小相等的内存区域,当一块用完时将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用效率不高,只有原来的一半
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法
# 说一下 jvm 有哪些垃圾回收器?
- Serial:最早的单线程串行垃圾回收器
- Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可作为 CMS 垃圾回收器的备选方案
- ParNew:是 Serial 多线程版本
- Parallel 和 ParNew 收集器类似是多线程,但 Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量
- Parallel Old 和 Parallel 老生代版本,Parallel 使用的是复制的内存回收算法,Parallel Old 使用的是标记 - 整理的内存回收算法
- CMS:一种以获得最短停顿时间为目标的收集器,非常适合用于 B/S 系统
- G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK9 以后默认的 GC 选项
# 详细介绍一下 CMS 垃圾回收器?
CMS(Concurrent Mark Sweep):是以牺牲吞吐量为代价来获得最短回收停顿时间得垃圾回收器,对于服务器要求响应速度得应用上来说,这种垃圾回收器非常适合,在启动得 jvm 参数上加上 “-XX:+UseConMarkSweepGC” 来指定使用 CMS 垃圾回收器
CMS 使用得是标记 - 清除算法,所以在 gc 回收时会产生大量得内存碎片,当剩余内存不能满足程序运行要求时,系统会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时性能会降低
# 新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低,老年代垃圾回收器一般采用的是标记 - 整理算法进行垃圾回收
# 简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老年代和新生代,新生代默认的空间占比总空间的 1/3,老年代默认占比 2/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)时,升级为老年代,大对象也会直接进入老年代。
老年代当空间占用到达某个值之后就会触发全局垃圾回收,一般使用标记整理的执行算法,以上这些循环往复就构成了整个分代垃圾回收的整个执行流程。
# 说一下 jvm 调优的工具?
jdk 自带了很多监控工具,都在 jdk bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具,第三方有 MAT(Memory Analyzer Tool)、GChisto
jconsole:用于对 jvm 中的内存、线程和类进行监控
jvisualvm:jdk 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 的变化等
MAT:一个基于 Elclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
GChisto:一款专业分析 gc 日志的工具
# 常用的 jvm 调优的参数都有哪些?
- -Xms2g:初始化堆大小为 2g
- -Xmx2g:堆最大内存为 2g
- -XX:NewRatio=4:设置年轻代和老年代的内存比例为 1:4
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 的比例为 8:2
- -XX:+UseParNewGC:指定使用 ParNew+Servial Old 垃圾回收器组合
- -XX:+UseParalleOldGC:指定使用 ParNew+ParNew Old 垃圾回收器组合
- -XX:+UseConMarkSweepGC:指定使用 CMS+Serial Old 垃圾回收器组合
- -XX:+PrintGC:开启打印 GC 信息
- -XX:+PrintGCDetails:打印 GC 详情信息
# 对象分配规则
- 对象优先分配到 Eden 区。如果 Eden 区没有足够的空间时,虚拟机进行一次 Minor GC
- 大对象直接进入老年代(大对象是指需要打量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
- 长期存活的对象直接进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC,那么对象会进入 Survivor 区,之后每经过一次 Minor GC,对象年龄加 1,直到到达阈值,对象进入老年区
- 动态判断对象的年龄。如果 Survivor 区相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象直接进入老年代
- 空间分配担保。每次进行 Minor GC,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC,如果小于检查 HandlePromotionFailure 设置,如果 true 则只进行 Minor GC,如果 false 则进行 Full GC
# 解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法
通常定义一个基本数据类型的变量,一个对象的引用,还有函数调用的现场保存都使用内存中的栈空间,而通过 new 关键字和构造器创建的对象放在堆空间中,程序中的字面量如直接书写的 10、hello world 和常量都是放在静态区中的。
栈空间操作最快但是栈很小,通常最大的对象放在堆空间,理论上整个内存没有被其他进程使用的空间甚至硬盘上虚拟内存都可以被当成堆空间使用。
String str = new String("Hello World"); |
上面语句变量 str 放在栈中,用 new 出来的字符串对象放在堆上,而 "Hello World" 放在静态区的。
# Java 对象创建过程
- JVM 遇到一条新建对象的指令时,首先去检查这个指令的参数是否在常量池中定义到一个类的符号引用,然后加载这个类。
- 为对象分配内存,一种办法是 “指针碰撞”、一种是 “空闲列表”,最终常用的办法是 “本地线程缓冲分配(TLAB)”。
- 将除对象头外的对象内存空间初始化为 0。
- 对对象头进行必要的设置
# 类的生命周期
- 加载:查询并加载类的二进制数据,在 Java 堆中创建一个 java.lang.Class 类的对象
- 连接:又包含三个内容:验证、准备、解析
- 验证:文件格式、元数据、字节码、符号引用验证
- 准备:为类的静态变量分配内存,并将其初始化默认值
- 解析:把类的引用转化为直接引用
- 初始化:为类的静态变量赋予正确的初始值
- 使用:new 出对象,程序中使用
- 卸载:执行垃圾回收
# Java 中会存在内存泄漏吗?
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题;然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。
比如:Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器不会回收这些对象,然而这些对象可能存在无用的垃圾对象,如果不及时关闭(close)或者清空(flush)一级缓存就可能导致内存泄漏。
# 类加载器双亲委派模型机制?
当一个类收到类加载请求后,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类完成类的加载。
# 什么是类加载器,类加载器有哪些?
实现通过对类的权限定名获取该类的二进制字节流的代码叫做类加载器。
主要有以下四种类加载器:
- 启动类加载器(Bootstrap ClassLoader):用来加载类的核心库,无法被 java 程序直接引用
- 扩展类加载器(Extensions ClassLoader):用来加载 Java 的扩展库,Java 虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类
- 系统类加载器(System ClassLoader):根据 Java 应用的类路径(classpath)来加载类,一般来说,Java 应用的类都是由它来完成加载的,可以通过 ClassLoader.getSystemClassLoader () 来获取它
- 用户自定义加载器,通过继承 java.lang.ClassLoader 类的方式实现