本文这个只是我自己整理的面试中多面到的一些JVM方面的知识点,就这样看它们都是孤立的,我是看《深入Java虚拟机》以及极客时间一个JVM系列的文章,才把这些概念给贯通的。
比如一个知识点:垃圾回收分Minor GC、Full GC。Minor发生在新生代,新生代分Eden、Survivor,而Survivor又分 From Survivor、To Survivor。新生代采用的是Copy机制的垃圾回收算法,通过空间换时间,它是怎么换的呢?To Survivor是一块空的内存,垃圾回收时,会把Eden、From Survivor中的可达的(区别堆外不可达)对象搬到 To Survivor中去,然后清除Eden、Form Survivor,整个过程接受后,交换From跟To,这期间有可能把对象从Eden、From搬去To的时候,To内存不足则需要借用老生代的内存,这就是所谓的内存担保。
Java内存结构
程序计数器:每个线程执行程序指令的行号
虚拟机栈:存放每个方法的栈帧,帧的入栈跟出栈就是方法执行的过程
本地方法栈:Native方法的栈
Java堆:保存Java对象的地方,细分为 Eden区, From Survivor空间, To Survivor空间(线程共享)
方法区:线程共享,存放已经被虚拟机加载进来的类信息,常量、静态变量,JIT编译后的数据代码。java的class文件首先进入的到方法区里面去。
运行时常量池
方法区的一部分。
四大引用
强引用:只要强引用还在,引用的对象永远不会被回收
软引用: 发生内存溢出之前会清除
弱引用:垃圾回收的时候会收走
虚引用:对象生命周期不会受到它影响,只是在被回收掉的时候,收到通知。
对象的存活
- 引用计数器
- 可达性算法(堆外的引用, GC Roots)
垃圾回收算法
- 标记清除法
- 复制算法
- 标记整理法
- 分代收集
安全点、Minor GC、Full GC、 STW、TLAB
safePoint、安全区域 。(不操作内存的写,不跟主存打交道) STW(stop the world)
eden, survior中调用 Minor GC,之所以没有用STW,添加了统计老生代到 新生代数据引用的卡表(脏位)
新生代(eden + from + to)TLAB(Thread Local Allocation Buffers)由于to Survior的空间不够,找老年代做内存担保。
当经历过几次 从from 到 to过程后依旧活着的对象就可以进入到 老生代中去了。
垃圾收集器
-
Serial (单线程,新生代,copy算法)
-
Parallel New(多线程,新生代,copy算法,时间优先)
-
Parallel Scavenge(多线程,新生代,copy算法,吞吐量优先)
-
Serial Old(单线程,老生代, 标记清除)
-
Parallel Old(多线程,老生代, 标记清除)
-
CMS:垃圾收集器跟应用并行(多线程,老生代, 标记清除)分两次清理,第一次清理的时候,工作线程跟垃圾线程并行,第二次STW。(Java 9 被废除)
-
G1 (新生代 + 老生代)
类加载过程
- 加载:验证Class文件是否按Class的结构走的,然后加载完成之后变成内存的结构
- 验证:不会等加载结束,其实交替进行的(文件格式验证、元数据验证、字节码验证、符号引用验证)
- 准备:分配内存,虚方法动态分配的方便发表。创建符号引用(所以跟验证是交叉的)
- 解析:(符号引用到直接引用:字段解析、类方法解析、接口方法解析)
- 初始化 (cinit<>)
即时编译(JIT优化)
热点方法,循环热点
C、C++属于静态编译,运行时候不会。java属于动态编译,虚方法动态分配,激进优化等使之更彻底,但是占用CPU。
优化方式:
-
内联优化(省去了方法的加载,压栈等。诡异的TR1图)
-
逃逸分析(对象没有被方法外引用的时候,直接到接近CPU的栈上分配存储空间。方法仅被一个线程调用到的时候,去掉同步锁。)
-
指令集(Compare And Sweep, Compare and Set),???
-
循环优化、向量优化
方法调用指令
- invokespecial(实例构造方法、私有方法、父类方法)
- invokestatic(静态方法,静态分配)
- invokevirtual(调用所有虚方法)
- invokeinterface(调用接口方法,运行时才)
- invokedynamic (方法句柄 MethodHandler, Lookup 不同于 反射,修改方法栈中的指令集)
反射的消耗
- Method.invoke(Class, Object[])方法中只能够是 Object类型,没有基本数据类型。自动装箱、拆箱损耗。
- Method.invoke没有办法内联了。
- 没有办法逃逸分析。
- 要进行方法找寻,还要往上查找父类中的所有方法。存储记录
- 本地实现、动态实现(超过一定的次数,动态实现会快一些)
Volatile
- 修改内存可见性,没有用线程中的寄存器作为缓存
- 禁止重排序,设置内存屏障(读读,读写,写读, 写写),添加指令 lock … 空指令,让lock指令后的指令没法重排到前面去(单线程里面不管 数据相关性、happens—before)
线程、锁
-
偏向锁 (当第一次进入到同步代码块的时候,不上锁,修改对象头中的Mark word,线程的ID等信息,通过CAS指令,全程只有一个线程,所以记录它偏向它,偏向锁)
-
轻量级锁 (只有一个线程,把线程上锁记录加入到 对象头中去,采用CAS,不再是第一个线程,而是另外的线程不同时间段执行同步代码块,有偏向锁到 轻量级锁)
-
重量级锁(一个线程在执行,另一个线程来竞争。轻量级锁 锁膨胀为重量级锁)等待的线程没有直接去wait,而是运行空指令处于就绪状态,等待运行中的线程释放锁,然后自己拿取,这样的叫自旋锁。为什么要添加这个自选状态呢?
当线程一旦进入到wait后,就统一交由CPU进行管理 + 调度,转为内核态,到时候唤醒运行变为用户态,这个过程的切换有很多资源的 消耗(内存的拷贝复制等),在这里运行空指令消耗可能比这个切换更加省CPU。但是对于其他锁而言就失去公平性了。
这样相比而言,协程有自己管理、调度部区分内核态、用户态,资源开支比较小,是现代语言(Kotlin等)中常用的技术。(抢占式调度、协同式)
synchronized(monitor, monitorenter, monitorexit)
ReentrantLock(重入锁), NewCondtion