< < < 知识记录 > > >
- JVM的内存模型(Java8)
- GC垃圾回收
- JVM调优常用参数
- java几种常见的OOM
- JMM(java内存模型)的特点
- volatile关键字的作用
- 什么是CAS?CAS有什么缺点?
- LockSupport
- 什么是AQS
- 常用线程池创建的几种方式
- 线程池创建的几个参数
- java的一些锁相关说明
- synchronized的作用?synchronized同步方法和synchronized同步代码块原理
- ReentrantLock和synchronized有什么区别?用新的Lock有什么好处?
- CountDownLatch、CyclicBarrier、Semaphore?
- 二叉查找树和平衡二叉树有什么关系
- 强平衡二叉树(AVL树)和弱平衡二叉树(红黑树)有什么区别
- B树与B+树的区别
- B+树相比于B树的查询优势
- HashMap
JVM的内存模型(Java8)
一:类加载子系统
1:加载:将.class二进制字节流文件从磁盘读取到内存中(通过文件的全限定名);
1). 启动类加载器(Bootstrap ClassLoader):JRE的核心类库,例如rt.jar包中java和sun包下的类,都将使用此类加载器进行加载
;
2). 扩展类加载器(Extension ClassLoader):负责加载JRE扩展目录ext中的jar包
;
3). 系统类加载(Application ClassLoader):负责加载ClassPath路径下的类包,就是平时我们自己开发时编写的类文件
;
4). 自定义类加载器:因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader,而且我们可以根据自己的需求,对class文件进行加密和解密,4.1:新建一个类继承自java.lang.ClassLoader,重写它的findClass方法;4.2:将class字节码数组转换为Class类的实例;4.3:调用loadClass方法即可
;
双亲委派机制:避免类的重复加载,类加载器收到加载请求时,不会立即加载而是先将加载请求委托给上一级的类加载器,直到启动类加载器,能加载启动类加载器直接加载,不能加载到扩展类加载器,扩展类加载器能加载直接加载,以此向下类推;
2: 链接
- 验证:验证字节码文件的正确性,主要包括四种验证;
文件格式验证,源数据验证,字节码验证,符号引用验证
- 准备:给类的静态变量分配内存,并赋予默认值;
- 解析:将常量池内的符号引用转换为直接引用的过程。
3:初始化:为静态变量赋予正确的初始值,此阶段才是程序员编写的程序变量赋予真正的初始值,执行静态代码块
二:运行时数据区
堆:线程之间共享区域,主要用来存放类的对象实例信息,堆分为老年代(Old Space)和年轻代(Young Space),老年代主要存放应用程序中生命周期长的存活对象或者Young Space存放不下的大对象,年轻代又分为Eden、S0和S1,Eden主要存放新生的对象;S0和S1是两个大小相同的内存区域,存放每次垃圾回收后Eden存活的对象,作为对象从Eden过渡到Old Space的缓冲地带;
方法区:线程之间共享区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
虚拟机栈:线程私有,描述的是 Java 方法执行的内存模型,每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程;
本地方法栈:区别于 Java 虚拟机栈的是,本地方法栈服务于Native本地方法;
程序计数器:线程私有,节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成;
三:执行引擎
解释器、即时编译器、垃圾回收器;
GC垃圾回收
四大算法:
- 引用计数:用于年轻代,给每个对象一个计数器,计数减到0的时候,回收,无法处理循环引用问题;
- 复制清除:用于年轻代,把空间分成两块,每次只对其中一块进行 GC,当这块内存使用完时,就将还存活的对象复制到另一块上面,例:Eden–>S0、S0–>S1采用了复制的算法,不适合用于存活对象多的情况,因为那样需要复制的对象很多;
- 标记清除:用于老年代,遍历所有的GC Roots,并将从GC Roots可达的对象标记设置为存活对象,然后遍历堆中的所有对象,将没有被标记可达的对象清除,大量的内存遍历工作,所以执行性能较低;
- 标记整理:用于老年代,在进行完标记清除之后,对内存空间进行压缩整理,节省内存空间,解决了标记清除算法内存不连续的问题,效率不高,移动对象需要耗费更多时间;
垃圾回收器:
- 串行垃圾回收器(Serial):单线程环境只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适用服务器环境;
- 并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时的用户线程也是暂停的,适用于科学计算/大数据处理;
- 并发垃圾回收器(CMS):用户线程和垃圾回收线程同时执行,低停顿用户线程,优点:并发收集低停顿;缺点:并发执行,对CPU资源压力大;采用标记清除会导致大量碎片;因为垃圾碎片占用内存,可以配置-XX:CMSFullGCsBeForeCompaction多少次垃圾回收之后进行Full GC(默认0,每次垃圾回收之后都进行内存整理);
- G1垃圾回收器(G1):将堆内存分割成不同的区域然后并发的对其进行垃圾回收,特点:与CMS一样与应用程序并发执行;整理空间更快,不会产生很多内存碎片;更短的停顿时间,停顿时间添加了预测机制,用户可以指定期望的停顿时间,region区域化垃圾回收器,避免全内存扫描,只需要按照区域进行扫描;
JVM调优常用参数
- -Xms:初始内存大小,一般为物理内存的1/64;
- Xmx:最大分配内存,一般为物理内存的1/4;
- -Xss:设置单个线程栈的大小,一般默认;
- -XX:MetaspceSize :设置元空间大小;
- -XX:+PrintGCDetails:开启打印GC和FullGC垃圾回收信息;
- -XX:SuivivorRatio:新生代与Suivivor比例,默认值为8,则Eden:S0:S1=8:1:1;
- -XX:NewRatio:年轻代与老年的比例,默认值为2,年轻代站1/3,老年代占2/3;
- -XX:MaxTenuringThreshold :设置垃圾回收的年龄,也就是从年轻到经过多少次GC,才到老年代;
java几种常见的OOM
- StackOverflowError:栈内存溢出,递归方法的调用可出现此错误;
- OutOfMemoryError:java heap space:堆内存溢出,new了很多的对象或者new了大的对象可出现此错误;
- OutOfMemoryError:GC overhead limit exceeded:GC时间太长引发的异常超过98%的时间用来做GC并且回收不到2%的堆内存,不停的往常量池里添加可出现此错误;
- OutOfMemoryError:Direct buffer memory:直接内存溢出,对象没有分配到JVM的堆内存,而是分配到了本地内存中,NIO的buffer操作分配到堆外内存;
示例
:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
- OutOfMemoryError:Metaspace:元空间内存溢出;
JMM(java内存模型)的特点
- 可见性:一个线程更新内存的值,立即通知其他线程,更新后的值对其他线程可见;
- 原子性:所有操作要么全部成功,要么全部失败,这些操作是不可拆分,操作过程中不可被中断;
- 有序性:对于单线程代码执行时,我们认为代码是从上到下执行的,但是在多线程环境下,程序的执行可能是乱序的;
volatile关键字的作用
java提供的轻量级同步机制;
- 保证可见性;
- 不保证原子性(java.util.concurrent.atomic包下的类可保证原子性);
- 禁止指令重排。
下面代码:reader方法依赖flag的值,我们希望程序是顺序执行的正常单线程程序没问题,但是在多线程环境下,writer方法可能先执行flag=true,这样reader方法获取的值就不确定了,volatile的一个特点就是禁止指令重排,固定必须先执行a=1然后flag=true
public class OrderSortDemo {
private int a = 0;
private boolean flag = false;
/**
* 写方法
*/
public void writer(){
a = 1;
flag = true;
}
/**
* 读方法,执行依赖flag的值
*/
public void reader(){
if(flag){
int i = a + 1;
}
}
}
双重检索DCL单例实现使用volatile代码
:
public class SingleInstance {
private static volatile SingleInstance instance = null;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (instance == null) {
synchronized (SingleInstance.class) {
if (instance == null) {
instance = new SingleInstance();
}
}
}
return instance;
}
}
什么是CAS?CAS有什么缺点?
方法名为compareAndSet(比较并赋值),主要就是自旋锁与Unsafe类,内存值V,期望值A,待修改值B,如果V与A的值相同,将内存值V更新为B,否则一直比较下去,直到成功(do…while…)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
缺点:
- 循环时间过长,开销大;
- 只能保证一个共享变量的原子操作,也就是compareAndSet方法this参数;
- ABA问题(
解决:new AtomicReference<V>(V initialValue):参数为一个初始值、new AtomicStampedReference<V>(V initialRef, int initialStamp):参数为一个初始值和一个时间戳版本号(类似于数据库的乐观锁)
);
LockSupport
线程等待唤醒机制,两个方法阻塞线程park()和解除阻塞线程unpark(),有一个permit许可证,只能有0和1两个值,park为0,unpark为1,有凭证消耗掉凭证然后正常退出,没有凭证则阻塞,凭证只能为一个累加无效,park和unpark成对出现;
阻塞线程和解除阻塞线程的方式:
- Object的wait和notify方法;
- Lock的Condition的await和singnal;
- LockSupport的park和unpark;
什么是AQS
AbstractQueuedSynchronizer(抽象队列同步器):是构建锁和其他同步器组件的整个JUC体系的基石,主要由一个变量state状态值和变种的CLH双向队列(头节点Node head和尾节点Node tail)组成;
原理:
l获取锁时通过CAS去更新state状态值操作,state初始值为0,说明没有被占用,当前线程可以获取锁,如果不为0说明被占用会尝试获取锁,仍获取不到会添加到等待队列,如队列不存在时初始一个队列(Node的thread值为null,waitStatus为0的哨兵节点作为头节点),如队列存在会在其后增加当前线程的一个Node节点,然后队列里的线程仍会自旋去尝试获取锁,如果仍获取不到,将当前线程的前节点的waitStatus改为-1,并通过LockSupport的park阻塞到等待队列,如果state的值等于0为空闲说明能够获取到锁然后出队,将该线程对应的Node节点设置为哨兵节点,以前的哨兵的节点将被回收掉;
常用线程池创建的几种方式
/**
* 一个线程池,一个线程,一个任务
*/
private static ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
/**
* 固定线程数的线程池,执行长期任务
*/
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
/**
* 缓存线程池,一个线程池N个线程,执行短期任务
*/
private static ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
/**
* 周期性线程池
*/
private static ExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(5);
说明:以上创建线程池的方式我们实际中并不会用到,而是下面自定义创建的方式
线程池创建的几个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
}
<<—核心类:ThreadPoolExecutor 下面为线程池的参数—>>:
- corePoolSize:核心线程数,正常工作的线程数量;
- maximumPoolSize:容纳同时执行的最大线程数量;
- keepAliveTime:空闲线程存活时间;
- unit:空闲线程存活时间单位;
- workQueue:存放任务的阻塞队列;
- threadFactory:线程工厂;
- handler:拒绝策略,有四种实现:
1.AbortPolicy:拒绝任务抛弃处理,并且抛出异常;
2.CallerRunsPolicy:拒绝任务直接抛弃;
3.DiscardOldestPolicy:重试添加当前的任务,直到成功;
4.DiscardPolicy:抛弃队列里面等待最久的一个线程,将当前任务加入队列;
<<—线程数的配置—>>
- CPU密集型:CPU核数+1;
- IO密集型:CPU核数*2或者CPU核数/1-阻塞系数(阻塞系数范围一般在0.8-0.9);
获取CPU核数的示例
:
// 获取CPU核数
int num = Runtime.getRuntime().availableProcessors();
<<—线程池工作原理—>>:
如果任务数大于corePoolSize则创建corePoolSize个线程去处理任务,再有任务进来首先看有没有能够处理任务的线程,如果没有可工作的线程并且任务队列没有满则将此任务放入到任务队列workQueue里,直到任务队列达到规定的数量,如果再有任务进来时,会将工作的线程增加到maximumPoolSize去处理任务,这时工作的线程数量已经达到maximumPoolSize并且阻塞的队列workQueue里的任务也是满的,再有任务进来的时候会调用我们的handler拒绝策略来处理此任务;当阻塞的队列workQueue任务数量随着时间减少并且有空余后,除了corePoolSize个工作线程继续工作,其他maximumPoolSize-corePoolSize线程将空闲keepAliveTime时间单位后,从线程池退出来;
java的一些锁相关说明
公平锁:有序的获取锁;>>示例如下:
new ReentrantLock(true)
非公平锁:直接获取锁,获取不到然后用公平锁的方式有序获取锁,非公平锁闭公平锁吞吐量大;ReentrantLock默认非公平锁、synchronized也是一种非公平锁;
可重入锁(递归锁):同步方法调用另一个同步方法或者同步代码块调用另一个同步代码块,同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;>>示例代码:
public class ReEnterLockDemo {
private static Object obj = new Object();
private static Lock lock = new ReentrantLock();
public static void syncMethod() {
new Thread(() -> {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "111");
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "222");
}
}
}, "threadA").start();
}
public static void lockMethod() {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "111");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "222");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "threadB").start();
}
public static void main(String[] args) {
syncMethod();
lockMethod();
}
}
自旋锁:线程不会立即阻塞,而是挂起采用循环的方式去尝试获取锁,好处是减少上下文切换,缺点是循环消耗CPU;>>示例代码:
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
独占锁(写锁):ReentrantReadWriteLock.WriteLock一次只能被一个线程所持有,ReentrantLock和synchronized都是独占锁;
共享锁(读锁):ReentrantReadWriteLock.ReadLock可被多个线程同时持有;
读写锁:ReentrantReadWriteLock,读锁:ReentrantReadWriteLock.ReadLock,写锁:ReentrantReadWriteLock.WriteLock;
synchronized的作用?synchronized同步方法和synchronized同步代码块原理
- 确保线程互斥的访问同步代码;
- 保证共享变量的修改能够及时可见;
- 有效解决重排序问题。
synchronized同步方法:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
synchronized同步代码块:当执行monitorenter时,如果目标对象的计数器为0,那么说明它没有被任何线程所持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并将计数器+1;在目标对象的计数器不为0时,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器+1,否则需要等待,直至持有线程释放该锁;当执行monitorexit时,java虚拟机将其计数器-1,计数器为0表示该锁已被释放。
ReentrantLock和synchronized有什么区别?用新的Lock有什么好处?
- synchronized是java的关键字,ReentrantLock是API层面的;
- synchronized底层是通过minitor对象完成,wait和notify方法也依赖monitor对象所在的同步方法或者同步代码块,自动释放,不可中断,默认为非公平锁;
- ReentrantLock为显示锁必须使用lock加锁和unlock释放锁,可以中断,默认也是非公平锁,可以设置为公平锁,通过
ReentrantLock(boolean fair)
设置; - synchronized的await和notify相当于ReentrantLock的Condition(条件)的await和signal,ReentrantLock可精确唤醒某个线程;
CountDownLatch、CyclicBarrier、Semaphore?
CountDownLatch:CountDownLatch(int count)
,通过countDown方法将计数量(count)减到0为止,要不会await阻塞;
CyclicBarrier:CyclicBarrier(int parties)
,增加到规定的数(parties)通过,要不会await阻塞;
Semaphore:Semaphore(int permits)
,多个线程抢占多个资源,到达数量(permits)等待,没到达线程占用锁,通过acquire占用方法和release释放方法;
二叉查找树和平衡二叉树有什么关系
二叉查找树:也称二叉搜索树,或二叉排序树。定义也比较简单,要么是一颗空 树,要么就是具有如下性质的二叉树:
- 若它的左子树不为空,则左子树上所有的节点值都小于它的根节点值;
- 若它的右子树不为空,则右子树上所有的节点值均大于它的根节点值;
- 它的左右子树也分别可以充当为二叉查找树;
- 没有键值相等的节点。
在二叉搜索树的基础上多了两个重要的特点:
- 左右两子树的高度差的绝对值不能超过 1;
- 左右两子树也是一颗平衡二叉树。
链接: link.
强平衡二叉树(AVL树)和弱平衡二叉树(红黑树)有什么区别
红黑树是在普通二叉树上,对每个节点添加一个颜色属性形成的,需要同时满足一下五条性质:
- 节点是红色或者是黑色;
- 根节点是黑色;
- 每个叶节点(NIL 或空节点)是黑色;
- 每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点)。
- 从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。
区别:AVL 树需要保持平衡,但它的旋转太耗时,而红黑树就是一个没有 AVL 树那样平衡,因此插入、删除效率会高于 AVL 树,而 AVL 树的查找效率显然高于红黑树。
链接: link.
二叉树图例:
B树与B+树的区别
B树:
- 关键字集合分布在整颗树中;
- 任何一个关键字出现且只出现在一个结点中;
- 搜索有可能在非叶子结点结束;
- 其搜索性能等价于在关键字全集内做一次二分查找。
B+树:
- 有 n 棵子树的非叶子结点中含有 n 个关键字(B树是 n-1 个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(B树是每个关键字都保存数据);
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针, 且叶子结点本身依关键字的大小自小而大顺序链接;
- 所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字;
- 通常在 B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点;
- 同一个数字会在不同节点中重复出现,根节点的最大元素就是 B+树的最大元素。
B+树相比于B树的查询优势
- B+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
- B+树查询必须查找到叶子节点,B 树只要匹配到即可不用管元素位置,因此 B+树查找更稳定(并不慢);
- 对于范围查找来说,B+树只需遍历叶子节点链表即可,B 树却需要重复地中序遍历