字节面试准备
Java 基础
锁和锁的状态
Java中,锁分大致可分为4种. 偏向锁,轻量级锁,重量级锁
在java内存模型中,每个对象都有一个对象头。Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。
偏向锁
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。一般锁的获取都是由同一个线程来操作的。在这种情况下如果不断地进行线程的阻塞或者释放是非常浪费的。所以Java语言就引入了偏向锁的概念。在同一个线程多次尝试获取锁的时候,会直接将锁分给当前线程,不进行加锁过程。
偏向锁加锁具体过程:
在线程尝试获取锁的时候,会查看当前锁对象头的偏向锁信息。如果是偏向锁并且线程为当前线程,则会直接让当前线程获取锁权限,不进行加锁。
如果为偏向锁状态但是是别的线程,则会进行锁升级,升级为自旋锁
如果为无锁状态,则会设置为偏向锁,并且将偏向线程id记录为当前线程id,以便下一次该线程访问直接进入
偏向锁解锁
偏向锁使用了一种等到竞争出现才释放锁的机制。所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
自旋锁
自旋锁是一种锁的实现方式
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。
所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。
自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。
轻量级锁
加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
解锁
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。
重量级锁
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
HashMap 扩容
HashMap在插入的元素数量过多的情况下,会进行hash扩容。
这个插入的元素数量过多的定义为:数组长度系数。系数默认为0.75;
扩容会将table长度2,并且重新计算内部元素所在table的下标。
JDK1.7之前
扩容采用头插法,新table中链表的顺序和旧列表中是相反的。链表在多线程下可能导致环状链表的问题。
JDK 1.8 之后
采用尾插法。
确定元素所在下标的方法为:hash&(length-1);由于length是二的N次幂,则length-1在二进制下有效位全为1,方便进行位运算
扩容后数组长度×2,在二进制下表示就是多了一位,length-1自然也是多一位.那么hash&(length-1)就会有两种结果,跟之前一样,或者是之前的二倍。方便定位元素所在table的下标
JVM
JVM架构图
堆,栈,方法区,本地方法区->运行时常量池,程序计数器,
-
方法区:所有类级别数据将被存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享的资源。
-
堆:存放对象的地方。所有线程共享一个堆内存。会出现并发问题
-
栈:对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:
a 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
b 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
c 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。如上是JVM三大核心区域
JVM内存模型
JVM内存划分为堆内存
和非堆内存
,堆内存分为年轻代
(Young Generation)、老年代
(Old Generation),非堆内存
就一个永久代
(Permanent Generation)在1.7之后将其变为了元空间。把对象根据存活概率进行分类,采用分代回收机制,从而减少扫描垃圾时间及GC频率。
年轻代又分为Eden
和Survivor
区。Survivor
区由FromSpace
和ToSpace
组成。Eden
区占大容量,Survivor
两个区占小容量,默认比例是8:1:1。From
和To
是主要为了解决内存碎片化。
堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
非堆内存用途:永久代
,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。有了元空间就不再会出现永久代OOM问题了!
元空间
注意有两个参数:
MetaspaceSize:初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize: 限制元空间大小上限,防止异常占用过多物理内存
GC
JVM GC时候核心参数
- -XX:NewRatio:是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3
- –XX:SurvivorRatio:
Eden区
占整个堆内存的百分比,默认为8,即伊甸区占80%; - –XX:NewSize: 新生代所占用大小,可以跟
-XX:NewRatio
同时设置 - –XX:MaxNewSize : 新生代最大大小.可以跟
-XX:NewRatio
同时设置
当
-XX:NewRatio
与–XX:NewSize
以及–XX:MaxNewSize
同时设置时,会尽量满足比例,然后不会小于–XX:NewSize
同时不会大于–XX:MaxNewSize
GC 日志分析
(2)JVM的GC日志Full GC日志每个字段彻底详解
[Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]
[Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]
[Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC会导致新生代Minor GC产生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC后,并没有降低内存占用,因为写的程序不断循环一直有引用)(7168K) (老年代总容量)] 8114K(GC前占用整个Heap空间大小)->7555K (GC后占用整个Heap空间大小) (9216K) (整个Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,类相关的一些信息) 2613K->2613K(1056768K) (GC前后基本没变,空间很大)], 0.1022588 secs(GC的耗时,秒为单位)] [Times: user=0.56 sys=0.02, real=0.10 secs](用户空间耗时,内核空间耗时,真正的耗时时间)
三种基本的GC算法
- 标记清除算法
- 复制算法
- 标记整理算法
查找对象是否可用的方法,之前是引用计数法。但是因为会出现循环引用,所以改为了根可达性算法.
根可达性算法
在Java语言中,可以作为GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
垃圾回收器
新生代收集器 | 线程 | 算法 | 优点 | 缺点 |
---|---|---|---|---|
Parallel Scavenge | 多线程(并行) | 复制算法 | 吞吐量优先 适用在后台运行不需要太多交互的任务 有GC自适应的调节策略开关 | 无法与CMS收集器配合使用 |
ParNew | 多线程(并行) | 复制算法 | 响应优先 适用在多CPU环境Server模式一般采用ParNew和CMS组合 多CPU和多Core的环境中高效 | Stop The World |
Serial收集器 | 单线程(串行) | 复制算法 | 响应优先 适用在单CPU环境Client模式下的默认的新生代收集器 无线程交互的开销,简单而高效(与其他收集器的单线程相比) | Stop The World |
老年代收集器 | 线程 | 算法 | 优点 | 缺点 |
---|---|---|---|---|
Serial Old收集器 | 单线程(串行) | “标记-整理”(Mark-Compact)算法 | 响应优先 单CPU环境下Client模式,CMS的后备预案。 无线程交互的开销,简单而高效(与其他收集器的单线程相比) | Stop The World |
Parallel Old收集器 | 多线程(并行) | 标记-整理 | 响应优先 吞吐量优先 适用在后台运行不需要太多交互的任务 有GC自适应的调节策略开关 | - |
CMS收集器 | 多线程(并发) | 标记-清除 | 响应优先 集中在互联网站或B/S系统服务、端上的应用。 并发收集、低停顿 | 1、对CPU资源非常敏感:收集会占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低 2、无法处理浮动垃圾 3、清理阶段新垃圾只能下次回收 4、标记-清除算法导致的空间碎片 |
新/老年代收集器 | 线程 | 算法 | 优点 |
---|---|---|---|
G1 | 多线程(并发) | 标记-整理+复制 | 1、面向服务端应用的垃圾收集器 2、分代回收 3、可预测的停顿 这是G1相对CMS的一大优势 4、内存布局变化:将整个Java堆划分为多个大小相等的独立区域(Region) 5、避免全堆扫描 |