JVM的一些常见面试题:
说说类加载过程
加载->链接(验证,准备,解析)->初始化
加载阶段::查找并加载类的二进制数据
验证:确保加载的类的正确性,比如是否符合JVM字节码规范,是否cafa babe开头,方法的参数个数和类型是否正确
准备:为类的静态变量分配内存,并将其初始化为默认值null,或者0,
解析:把类中的符号引用转换为直接引用,符号引用是一个指向某对象的一个标识,并不指向真实的内存地址,编译时生成;而直接引用则是对符号引用进行解析,并找到引用对应的实际的内存地址或偏移,运行时生成。
初始化:执行类构造器方法,对类的静态变量,静态代码块执行初始化操作。直接对静态字段赋值也会被加入到类构造器方法里面(按照先后顺序执行),主体部分就是静态代码块。(类构造器方法!=构造方法,而是静态代码块和直接赋值)
类加载器有几种?如何破坏双亲委派机制?
系统默认有三种。引导类加载器,拓展类加载器,系统类加载器;不过还可以自定义类加载器。
自定义类加载器(继承ClassLoader),重写loadClass方法。因为双亲委派机制的逻辑在loadClass方法里。
如果单纯想要自定义一种类加载逻辑,该类和系统的类没有冲突,则继承ClassLoader,重写findClass方法即可,在使用loadClass找不到的时候就会使用重写的findClass加载类。
如何自定义类加载器?
创建一个类,继承ClassLoader类,重写findClass方法;loadClass方法会先通过双亲委派机制加载类,加载不到的情况下会通过调用重写的findClass方法来从指定的位置加载类。
jdk1.8默认的垃圾收集器是什么?
- 1.7和1.8使用parellelGC和parellelOldGC,1.9之后一直是g1
如果查看当前使用的是什么垃圾收集器?(如何查看jvm参数?)
- 通过命令java -XX:+PrintCommandLineFlags -version可以打印jvm的各种参数,里面就有使用的垃圾收集器。
cpu一直占用过高,如何排查?
- 首先使用top命令来找到cpu占用高的进程(window使用jps),如果是java进程,记住pid。
- 然后再使用top -Hp pid来找出该进程中占用cpu最多的线程。
- 将堆栈信息保存到一个.log文件,使用命令 jstack pid > thread_stack.log
- 最后打开该文件,将线程的pid转换成16进制,然后在该文件里面找到该线程的信息就可以了。
高内存占用,如何排除?
- 首先使用top命令来找到内存占用高的进程(window使用jps),如果是java进程,记住pid。
- 使用ps p pid -L -o pcpu,pmem,pid,tid,time,tname,cmd 找到进程中内存占用高的线程pid
- 将堆栈信息保存到一个.log文件,使用命令 jstack pid > thread_stack.log
- 最后打开该文件,将线程的pid转换成16进制,然后在该文件里面找到该线程的信息就可以了。
什么时候会触发GC?
当Eden满了的时候,会触发minorGC
当老年代空间不足的时候,会触发fullGC
当方法区空间不足的时候,也会触发fullGC
当minorGC进入老年代的数据大小大于老年代剩余空间大小的时候,会触发fullGC
调用System.gc()建议执行垃圾回收的时候会调用fullGC
GC如何判断一个对象是否可回收?
引用计数法:每个对象都有一个引用计数器,用来统计指向对象的引用。有新引用指向对象,引用计数器加一,一个引用不再指向对象,引用计数器减一,如果引用计数器为零,对象会可回收。(不过java中没有采用该方法,因为无法处理循环引用)
可达性分析法:从GC Roots开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明该对象不可用,虚拟机就判断该对象可回收。不过可回收不代表一定会被回收,有可能在即将回收对象,执行finalize方法的时候重新有引用指向该对象,那么该对象就会复活,不过只可以复活一次。
哪些对象可作为GC Roots?
虚拟机栈引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中引用的对象。
说说锁升级过程
在jdk6之前,只有重量级锁,因此锁的性能较差。
之后就引入了偏向锁和轻量级锁,并有了锁升级的过程。
一开始加的是偏向锁,虽然叫锁,但实际并没有做锁操作,只是在锁对象的mark word里面记录获取偏向锁的线程id,这样下次该线程进入的时候只需要判断一下是否是同一个线程就可以直接进入同步块执行,连cas都不用,性能好。
如果后面尝试获取偏向锁的是另一个线程,在发现偏向锁记录的线程id不是当前线程的时候,会去尝试获取偏向锁。jvm会根据存储的线程id去查看原线程是否还存活,如果不存活则新线程获取锁成功,如果原线程还存活则在安全点STW暂停原线程,并通过判断原线程的栈帧是否还有该锁记录来判断原线程是否还在同步块中,如果没有锁记录,则代表不在同步块,撤销偏向锁,新线程获取锁;如果还在同步块则将锁升级为轻量级锁。
轻量级锁的时候,新线程会通过自旋来获取锁(通过CAS操作修改锁对象的Mark Word),如果成功获取到锁则在线程栈帧中存储锁记录(mark word,是复制一份),并让锁的对象头指向该记录;如果自旋到一定次数还未获取到锁(或等待线程数超过内核可用CPU数、或已有线程因获取锁失败进入阻塞状态),则需要将锁升级为重量级锁来减少自旋造成的性能损耗
重量级锁(创建Monitor对象,并通过CAS将锁的对象头指向Monitor对象),并阻塞还在该锁对象自旋的线程,将这些线程封装成一个个节点加入到锁的等待队列,等待被唤醒。
轻量级锁通过修改锁对象头的指向的锁记录来实现,指向哪个线程持有的锁记录,就是哪个线程正在持有锁。
重量级锁获取锁通过修改Monitor对象的持有锁线程指针来实现,重量级锁都共用一个Monitor对象。
monitor对象:
typedef struct {
volatile intptr_t _owner; // 持有锁的线程指针
ObjectWaiter* _EntryList; // 阻塞等待锁的线程队列
ObjectWaiter* _cxq; // 竞争锁的线程栈
volatile int _recursions; // 重入次数
volatile int _count; // 锁计数器
} ObjectMonitor;
优化建议:
尽量缩少同步块的大小
竞争激烈可考虑关闭偏向锁
也可以调整自旋阈值
未完待续…