字节面试准备

Java 基础

锁和锁的状态

Java中,锁分大致可分为4种. 偏向锁,轻量级锁,重量级锁

在java内存模型中,每个对象都有一个对象头。Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。
a
在这里插入图片描述

偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。一般锁的获取都是由同一个线程来操作的。在这种情况下如果不断地进行线程的阻塞或者释放是非常浪费的。所以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频率。

年轻代又分为EdenSurvivor区。Survivor区由FromSpaceToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1FromTo是主要为了解决内存碎片化。

堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据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算法
  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法

查找对象是否可用的方法,之前是引用计数法。但是因为会出现循环引用,所以改为了根可达性算法.

根可达性算法

在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、避免全堆扫描
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值