Java底层–JVM与GC
一. JAVA的理解
1.1 谈谈你对Java的理解?
- 平台无关性 [一次编译,到处运行]
- GC
- 语言特性
- 面向对象
- 类库
- 异常处理
可以针对这几个方面去谈
1.2 Compile Once,Run Anywhere如何实现?
1.3 javac是编译,javap可以反编译,其API如下:
Linux下连接到远程:ssh work@115.28.159.6
本地文件上传到远程:scp xxx work@115.28.159.6:~
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台上运行不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令;
1.4 为什么JVM不直接将源码解析成机器码去执行
- 准备工作: 每次执行都需要各种检查
- 兼容性: 也可以将别的语言解析成字节码
二.JVM如何加载.class文件
2.1 Java虚拟机
- 只要这个平台有对应的Java虚拟机,就可以在这个平台上运行Java程序
- 加载流程图示:
- 结构解析:
- Class Loader: 依据特定格式,加载class文件到内存
- Execution Engine: 对命令进行解析
- Native Interface: 融合不同开发语言的原生库为Java所用
- Runtime Data Area:JVM内存空间结构模型
2.2 什么是反射
-
谈谈反射:
- JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制;
-
写一个反射的例子:
//反射执行Robbot类中的throwHello方法
public class ReflectSample{
public static void main(String[] args) throws ClassNotFounException ,IllegalAccessException{
Class rc=Class.forName("com.interview.javabasic.reflect.Robot");
Robot r=(Robot)rc.newInstance(); //创建一个实例
System.out.println("Class name is"+ rc.getName());
Method getHello=rc.getDeclaredMethod("throwHello",String.class); //能获取包括私有方法在内的所有方法,但是不能获取继承以及实现的一些方法
getHello.setAccessible(true); //设置为true才能访问私有
Object str=getHello.invoke(r,"Bob");
System.out.println("getHello result is "+ str);
//---------------另外一种Method方法---------------
Method sayHi=rc.getMethod("sayHi",String.class);
sayHi.invoke(r,"welcome"); //不能获取私有方法,但是可以获取到其他的方法以及继承方法和实现等;
}}
反射就是将java中的各种成分映射成一个个java对象
三. ClassLoader
3.1 类从编译到执行的过程
- 编译期将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转换为JVM重的Class对象
- JVM利用Class对象实例化为Robot对象
3.2 谈谈ClassLoader
- ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获取Class二进制数据流,它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作;
3.3 ClassLoader的种类
- BootStrapClassLoader :C++编写,加载核心库java.*
- ExtClassLoader:java编写,加载扩展库javax.*
- AppClassLoader:java编写,加载程序所在目录
- 自定义ClassLoader:java编写,定制化加载
ClassLoader有很多方法,其中一个比较重要的方法就是loadClass方法;
public Class<?> loadClass(String name) throws ClassNotFoundException{
return loadClass(name,false); //默认是false
}
3.4 自定义ClassLoader
- ClassLoader代码如下:
public class MyClassLoader extends ClassLoader{
private String path;
private String classLoaderName;
public MyClassLoader(String path,String classLoaderName){
this.path=path;
this.classLoaderName=classLoaderName;
}
//用于寻找类文件
@Override
public class findClass(String name){
byte[] b=loadClassDate(name);
return defineClass(name,b,0,b.length);
}
//用于加载类文件
private byte[] loadClassData(String name){
name=path+name+".class";
InputStream in =null;
ByteArrayOutputStream out=null;
try{
in =new FileInputStream(new File(name));
out=new ByteArrayOutputStream();
int i=0;
while((i=in.read())!=-1){
out.wirte(i);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
out.close();
in.close();
}catch(Exception e){
e.printStackTrace();
}
}
return out.toByteArray();
}
}
- 类如下:
public class Wali{
static{
System.out.println("Hello Wali");
}
}
- 测试类如下:
public class ClassLoaderChecker{
public static void main(String[] args) throws ClassNotFoundException IllegalAccessException{
MyClassLoader m=new MyClassLoader("/Users/baidu/Desktop/","myClassLoader");
Class c=m.loadClass("Wali");
System.out.println("Wali");
c.newInstance();
}
}
- 显示效果如下:
在自定义ClassLoader的时候,重写findClass方法时,我们不仅可以去加载二进制文件,因为defineClass传入只要是二进制流就是合法的,我们可以去远程访问一些资源,也可以对类文件的某些部分进行加密,findClass解密,也可以修改二进制代码,给类添加信息,即字节码增强机制,也可以延伸去考虑AOP的实现;
四. 类加载器的双亲委派机制
4.1 谈谈类加载器的双亲委派机制
- 图示:
解析: 有两个过程,一个过程是判断是否已经加载过,另外一个过程是进行加载;先进行判断是否已经加载过,这时是从下往上,比如最开始是CustomClassLoader有没有加载过,如果没有则交给AppClassLoader进行判断,一直到BootstrapClassLoader进行判断,如果它也没有加载过则开始进行第二个过程;第二过程是自顶向下,从BootstrapClassLoader进行判断,是否在指定的Jar包中,如果有则进行加载,如果没有则往下继续逐层判断;
4.2 为什么要使用双亲委派机制去加载类?
- 避免多份同样字节码的加载
- 保证java自身类的安全
4.3 类的加载方式
- 隐式加载:new
- 显式加载: loadClass ,forName 等
4.4 loadClass和forName的区别?
- 区别:
- Class.forName得到的class是已经初始化完成的
- Classloader.loadClass得到的class是还没有链接的
forName会加载类,执行其中的static静态方法,而Classloader则不会
- 类的装载过程:
- 加载:
- 通过ClassLoader加载class文件字节码,生成class对象
- 链接:
- 校验: 检查加载的class的正确性和安全性
- 准备: 为类变量分配存储空间并设置类变量初始值
- 解析: JVM将常量池内的符号引用转换为直接引用
- 初始化:
- 执行类变量赋值和静态代码块
- 加载:
五. 内存模型
5.1 内存简介
- 图示:
5.2 地址空间划分
- 内核空间:系统调用硬件等的内存空间
- 用户空间:Java实际使用的内存空间
5.3 JVM内存模型-JDK8
-
图示:
-
解析:
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:MetaSpace、Java堆
5.4 程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一条需要执行的字节码指令
- 和线程是一对一的关系即"线程私有"
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄露
5.5 虚拟机栈(Stack)
-
图示:
-
解析:
- Java方法执行的内存模型
- 包含多个栈帧
-
局部变量表和操作数栈:
- 局部变量表: 包含方法执行过程中的所有变量 [为操作栈做数据支撑]
- 操作数栈: 入栈、出栈、复制、交换、产生消费变量
5.6 常见问题
-
递归为什么会引发java.lang.StackOverflowError异常
- 递归过深,栈帧数超出虚拟栈深度
-
元空间(MetaSpace)与永久代(PermGen)的区别
- 元空间使用本地内存,而永久代使用的是jvm的内存
- 此异常不存在了: java.lang.OutOfMemoryError:PermGen space
-
MetaSpace相比PermGen的优势
- 字符串常量池存在永久代中,容易出现性能问题和内存溢出
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难;
- 永久代会为GC带来不必要的复杂性
- 方便HotSpot与其他JVM如Jrockit的集成;
5.7 Java堆(Heap)
- 图示:
它是对象实例的分配区域,也是GC管理的主要区域,也被称为GC堆;
5.8 常见问题
-
JVM三大性能调优参数 -Xms -Xmx -Xss的含义
- -Xss:规定了每个线程虚拟机栈(栈堆)的大小
- -Xms:堆的初始值
- -Xmx:堆能达到的最大值
-
Java内存模型中堆和栈的区别:
- 内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储: 数据区需要在编译时未知,运行时模块入口前确定
- 堆式存储: 编译时或运行时模块入口都无法确定,动态分配
- 管理方式:
- 栈自动释放,堆需要GC
- 空间大小:
- 栈比堆小
- 碎片相关:
- 栈产生的碎片远小于堆
- 分配方式:
- 栈支持静态和动态分配,而堆仅支持动态分配
- 效率:
- 栈的效率比堆高
- 内存分配策略
-
不同JDK版本之间的intern()方法的区别–JDK6VS JDK6+
- 图示:
- 图示:
不仅能添加对象,常量池还能添加引用
六. GC-- 垃圾回收
6.1 垃圾回收–标记算法
-
对象被判定为垃圾的标准
- 没有被其他对象引用
-
判断对象是否为垃圾的算法:
- 引用计数算法
- 概述:
- 通过判断对象的引用数量来决定对象是否可以被回收
- 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
- 任何引用计数为0的对象实例可以被当做垃圾收集;
- 优缺点:
- 优点:执行效率高,程序执行受影响较小
- 缺点: 无法检测出循环引用的情况,导致内存泄漏;
- 概述:
- 可达性分析算法
- 概述:
- 通过判断对象的引用链是否可达来决定对象是否可以被回收;
- 如果不可达的则可以GC回收;
- 概述:
- 引用计数算法
-
可以作为GC Root的对象
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
6.2 谈谈你了解的垃圾回收算法
-
标记-清除算法(Mark and Sweep)
- 概述:
- 标记: 从根集合进行扫描,对存活的对象进行标记
- 清除: 对堆内存从头到尾进行线性遍历,回收不可达对象内存
- 优缺点:对象存活较多,清除操作较少 缺点:容易碎片化
- 概述:
-
标记-复制算法(Copying) 年轻代 要空出50%的空间用于复制,对象存活率低于10%
- 概述:
- 分为对象面和空闲面
- 对象在对象面上创建
- 存活的对象呗从对象面复制到空闲面
- 将对象面所有对象内存清除
- 优缺点:
- 解决碎片化问题
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景 (复制会消耗性能)
- 概述:
-
复制算法图示:
-
标记整理算法(Compacting) 用于老年代,对象存活率很高,不需要空出50%
- 概述:
- 标记: 从根集合进行扫描,对存活的对象进行标记
- 清除: 移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收;
- 优缺点:
- 避免内存的不连续性
- 不用设置两块内存互换
- 适用于存活率高的场景
- 概述:
-
图示:
-
分代收集算法(Generational Collector)
- 垃圾回收算法的组合拳
- 按照对象生命周期的不同划分区域以采用各不同的垃圾回收算法
- 目的: 提高JVM的回收效率
-
GC的分类:
-
Minor GC [用于年轻代中,采用的是复制算法]
- 年轻代: 尽可能快速地收集掉那些生命周期短的对象
- Eden区
- 两个Survivor区
- 年轻代: 尽可能快速地收集掉那些生命周期短的对象
-
图示:
Minor GC回收是直接回收Eden和from空间中的内容;Eden的空间较大,大多数的对象都死亡在这个里面
- Full GC [用于老年代中]
JDK8以前有年轻代,老年代和永久代,而现在只有年轻代和老年代,永久代的数据被存入元空间中;
6.3 对象如何晋升到老年代
- 经历一定Minor次数依然存活的对象
- Surbibor区中存放不下的对象
- 新生成的大对象(-XX:+PretenuerSizeThreshold)
6.4 常用的调优参数:
- XX:SurvivorRatio: Eden和Survivor的比值,默认8:1
- XX:NewRatio:老年代和年轻代内存大小的比例
- XX:MaxTenuringThreshold:对象从年轻代晋升到老生代经过GC次数的最大阀值;
6.5 老年代
-
概述:存放生命周期较长的对象
-
使用的GC算法:
- 标记-清理算法
- 标记-整理算法
-
GC:
- Full GC和Major GC
- Full GC比Minor GC慢,但执行频率低
-
触发Full GC的条件:
- 老年代空间不足
- 永久代空间不足[JDK8以前]
- CMS GC时出现promotion failed, concurrent mode failure
- Minor GC晋升到老年代的平均大小大于老年代的剩余空间
- 调用System.gc(); 只是提醒回收,GC不一定会立即执行
-
Stop-the-World
- JVM由于要执行GC而停止了应用程序的执行
- 任何一种GC算法中都会发生
- 多数GC优化通过减少Stop-the-world发生的时间来提高程序性能;
-
Safepoint: 【安全点】
- 分析过程中对象引用关系不会发生变化的点
- 产生Safepoint的地方:方法调用;循坏跳转;异常跳转等;
- 安全点数量得适中;
-
JVM的运行模式:
- Server
- Client
-
JVM Server模式与client模式启动的差别?
- Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升.
- 原因是:
- 当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.
- 所以通常用于做服务器的时候我们用服务端模式,如果你的电脑只是运行一下java程序,就客户端模式就可以了;服务器模式编译更彻底,然后垃圾回收优化更好,这当然吃的内存要多点相对于客户端模式。
- 怎么修改JVM的启动模式呢?
- 64位系统默认在 JAVA_HOME/jre/lib/amd64/jvm.cfg
- 32在目录JAVA_HOME/jre/lib/i386/jvm.cfg
-
垃圾收集器之间的关系,图示:
如果两个收集器之间有连线,说明它们可以搭配使用
6.6 常见的垃圾收集器—年轻代
-
Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程手机,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器;
-
ParNew 收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余的行为、特点和Serial收集器一样
- 单核执行效率不如Serial,在多核下执行才有优势 [电脑有几个核心,就可以开几个线程]
-
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 比起关注用户线程停顿时间,更关注系统的吞吐量
- 在多核下执行才有优势,Server模式下默认的年轻代收集器
6.7 常见的垃圾收集器—老年代
-
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- 单线程手机,进行垃圾收集时,必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器;
-
Parallel Old收集器(-XX:+UseParallelOldGC,标记整理算法)
- 多线程,吞吐量优先
-
CMS收集器(+XX:+UseConcMarkSweepGC,标记-清除算法) //可与其他收集器配合使用
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预处理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
6.8 既用于年轻代又用于老年代的垃圾收集器–G1收集器
- G1 收集器(-XX:+UseG1GC, 复制+标记-整理算法)
-
Garbage First收集器的特点:
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
-
与其他收集器的不同地方
- 将整个Java堆内存划分成多个大小相等的Region
- 年轻代和老年代不再物理隔离
-
JDK11 出的垃圾收集器还有: Epsilon GC 和 ZGC
6.9 GC相关的面试题
-
Java中的强引用,软引用,弱引用,虚引用有什么用?
-
强引用(Strong Reference)
- 最普遍的引用:Object obj=new Object();
- 抛出OutOfMemoryError终止程序也不会回收具有强引用的对象
- 通过将对象设置为null来弱化引用,使其被回收
-
软引用(Soft Reference)
- 对象出在有用但非必须的状态
- 只有当内存空间不足时,GC会回收该引用的对象的内存
- 可以用来实现高速缓存;
String str=new String("abc"); //强引用 SoftReference<String> softRef=new SoftReference<String>(str); //软引用
-
弱引用(Weak Reference)
- 非必须的对象,比软引用更弱一些
- GC时会被收回
- 被回收的概率不大,因为GC线程优先级比较低
- 适用于引用偶尔被使用且不影响垃圾手机的对象
-
虚引用(PhantomReference)
- 不会决定对象的生命周期
- 任何时候都可能被垃圾收集器回收
- 跟踪对象被垃圾收集器回收的活动,起哨兵作用
- 必须和引用队列ReferenceQueue联合使用
-
-
图示:
-
级别:
- 强引用> 软引用> 弱引用>虚引用
-
引用队列(ReferenceQueue)
-
概述:引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。
- 无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达
- 存储关联的且被GC的软引用,弱引用以及虚引用;
-
应用:通过引用队列可以了解JVM垃圾回收情况。
-
代码示例:
-
// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();
// 软引用
SoftReference<String> sr = new SoftReference<String>(new String("Soft"), rq);
// 弱引用
WeakReference<String> wr = new WeakReference<String>(new String("Weak"), rq);
// 幽灵引用
PhantomReference<String> pr = new PhantomReference<String>(new String("Phantom"), rq);
// 从引用队列中弹出一个对象引用
Reference<? extends String> ref = rq.poll();
彩蛋一. --找工作的最佳时机
-
金三银四时找工作的最佳时期吗?
- 优势:
- 供选择的公司多,机会多
- 劣势:
- 人才供应量旺盛
- 成为备胎的概率大增,获取offer时间较慢
- 若无明显竞争力,薪资涨幅相对不会很高
- 优势:
-
相对容易找到工作的时期
- 临近年末的时候:
- 大多数人不愿意在这个时候跳槽,导致远低于求
- 门槛变低,通过率变高
- 拿到offer的时间会相对变短
- 临近年末的时候:
-
年末跳槽的优劣势
- 优势:
- 薪水涨幅空间可能会大
- 能去心仪的公司的概率相对较大
- 劣势:
- 充当“救火英雄”的面大
- 优势:
彩蛋二. 找工作的渠道分析
-
同事朋友的内推 [最为推荐的一种]
- 简历直达团队,避免被非团队的人员筛选掉
- 知己知彼
- 避免简历被锁
- 注意: 碍于情面,错过后面的好公司
-
招聘网站的投递 [最不推荐的一种]
- 简历会被投递
- 太多的虚假信息
-
猎头招聘 [人性最真实的一面]
- 猎头对特定的公司、职位比较熟悉
- 找干练的猎头:是否做到精准推送,建议是否足够好
- 找多个猎头,多方面了解市场
- 做好恶战的准备
- 搭线练手公司,查漏补缺
-
总结:
- 利用猎头对公司进行分类:练手公司和心仪公司
- 竭尽全力挖掘目标公司的内部员工