声明:
- 背景:本人为24届双非硕校招生,已经完整经历了一次秋招,拿到了三个offer。
- 本专题旨在分享自己的一些Java开发岗面试经验(主要是校招),包括我自己总结的八股文、算法、项目介绍、HR面和面试技巧等等,如有建议,可以友好指出,感谢,我也会不断完善。
- 想了解我个人情况的,可以关注我的B站账号:东瓜Lee
Java程序的运行环境(Java二进制字节码的运行环境)
好处:
- 一次编写, 到处运行
- 自动内存管理,垃圾回收机制(GC)
面试题:
JVM组成:
-
JVM有什么作用?
JVM 其实就是一个虚拟机,它在计算机上模拟了一台真正的计算机,而且是与平台无关的,这意味着 Java 代码可以在任何支持JVM的平台上运行,而不需要考虑平台的具体细节。JVM 是 Java 的核心,它为 Java 语言提供了强大的功能和平台无关性。
JVM 的主要作用包括:
- Java 代码的编译和解释执行:Java代码首先被编译成字节码,然后由JVM解释执行字节码。JVM 使用即时编译技术(Just-In-Time Compilation,JIT)将频繁执行的代码编译成本地机器代码,从而提高执行效率。
- 内存管理:JVM 提供了垃圾回收机制,自动管理内存分配和释放,避免了内存泄漏和空指针异常等问题。
- 安全管理:JVM 可以对 Java 代码进行安全性检查,防止恶意代码对系统造成损害。
- 多线程支持:JVM 可以处理多个线程的并发执行,保证线程安全。
- 跨平台兼容性:JVM 实现了一种平台无关的执行环境,使 Java 代码可以在不同平台上运行。
-
什么是程序计数器
- 线程私有的(没有线程安全问题),内部保存的是字节码的行号,存储 正在执行的字节码指令地址。
- 把Java编译后的class文件,反编译成汇编语言代码后就能看到,程序的执行是按照命令的行号来的,程序计数器记录的就是这个行号(比如线程1执行到了第10行,然后cpu切换到了其他线程,其他线程执行完后继续来执行线程1,就会从程序计数器中得到第10行继续执行)。
-
什么是Java虚拟机栈(Java方法栈)
- 栈的特点就是先进后出 后进后出的数据结构
- 每个线程运行时所需要的内存,称为虚拟机栈(多个线程运行,就会创建多个虚拟机栈,线程安全的)
- 每个栈由多个栈帧(frame) 组成,对应着每次方法调用时所占用的内存(但是每个线程只能有一一个活动栈帧,对应着当前正在执行的那个方法)
-
什么是本地方法栈?
本地方法就是native method,它底层并不是由java实现的,一般是和操作系统底层比较相关的方法。
本地方法栈就是用来支持本地方法的调用逻辑的。
本地方法栈和虚拟机栈一样 都是用于支持当前线程的方法调用的,所以都是线程安全的。
-
垃圾回收是否涉及栈内存?
- 垃圾回收主要指就是堆内存,对象存储在堆内存,垃圾回收器回收的主要是对象
- 对于栈而言,当栈帧弹栈以后(当前方法执行完毕),内存就会释放,所以不需要被GC回收
-
栈内存分配越大越好吗?
- 未必,默认的栈内存通常为1024k(1m)
- 栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
-
什么情况下会导致栈内存溢出?
- 栈帧过多(方法调用过多)导致栈内存溢出,典型问题:递归调用
- 栈帧过大(每个方法分配的空间太大)导致栈内存溢出
- StackOverflowError
- 方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围(不作为形参也不作为返回值),它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围(比如作为形参和返回值),则需要考虑线程安全问题
-
你能给我详细的介绍下Java的堆吗?
-
是所有线程共享的内存空间(有线程安全问题),可以被垃圾回收器回收
-
存储对象实例和数组
-
当堆内存不够时,就会抛出OutOfMemoryError异常
-
堆是由年轻代和老年代组成的(比例为1:2)
年轻代又被分为了三个区域:Eden、survivor(from+to)
-
不同于数据结构中的堆(最大堆,最小堆)
-
-
能不能介绍一下方法区
- 方法区(Method Area)是各个线程共享的内存区域(具有线程安全问题)
- 方法区是一个抽象的概念,永久代(before)和元空间(now)是实现
- 主要存储类的信息、运行时常量池
- 运行时常量池可以看作是一张表, 虚拟机指令根据这张常量表找到要执行的信息
- 虚拟机启动的时候创建方法区,关闭虚拟机时释放方法区
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
-
堆和栈的区别是什么
-
jvm上:
- 堆内存是用来存储Java对象和数组的,堆内存会使用GC来执行垃圾回收,所有线程共有的(有线程安全问题)
- 栈内存一般会用来存储局部变量和方法调用,栈内存不会使用GC,不同的线程对应有不同的栈(没有线程安全问题)
- 两者出现内存溢出,都会抛异常,一个是OutOfMemoryError,一个是StackOverFlowError
- 栈的空间大小远远小于堆的空间
-
数据结构上:
- 栈是一种线性表,符合先进后出,后进先出的特点
- 堆是一种树形结构,是一种特殊的完全二叉树
- 如果所有的节点都>=它的父节点,那么这个堆就是最小堆
- 如果所有的节点都<=它的父节点,那么这个堆就是最大堆
-
-
你听过直接内存吗
- 直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高
- 普通IO也叫做BIO,NIO的效率更高(比如作文件拷贝的时候)
-
Java从源代码文件.java 到代码执行的过程
- **编译:**编译器将.java源代码文件编译成.class字节码文件(编译过程会对源代码做语法分析、语义分析、注解处理等等)
- **加载:**类加载器会将.class字节码文件加载到JVM中(加载过程又可以分为:装载、连接、初始化)
- **解释:**JVM可以把字节码转换为操作系统可以识别的指令
- **执行:**操作系统就可以调用CPU来执行指令了
-
什么是类加载器,类加载器有哪些?
JVM只会运行二进制文件,类加载器的作用就是将.class字节码文件加载到JVM中,从而解释执行字节码文件以启动Java程序。
- 启动类加载器(BootStrap ClassLoader):加载JAVA HOME/jre/lib目录下的库(底层是c++实现的)
- 扩展类加载器(ExtClassLoader):加载JAVA HOME/jre/lib/ext目录中的类
- 应用类加载器(AppClassLoader):加载classPath下的类
- 自定义类加载器(CustomizeClassLoader):继承自ClassLoader,实现自定义类加载规则
-
什么是双亲委派模型?
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果上级已经加载了这个类了,就直接返回给下级子加载器,如果该类委托的上级没有被加载,子加载器才会加载该类。
比如一个Student类,首先应用类加载器 向上委托到 扩展类加载器,扩展类加载器 继续向上委托到 启动类加载器,发现没有加载过这个类,那么子加载器(应用类加载器)就会加载Student类。
如果是String类,应用类加载器 一直向上委托到 启动类加载器,启动类加载器中已经加载过String类了,那么就可以直接返回给子加载器(应用类加载器)。
-
JVM为什么采用双亲委派机制?
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,以保证类加载的唯一性。
- 为了安全,保证类库API不会被修改(比如String类已经被加载了,又去定义一个String类就会报错)。
-
说一下类装载的执行过程?【待学习】
- 加载:查找和导入.class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量的初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量、静态代码块执行初始化操作
- 使用:JVM开始从入口方法执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始消耗创建的class对象
-
对象什么时候可以被垃圾回收器GC回收?
简单的来说就是对象没有被引用了,也就是被定义为了垃圾对象,就可以被垃圾回收器回收了。
那怎么定义对象是否为垃圾对象呢,一般会有两种方式:引用计数器法、可达性分析算法
-
引用计数法:每个对象都有一个引用计数器,记录了它被引用的次数,如果为0,就被定义为垃圾对象。
虽然用起来简单,但是具有一个致命的问题,就是循环引用的问题,比如一个对象A有个属性引用了对象B,对象B有个属性引用了对象A,那么即使他们俩没有被其它的变量引用了,他们对应的引用计数器的值还是都为1,那实际上这两个对象都不需要用了,但是垃圾回收器也回收不了他们,就会引发内存泄露的问题。所以Java不是采用这种机制。
-
可达性分析算法:会有个GC Roots对象作为根节点,沿着这个节点向下走,如果可以被扫描到的对象,就是可达对象,也就是不能被回收的,不可达的对象就说明是垃圾对象,要被回收。
可以作为GC Roots的对象一般有:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
-
-
JVM垃圾回收算法有哪些?
-
标记-清除算法:分为标记和清除两个阶段,首先使用可达性分析算法标记所有存活的对象,标记完成后 统一回收所有没有被标记的对象(也就是垃圾对象)。
缺点:标记清除之后会产生大量不连续的内存碎片,如果内存不连续了,则有可能不能存储一些占用内存大的对象或者数组。
-
标记-整理算法:对标记-清除算法的改进,也是先标记出所有存活的对象,然后把所有没有被标记的对象进行清除,但是它清除之后呢,会将目前存活的对象往一端移动,也就是进行整理,这样就使得内存是连续的,没有内存碎片了。
但是因为多了个整理的过程,所以垃圾回收的效率会受到影响。
-
复制算法:把内存分为大小相等的两块区域,一片区域B不存放对象,另一片区域A使用可达性分析算法标记出存活的对象,统一放置到区域B,然后再把区域A中的剩余对象(也就是要被回收的对象)给全部回收了。
优点:效率比较高,而且也没有内存碎片
缺点:实际可使用的内存空间缩小为原来的一半,内存利用率较低。
-
-
说一下JVM中的分代回收?
- 堆的区域划分
- 堆被分为了两份:新生代和老年代(1: 2)
- 对于年轻代,内部又被分为了三个区域,Eden区,幸存者区survivor(分成from和to)(8:1:1)
- 对象回收分代回收的策略
- 新创建的对象,都会先分配到Eden区
- 当Eden区内存不足时,使用可达性分析算法标记出Eden区和from区存活的对象,将存活的对象采用复制算法复制到to区中,复制完毕后,将Eden区和from区的内存释放
- 经过一段时间后,Eden区又出现内存不足,标记Eden区和to区的存活对象,将其复制到from区
- 当from区对象经过了很多次垃圾回收(默认15次),直接放到老年代
- 堆的区域划分
-
说一下JVM有哪些垃圾回收器?
-
串行垃圾回收器
是指使用单线程来进行垃圾回收,也就是垃圾回收时,只有一个线程在工作,适合堆内存较小的,比如说PC电脑。
而且一个线程在进行垃圾回收的时候,其他所有线程都要暂停,等待垃圾回收的完成,
具体来说包含Serial和Serial Old串行垃圾回收器:
- Serial作用于新生代,采用复制算法
- Serial Old作用于老年代,采用标记-整理算法
-
并行垃圾回收器
JDK8默认采用的就是并行的垃圾回收器,也就是说有多个线程同时进行垃圾回收,而且其他的线程也都要暂停,等待垃圾回收的完成,才能继续执行。
具体来说包含Parallel New和Parallel Old并行垃圾回收器:
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记-整理算法
-
并发(CMS)垃圾回收器
CMS是一个并发的、使用标记-清除算法的垃圾回收器,该回收器主要是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
-
G1垃圾回收器
-
-
强引用、软引用、弱引用、虚引用是什么?有什么区别?
引用主要是用来引用对象的,被分为四种类型,区别主要就是在对对象进行垃圾回收的时候,会有不同
- 强引用:最普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其引用的对象进行回收。
User user = new User();
- **软引用:**如果一个对象时被软引用的,那么在垃圾回收的时候,如果内存空间还足够,这个对象就不会被回收,如果再次垃圾回收的时候,发现内存空间不足了,才会被回收软引用的对象。
User user = new User(); SoftReference softReference = new SoftReference(user);
- 弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。
User user = new User(); WeakReference weakReference = new WeakReference(user);
4. 虚引用:配合引用队列使用
-
JVM调优的参数可以在哪里设置
-
使用SSM做开发的时候,war包部署在tomcat服务器中,可以在tomcat的文件中设置
修改TOMCAT_HOME/bin/catalina.sh文件
-
使用SpringBoot做开发的时候,是使用的jar包,在启动命令中设置
Java -Xms512 -Xmx1024m -jar
-
-
用的JVM调优的参数都有哪些?
JVM调优主要就是调整新生代、老年代的堆内存空间大小,虚拟机栈空间,元空间大小,还可以调整垃圾回收器的类型。
新生代中的对象什么时候进入老年代?
在对象被创建的时候,一般就是放在新生代,如果有这么几种情况就会放到老年代:
- 达到晋升的年龄:新生代对象在经历GC的时候,如果没有被回收,就会年龄+1,如果年龄到达阈值,就会进入老年代,默认情况下阈值为15,当然也可以通过JVM调优参数来设置。
- 如果创建的对象很大,也可能会直接进入老年代,具体的大小也可以通过JVM调优参数来设置。
-
说一下JVM调优的工具?
-
jdk自带的命令工具:
jps:进程状态信息
jstack:查看Java进程内线程的堆栈信息
jmap:查看堆转信息
jhat:堆转储快照分析工具
jstat:JVM统计监测工具 -
可视化工具
jconsole:用于对jvm的内存,线程,类的监控VisualVM:能够监控线程,内存情况
-
-
Java内存泄露的排查思路?
(在实际开发中,遇到内存溢出的情况,怎么解决?借助什么工具?解决流程?)
内存泄漏有几种情况:
- 虚拟机栈:StackOverFlowError,一般就是递归造成的,方法调用过多,栈帧过多,虚拟机栈空间有限,一般为1024k,就容易爆栈,可以去看下递归的边界条件有没有设置好
- 方法区:OutOfMemoryError:Metaspace,一般是动态加载的类太多了,可以从这个方面去排查
- 堆:OutOfMemoryError:java heap space,内存泄漏通常是指的堆内存,一般是出现了一些大对象没有被回收的情况
- 通过jmap或设置m参数 来获取堆内存的快照dump文件
- 通过VisualVM可视化工具工具 去分析dump文件,VisualVM可以加载离线的dump文件
- 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
- 找到对应的代码,通过阅读上下文的情况,进行修复即可
-
CPU飙高排查方案与思路?
- 在linux中可以使用top命令查看cpu的使用情况,可以看到是哪一个进程占用cpu较高
- 使用ps命令查看这个进程中的线程信息
- 使用jstack命令查看进程中哪些线程出现了问题,最终定位问题(可能的情况比如说,一个线程里面开了死循环)。
【后续继续补充,敬请期待】