JAVA中的乐观锁与CAS算法
对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。
到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;
如果没有被修改,那就执行修改操作,返回修改成功。
乐观锁一般都采用
Compare And Swap
(
CAS
)算法进行实现。顾名思义,该算法涉及到了两个操作, 比较(Compare
)和交换(
Swap
)。
CAS
算法的思路如下:
1.
该算法认为不同线程对变量的操作时产生竞争的情况比较少。
2.
该算法的核心是对当前读取变量值
E
和内存中的变量旧值
V
进行比较。
3.
如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值
N
。
4.
如果不等,就认为在读取值
E
到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
简述java创建对象的过程
1.
检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、 解析和初始化,如果没有就先执行类加载。
2.
通过检查通过后虚拟机将为新生对象分配内存。
3.
完成内存分配后虚拟机将成员变量设为零值
4.
设置对象头,包括哈希码、
GC
信息、锁信息、对象所属类的类元信息等。
5.
执行
init
方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址 赋值给引用变量。
简述JVM给对象分配内存的策略
1.
指针碰撞: 这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。
2.
空闲列表: 对于
Java
堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
java对象内存分配是如何保证线程安全的
1.
对分配内存空间采用
CAS
机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
2.
每个线程在
Java
堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块
"
私
有
"
内存中分配。一般采用这种策略。
简述对象的内存布局
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
对象头主要包含两部分数据:
MarkWord
、类型指针。
MarkWord
用于存储哈希码(
HashCode
)、
GC 分代年龄、锁状态标志位、线程持有的锁、偏向线程ID
等信息。
类型指针即对象指向他的类元数据指针,如果对象是一个
Java
数组,会有一块用于记录数组长度的数
据, 实例数据存储代码中所定义的各种类型的字段信息。
对齐填充起占位作用。
HotSpot
虚拟机要求对象的起始地址必须是
8
的整数倍,因此需要对齐填充。
如何判断对象是否是垃圾
引用计数法:设置引用计数器,对象被引用计数器加
1
,引用失效时计数器减
1
,如果计数器为
0
则被 标记为垃圾。会存在对象间循环引用的问题,一般不使用这种方法。
可达性分析:通过
GC Roots
的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,如果 某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots
的对象包括虚拟机栈和本地方法栈中引用 的对象、类静态属性引用的对象、常量引用的对象。
简述java的引用类型
强引用: 被强引用关联的对象不会被回收。一般采用
new
方法创建强引用。
软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用
SoftReference
类来创建 软引用。
弱引用:垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用
WeakReference
类来创建弱引用。
虚引用: 无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必 须与引用队列联合使用。
简述标记清除算法、标记整理算法和标记复制算法
标记清除算法:先标记需清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。
标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存
标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。
简述分代收集算法
根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代,对这两块采用不同的算法。
新生代使用:标记复制算法
老年代使用:标记清除或者标记整理算法
简述Serial垃圾收集器
单线程串行收集器。垃圾回收的时候,必须暂停其他所有线程。新生代使用标记复制算法,老年代使用 标记整理算法。简单高效。
简述ParNew垃圾收集器
可以看作
Serial
垃圾收集器的多线程版本,新生代使用标记复制算法,老年代使用标记整理算法。
简述Parallel Scavenge垃圾收集器
注重吞吐量,即
cpu
运行代码时间
/cpu
耗时总时间(
cpu
运行代码时间
+
垃圾回收时间)。新生代使用标 记复制算法,老年代使用标记整理算法。
简述CMS垃圾收集器
注重最短时间停顿。
CMS
垃圾收集器为最早提出的并发收集器,垃圾收集线程与用户线程同时工作。采 用标记清除算法。该收集器分为初始标记、并发标记、并发预清理、并发清除、并发重置这么几个步骤:
初始标记:暂停其他线程
(stop the world)
,标记与
GC roots
直接关联的对象。并发标记:可达性分析过 程(
程序不会停顿
)
。
并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象,重新标记,暂停虚拟机(
stop the world)扫描
CMS
堆中剩余对象。
并发清除:清理垃圾对象,
(
程序不会停顿
)
。
并发重置,重置
CMS
收集器的数据结构。
简述G1垃圾收集器
和之前收集器不同,该垃圾收集器把堆划分成多个大小相等的独立区域(
Region
),新生代和老年代不 再物理隔离。通过引入 Region
的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。
初始标记:标记与
GC roots
直接关联的对象。
并发标记:可达性分析。
最终标记,对并发标记过程中,用户线程修改的对象再次标记一下。
筛选回收:对各个
Region
的回收价值和成本进行排序,然后根据用户所期望的
GC
停顿时间制定回收计 划并回收。
简述Minor GC
Minor GC
指发生在新生代的垃圾收集,因为
Java
对象大多存活时间短,所以
Minor GC
非常频繁,一 般回收速度也比较快。
简述Full GC
Full GC
是清理整个堆空间
—
包括年轻代和永久代。调用
System.gc(),
老年代空间不足,空间分配担保失
败,永生代空间不足会产生
full gc
。
常见内存分配策略
大多数情况下对象在新生代
Eden
区分配,当
Eden
没有足够空间时将发起一次
Minor GC
。
大对象需要大量连续内存空间,直接进入老年代区分配。
如果经历过第一次
Minor GC
仍然存活且能被
Survivor
容纳,该对象就会被移动到
Survivor
中并将年龄
设置为
1
,并且每熬过一次
Minor GC
年龄就加
1
,当增加到一定程度(默认
15
)就会被晋升到老年 代。
如果在
Survivor
中相同年龄所有对象大小的总和大于
Survivor
的一半,年龄不小于该年龄的对象就可 以直接进入老年代。
空间分配担保。
MinorGC
前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果 满足则说明这次 Minor GC
确定安全。如果不,
JVM
会查看
HandlePromotionFailure
参数是否允许担保
失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满 足将Minor GC
,否则改成一次
FullGC
。
简述JVM类加载过程
加载:
1.
通过全类名获取类的二进制字节流
.
2.
将类的静态存储结构转化为方法区的运行时数据结构。
3.
在内存中生成类的
Class
对象,作为方法区数据的入口。
验证:对文件格式,元数据,字节码,符号引用等验证正确性。
准备:在方法区内为类变量分配内存并设置为
0
值。
解析:将符号引用转化为直接引用。
初始化:执行类构造器
clinit
方法,真正初始化。
简述JVM中的类加载器
BootstrapClassLoader
启动类加载器:加载
/lib
下的
jar
包和类。
C++
编写。
ExtensionClassLoader
扩展类加载器:
/lib/ext
目录下的
jar
包和类。
java
编写。
AppClassLoader
应用类加载器,加载当前
classPath
下的
jar
包和类。
java
编写。
简述双亲委派机制
一个类加载器收到类加载请求之后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果
没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法
完成时才尝试自己加载。
加载类顺序:
BootstrapClassLoader->ExtensionClassLoader->AppClassLoader->CustomClassLoader
检查类是否加载顺序:
CustomClassLoader->AppClassLoader->ExtensionClassLoader->BootstrapClassLoader
双亲委派机制的优
1.
避免类的重复加载。相同的类被不同的类加载器加载会产生不同的类,双亲委派保证了
java
程序的
稳定运行。
2.
保证核心
API
不被修改。
如何破坏双亲委派机制
重载
loadClass()
方法,即自定义类加载器。
如何构建自定义类加载器
1.
新建自定义类继承自
java.lang.ClassLoader
2.
重写
findClass
、
loadClass
、
defineClass
方法
JVM常见调优参数
-Xms
初始堆大小
-Xmx
最大堆大小
-XX:NewSize
年轻代大小
-XX:MaxNewSize
年轻代最大值
-XX:PermSize
永生代初始值
-XX:MaxPermSize
永生代最大值
-XX:NewRatio
新生代与老年代的比例
小牛和同学开发的八股文网站
http://interviewtop.top
持续更新
BAT
面试题。
什么是内核态和用户态?
为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。
内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统
内所有的存储空间。
用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。
用户程序运行在用户态
,
操作系统内核运行在内核态。
如何实现内核态和用户态的切换?
处理器从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。
1.
系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种软中断。
2.
异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。
3.
外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。
什么是虚拟地址,什么是物理地址?
地址空间是一个非负整数地址的有序集合。
在一个带虚拟内存的系统中,
CPU
从一个有
N=pow(2,n)
个地址的地址空间中生成虚拟地址,这个地址
空间称为虚拟地址空间(
virtual address space
)
,
现代系统通常支持
32
位或者
64
位虚拟地址空间。
一个系统还有一个物理地址空间(
physical address space
),对应于系统中物理内存的
M
个字节。
地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。
一旦认识到了这种区别,那么我们就可以将其推广,允许每个数据对象有多个独立的地址,其中每个地
址都选自一个不同的地址空间。这就是虚拟内存的基本思想。
主存中的每字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。
什么是虚拟内存?
为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存
(VM)
。虚
拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个
大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
1.
它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要
在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
2.
它为每个进程提供了一致的地址空间,从而简化了内存管理。
3.
它保护了每个进程的地址空间不被其他进程破坏。
为什么要引入虚拟内存?
1.
虚拟内存作为缓存的工具
虚拟内存被组织为一个由存放在磁盘上的
N
个连续的字节大小的单元组成的数组。
虚拟内存利用
DRAM
缓存来自通常更大的虚拟地址空间的页面
2.
虚拟内存作为内存管理的工具。
操作系统为每个进程提供了一个独立的页表,也就是独立的虚拟地址空间。多个虚拟页面可以映射到同一个物理页面上。
简化链接:
独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据
实际存放在物理内存的何处。
例如:一个给定的
linux
系统上的每个进程都是用类似的内存格式,对于
64
为地址空间,
代码段总是从虚拟地址)
0x400000
开始,数据段,代码段,栈,堆等等。
简化加载:
虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件
中
.text
和
.data
节加载到一个新创建的进程中,
Linux
加载器为代码和数据段分配虚拟页
VP
,把
他们
标记为无效(未被缓存)
,将页表条目指向目标文件的起始位置。
加载器从不在磁盘到内存实际复制任何数据,在每个页初次被引用时,虚拟内存系统会按
照需要自动的调入数据页。
简化共享:
独立地址空间为
OS
提供了一个管理用户进程和操作系统自身之间共享的一致机
制。
一般:每个进程有各自私有的代码,数据,堆栈,是不和其他进程共享的,
这样
OS
创建
页表,将虚拟页映射到不连续的物理页面。
某些情况下,需要进程来共享代码和数据。例如每个进程调用相同的操作系统内核代码,
或者
C
标准库函数。
OS
会把不同进程中适当的虚拟页面映射到相同的物理页面。
简化内存分配:
虚拟内存向用户提供一个简单的分配额外内存的机制。当一个运行在用户进程
中的程序要求额外的堆空间时(如
malloc
),
OS
分配一个适当
k
大小个连续的虚拟内存页面,
并且将他们映射到物理内存中任意位置的
k
个任意物理页面,
因此操作系统没有必要分配
k
个连
续的物理内存页面,页面可以随机的分散在物理内存中
。
虚拟内存作为内存保护的工具。不应该允许一个用户进程修改它的只读段,也不允许它修改任何内
核代码和数据结构,不允许读写其他进程的私有内存,不允许修改任何与其他进程共享的虚拟页
面。每次
CPU
生成一个地址时,
MMU
会读一个
PTE
,通过在
PTE
上添加一些额外的许可位来控制对
一个虚拟页面内容的访问十分简单。
常见的页面置换算法
当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,
替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:
先进先出
(FIFO)
算法:
思路:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。
实现:按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。
特点:实现简单;性能较差,调出的页面可能是经常访问的
最近最少使用(
LRU
)算法
:
思路: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页
面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
实现:缺页时,计算内存中每个逻辑页面的上一次访问时间,选择上一次使用到当前时间最长
的页面
特点:可能达到最优的效果,维护这样的访问链表开销比较大
当前最常采用的就是
LRU
算法。
最不常用算法(
Least Frequently Used, LFU
)
思路:缺页时,置换访问次数最少的页面
实现:每个页面设置一个访问计数,访问页面时,访问计数加
1
,缺页时,置换计数最小的页
面
特点:算法开销大,开始时频繁使用,但以后不使用的页面很难置换
请说一下什么是写时复制?
如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一
个指向这个资源的指针就可以了。只要没有进程要去修改自己的
“
副本
”
,就存在着这样的幻觉:每
个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源
“
副
本
”
,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。
这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就
是名称的由来:在写入时进行复制。
写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处
就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。
在使用虚拟内存的情况下,写时复制(
Copy-On-Write
)是以页为基础进行的。所以,只要进程不
修改它全部的地址空间,那么就不必复制整个地址空间。在
fork()
调用结束后,父进程和子进程都相
信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的
父进程或子进程共享。
简述最小生成树和其对应的算法
对于有
n
个结点的原图,生成原图的极小连通子图,其包含原图中的所有
n
个结点,并且有保持图连通
的最少的边。
普里姆算法:取图中任意一个顶点
v
作为生成树的根,之后往生成树上添加新的顶点
w
。在添加的顶点
w
和已经在生成树上的顶点
v
之间必定存在一条边,并且该边的权值在所有连通顶点
v
和
w
之间的边中
取值最小。之后继续往生成树上添加顶点,直至生成树上含有
n-1
个顶点为止。
克鲁斯卡尔算法:先构造一个只含
n
个顶点的子图
SG
,然后从权值最小的边开始,若它的添加不使
SG
中产生回路,则在
SG
上加上这条边,如此重复,直至加上
n-1
条边为止。
简述最短路径算法
Dijkstral
算法为求解一个点到其余各点最小路径的方法,其算法为:
假设我们求解的是顶点
v
到其余各个点的最短距离。
n
次循环至
n
个顶点全部遍历:
1.
从权值数组中找到权值最小的,标记该边端点
k
2.
打印该路径及权值
3.
如果存在经过顶点
k
到顶点
i
的边比
v->i
的权值小
4.
更新权值数组及对应路径
简述堆
堆是一种完全二叉树形式,其可分为最大值堆和最小值堆。
最大值堆:子节点均小于父节点,根节点是树中最大的节点。
最小值堆:子节点均大于父节点,根节点是树中最小的节点。
简述set
Set
是一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。
小牛和同学开发的八股文网站
http://interviewtop.top
持续更新
BAT
面试题。
MySQL是如何保证主备一致的?
MySQL
通过
binlog
(二进制日志)实现主备一致。
binlog
记录了所有修改了数据库或可能修改数据库的 语句,而不会记录select
、
show
这种不会修改数据库的语句。在备份的过程中,主库
A
会有一个专门的 线程将主库A
的
binlog
发送给 备库B
进行备份。
其中
binlog
有三种记录格式:
1. statement:
记录对数据库进行修改的语句本身,有可能会记录一些额外的相关信息。优点是
binlog
日 志量少,IO
压力小,性能较高。缺点是由于记录的信息相对较少,在不同库执行时由于上下文的环 境不同可能导致主备不一致。
2. row:
记录对数据库做出修改的语句所影响到的数据行以及对这些行的修改。比如当修改涉及多行数 据,会把涉及的每行数据都记录到binlog
。
优点是能够完全的还原或者复制日志被记录时的操作。
缺点是日志量占用空间较大,
IO
压力大,性能消耗较大。
3. mixed:
混合使用上述两种模式,一般的语句使用
statment
方式进行保存,如果遇到一些特殊的函
数,则使用
row
模式进行记录。
MySQL
自己会判断这条
SQL
语句是否可能引起主备不一致,如果有
可能,就用
row
格式,
否则就用
statement
格式。但是在生产环境中,一般会使用
row
模式。
redo log与binlog的区别?
1. redo log
是
InnoDB
引擎特有的,只记录该引擎中表的修改记录。
binlog
是
MySQL
的
Server
层实现
的,会记录所有引擎对数据库的修改。
2. redo log
是物理日志,记录的是在具体某个数据页上做了什么修改;
binlog
是逻辑日志,记录的是这 个语句的原始逻辑。
3. redo log
是循环写的,空间固定会用完;
binlog
是可以追加写入的,
binlog
文件写到一定大小后会切 换到下一个,并不会覆盖以前的日志。
crash-safe能力是什么?
InnoDB
通过
redo log
保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为
crashsafe。
WAL技术是什么?
WAL
的全称是
Write-Ahead Logging
,它的关键点就是先写日志,再写磁盘。事务在提交写入磁盘前, 会先写到redo log
里面去。如果直接写入磁盘涉及磁盘的随机
I/O
访问,涉及磁盘随机
I/O
访问是非常消耗 时间的一个过程,相比之下先写入redo log
,后面再找合适的时机批量刷盘能提升性能。
两阶段提交是什么?
为了保证
binlog
和
redo log
两份日志的逻辑一致,最终保证恢复到主备数据库的数据是一致的,采用两阶 段提交的机制。
1.
执行器调用存储引擎接口,存储引擎将修改更新到内存中后,将修改操作记录
redo log
中,此时
redo log
处于
prepare
状态。
2.
存储引擎告知执行器执行完毕,执行器生成这个操作对应的
binlog
,并把
binlog
写入磁盘。
3.
执行器调用引擎的提交事务接口,引擎把刚刚写入的
redo log
改成提交
commit
状态,更新完成。
只靠binlog可以支持数据库崩溃恢复吗?
不可以。
历史原因:
1. InnoDB
在作为
MySQL
的插件加入
MySQL
引擎家族之前,就已经是一个提供了崩溃恢复和事务支持 的引擎了。InnoDB
接入了
MySQL
后,发现既然
binlog
没有崩溃恢复的能力,那引入
InnoDB
原的
redo log
来保证崩溃恢复能力。
实现原因:
2. binlog
没有记录数据页修改的详细信息,不具备恢复数据页的能力。
binlog
记录着数据行的增删改, 但是不记录事务对数据页的改动,这样细致的改动只记录在redo log
中。当一个事务做增删改时, 其实涉及到的数据页改动非常细致和复杂,包括行的字段改动以及行头部以及数据页头部的改动, 甚至b+tree
会因为插入一行而发生若干次页面分裂,那么事务也会把所有这些改动记录下来到
redo log中。因为数据库系统进程
crash
时刻,磁盘上面页面镜像可以非常混乱,其中有些页面含有一些 正在运行着的事务的改动,而一些已提交的事务的改动并没有刷上磁盘。事务恢复过程可以理解为 是要把没有提交的事务的页面改动都去掉,并把已经提交的事务的页面改动都加上去这样一个过 程。这些信息,都是binlog
中没有记录的,只记录在了存储引擎的
redo log
中。
3.
操作写入
binlog
可细分为
write
和
fsync
两个过程,
write
指的就是指把日志写入到文件系统的
page
cache
,并没有把数据持久化到磁盘
,fsync
才是将数据持久化到磁盘的操作。通过参数设置
sync_binlog
为
0
的时候,表示每次提交事务都只
write
,不
fsync
。此时数据库崩溃可能导致部分提交 的事务以及binlog
日志由于没有持久化而丢失。