随着肺炎疫情的好转,身边认识的朋友也陆续完成了大厂的面试。他们遇到的问题可以说是五花八门,有原理,也有项目经验,最终每个人的面试结果也有好有坏。
以下是我整理了朋友面试遇到的一些疑难问题,我会尽量及时更新新的面试题。除了对自己的知识点查缺补漏外,也希望能让即将参加或者正在进行面试的朋友们有些收获!文章中有不足的地方也请指出,我会及时修改以免误导大家!在这里先提前预祝各位面试顺利!^o^
Java序列化和反序列化是指把Java对象转换为有序字节序列、有序字节序列恢复为Java对象的过程。在网络上传输或者保存在本地文件,保证对象的完整性和可传递性。
序列化流程:将对象实例相关的元数据输出->递归地输出类的父类描述->从最顶层的父类开始输出对象实例的实际数据值->从上至下递归输出实例的数据
注意以下几点:
1)仅对对象的状态(属性值)进行保存,而不管对象的方法
2)静态成员数据不能被序列化
3)当父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
1)将需要序列化的对象所属类实现java.io.Serializable标记接口(implements Serializable),若对象所属类未实现Serializable接口,序列化时会抛出java.io.NotSerializableException运行时异常。
其中serialVersionUID保证版本号的一致性,若不显式指定Java编译器会自动为其生成一个默认值(会随着类代码的变动而变动,我的同事就遇到过修改类代码后,调用方和被调用方因为serialVersionUID的不一致而引发的线上问题)。
2)在Java程序中使用对象输出流(ObjectOutputStream),通过其writeObject()方法来实现序列化操作
3)在Java程序中使用对象输入出流(ObjectInputStream),通过其readObject()方法来实现反序列化操作
JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法。这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
以下为常用方法
找到Class类
1)Class.forName("com.zdx.model.Person");
2)Person.Class
3)p.getClass()
获取类方法
Method[] methods = class.getDeclaredMethods();
Method m = class.getMethod("eat", int.Class, int.Class);
m.invoke(1,1);
获取属性
class.getFields()
1)异常方法异步重试
使用方法:保存出现异常的类名、方法名和入参,异步轮训调用接口获取数据库中保存的信息,通过反射的方式重新执行该方法!
1)Method#invoke 方法会对参数做封装和解封操作,当调用次数达到一定量的时候,还会导致 GC。
2)需要检查方法可见性、校验参数
3)反射方法难以内联,Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。
4)JIT 无法优化,因为反射涉及到动态加载的类型,所以无法进行优化。
主要分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。
方法区:主要存放类的信息、静态变量、常量、编译后的方法代码,永久代PermGen是方法区的实现,JDK1.8后永久代被移除换成了元空间Metaspace(元空间不在虚拟机中,而是使用本地内存,大小取决于32位/64位系统可虚拟的内存大小)。
堆:用来存放对象实例,根据对象的存活周期划分为新生代和老年代OldGen,新生代:老年代占比为1:2,新生代又可以细化为Eden:From Survivor:To Survivor占比为8:1:1。可通过jvm启动参数调整比例(-XX:SurvivorRatio=8)。
虚拟机栈:每个线程都有自己的一个虚拟机栈,栈保存着局部变量以及所有调用的方法的参数和返回值。其他线程无法访问该线程的栈中数据(堆内存和方法区中的静态变量等数据可以被线程共享)。栈仅能保存基本类型和对象引用。
本地方法栈:与虚拟机栈类似,本地方法即native方法。
程序计数器:用于记录每条线程当前所执行到的字节码行号(每条线程都有独立的程序计数器,且各线程间互不影响),以便线程切换后能恢复到正确的执行位置。
贴一段网上找的jvm参数配置
exec /usr/local/jdk1.8/bin/java -jar -Dorg.apache.tomcat.util.net.NioSelectorShared=false -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -server -Xmx4g -Xms4g -Xmn1g -XX:SurvivorRatio=8 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=2 -XX:-UseBiasedLocking -XX:-DisableExplicitGC -XX:+PrintFlagsFinal -XX:+PrintCommandLineFlags -verbose:gc -XX:+PrintClassHistogram -XX:+PrintReferenceGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=100M -Xloggc:/home/xxx/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/xxx -XX:ErrorFile=/home/XXX/hs_err_pid%p.log
堆区:
-Xms 初始堆大小,默认是物理内存的1/64
-Xmx 最大堆大小,物理内存的1/4
空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制,一般会将将两者设置相等
-Xmn 年轻代大小
-XX:NewSize 设置年轻代大小
-XX:MaxNewSize 年轻代最大值
-XX:SurvivorRatio 新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例
非堆区:
-XX:PermSize 设置持久代(perm gen)初始值(非堆内存初始值),物理内存的1/64
-XX:MaxPermSize 设置持久代最大值(最大非堆内存),物理内存的1/4
与垃圾回收相关的JVM参数:
打印相关
-XX:+PrintGCDetails 打印GC的细节
-XX:+PrintGCDateStamps 打印GC操作的时间戳
-XX:+PrintFlagsFinal 打印出XX选项在运行程序时生效的值
-XX:+PrintCommandLineFlags 打印HotSpotVM 采用的自动优化参数
-XX:+PrintClassHistogram 打印出实例的数量以及空间大小
-XX:+PrintReferenceGC 打印各种引用的处理时间
-XX:+PrintGCApplicationStoppedTime 打印应用被暂停的时间
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 用于记录STW发生的原因、线程情况、STW各个阶段的停顿时间等
-XX:+PrintHeapAtGC 打印堆信息
-XX:+PrintTenuringDistribution 打印每次Minor GC时Survivor区中各个年龄段的对象的大小
-XX:+UseGCLogFileRotation 打开或关闭GC日志滚动记录功能,要求必须设置 -Xloggc参数
-XX:NumberOfGCLogFiles=5 设置滚动日志文件的个数,必须大于1
-XX:GCLogFileSize=100M 设置滚动日志文件的大小
-Xloggc:/home/xxx/gc.log gc日志地址
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/xxx 当JVM发生OOM时,自动生成DUMP文件
-XX:ErrorFile 当由于目标应用程序中的错误而发生崩溃时
-verbose:gc 在控制台输出GC情况
配置相关
-XX:NewRatio 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold 控制新生代需要经历多少次GC晋升到老年代中的最大阈值
-XX:TargetSurvivorRatio 设置幸存区的目标使用率
-XX:ParallelGCThreads JVM在进行并行GC的时候,用于GC的线程数
-XX:-DisableExplicitGC System.gc()的调用就会变成一个空调用,完全不会触发任何GC
-XX:-UseBiasedLocking 禁用偏向锁(在存在大量锁对象的创建并高度并发的环境下禁用偏向锁能够带来一定的性能优化)
PS:
-Dorg.apache.tomcat.util.net.NioSelectorShared 不使用共享的selector,而是每个thread单独使用各自的selector
-Djava.security.egd SecureRandom在java各种组件中使用广泛,可以可靠的产生随机数。但在大量产生随机数的场景下,性能会较低。这时可以使用"-Djava.security.egd=file:/dev/./urandom"加快随机数产生过程
1)synchronized修饰的方法,在编译后会在字节码中添加了一个ACC_SYNCHRONIZED
的flags
当线程执行到某个方法时,JVM会去检查该方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了那线程会去获取这个对象所对应的monitor
对象(每一个对象都有且仅有一个与之对应的monitor
对象),获取成功后才执行方法体,方法执行完再释放monitor
对象,在这一期间,任何其他线程都无法获得这个monitor
对象。
2)synchronized修改的代码块,在编译后会在同步代码块前插入monitorenter
,在同步代码块结束后插入monitorexit
。
线程执行代码块时遇到的monitorenter
和monitorexit
指令依赖monitor
对象完成。
1)synchronized会自动释放锁,Lock需在finally中手工释放锁(unlock)
2)synchronized无法判断是否获取锁的状态,Lock可以(trylock)
3)用synchronized关键字没有获得锁的线程会一直等待下去,而Lock锁就不一定会等待下去,可以不用一直等待就结束了
4)synchronized不可中断,而Lock锁可中断
5)Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
1)数据库
利用主键唯一,存入主键,用完删除。
缺点:虽然实现简单但是操作数据库需要一定的开销,性能较差。
2)zookeeper
上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点。
缺点:每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点,性能上可能并没有缓存服务那么高。
3)redis
使用redisson(redis 官方的分布式锁组件),或者lua脚本
加锁
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"
解锁
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end"
缺点:需要设置超时时间(时间设置有误可能导致还没有执行完就释放锁)
1)数据存在内存中,每秒可以处理10 万+的读写操作。
2)定期通过异步操作把数据库数据 flush 到硬盘上进行保存。fork子进程持久化。
3)支持String 、List、Set、Sorted Set、Hash。
4)提供 RDB 和 AOF 两种数据的持久化存储方案。
5)redis是单线程的,无法充分利用多核服务器的CPU。