罗曼·罗兰说:“世界上只有一种真正的英雄主义,那就是在看清生活的真相之后,依然热爱生活。”
JAVA开发知识点汇总
- JAVA
- JVM
- 垃圾标记算法
- 三色标记法
- 可达性分析法
- 引用计数法
- 可以作为GCroots的对象有哪些?
- GC的种类和触发机制
- 年轻代触发机制(Minor GC)
- 老年代触发机制(Major GC/Full GC)
- Full GC 触发机制
- 为什么需要把Java堆分代?
- 为什么新生代中Eden:S1:S2 = 8:1:1?
- 垃圾回收器如何选择?
- 导出JVM内存快照
- JVM参数和调优
- jvm调优的本质?
- 需要垃圾回收的上线表现?
- JVM调优原则
- JVM运行参数设置
- 并发编程
- 线程池
- 锁
- 锁升级与锁降级
- 锁粗化(锁膨胀)
- 可重入锁
- 公平锁
- 读锁与写锁
- 共享锁和排他锁的区别?
- 线程终止的优雅方式:两阶段终止
- 数据库
- MySQL
- MySQL的MVCC机制是什么和怎么实现的?
- 数据库的两阶段提交是什么?
- Redis
- redis持久化
- redis主从
- redis集群
- redis哨兵
- 零拷贝
JAVA
JVM
垃圾标记算法
三色标记法
主流的垃圾收集器基本上都是基于可达性分析算法来判定对象是否存活的。根据对象是否被垃圾收集器扫描过而用白、灰、黑三种颜色来标记对象的状态的一种方法。而其中
白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始阶段,所有的对象都是白色的,若在分析结束之后对象仍然为白色,则表示这些对象为不可达对象,对这些对象进行回收。
灰色:表示对象已经被垃圾收集器访问过,但是这个对象至少存在一个引用(属性)还没有被扫描过。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描过。黑色表示这个对象扫描之后依然存活,是可达性对象,如果有其他对象引用指向了黑色对象,无须重新扫描,黑色对象不可能不经过灰色对象直接指向某个白色对象。
缺点:实际上在并发的情况下会存在多标和漏标的问题。
参考: https://blog.csdn.net/weixin_39555954/article/details/127623284
可达性分析法
通过一系列称为GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象是不可用的。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时候计数器为0时的对象就是不能再被使用。
缺点是可能存在循环引用的问题
可以作为GCroots的对象有哪些?
1、虚拟机栈中引用的对象。比如:各个线程被调用的方法中使用到的参数、局部变量等。
2、本地方法栈内JNI(通常说的本地方法)引用的对象。
3、方法区中类静态属性引用的对象。比如:Java类的引用类型静态变量
4、方法区中常量引用的对象。比如:字符串常量池(string Table) 里的引用
5、所有被同步锁synchronized持有的对象
6、Java虚拟机内部的引用。基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError) ,系统类加载器。
7、反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
8、除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)。
GC的种类和触发机制
GC英文全称为Garbage Collection
JVM常见的GC包括三种:Minor GC,Major GC与Full GC
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:
a、一种是部分收集(Partial GC)
b、一种是整堆收集(Full GC)
部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集,其中又分为:
a、新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
b、老年代收集(Major GC/Old GC):只是老年代的垃圾收集
c、混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,目前,只有G1 GC会有这种行为
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
注意:JVM在进行GC的时候,并非每次都对所有区域都进行垃圾回收,大部分时候回收的都是指新生代。
年轻代触发机制(Minor GC)
当年轻代空间不足时,就会触发Minor GC,这里的年轻代空间不足指的是Eden区满,Survivor区满不会触发GC(每次Minor GC 会清理年轻代的内存)。
因为JVM汇总大部分对象朝生夕死,MinorGC会发生的很频繁,回收也比较迅速。Minor
GC会引发STW,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行
老年代触发机制(Major GC/Full GC)
当老年代空间不足时,会尝试触发MajorGC ,如果之后空间依旧不足,就会触发MajorGC。
Full GC 触发机制
触发Full GC执行的情况有如下五种:
1、调用System.gc(),系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5、由Eden区,from区向to区复制时,对象大小大于to区可用内存,则把对象转存到老年代,并且老年代的可用内存小于该对象大小(如果Full GC后空间仍不足会抛出OOM异常)
Full GC也是开发和JVM调优时需要尽可能避免或者将减少的
为什么需要把Java堆分代?
不同对象的生命周期不同,而且百分之八九十的对象都是临时对象,分代回收就是为了百分之十左右的长时间存活的对象进行管理和减少回收次数,优化了内存和应用的效率。
为什么新生代中Eden:S1:S2 = 8:1:1?
发生垃圾回收时,会将Eden区和Survivor from中还存活的对象一次性复制到另一块Survivor区域(复制算法),然后就清空调Eden区和Survivor from区中的数据。这样新生代中可用的内存:复制算法所需要的担保内存 = 9:1,这样即使所有的对象都不会存活,那么也只会“浪费”10%的内存空间。不过我们也无法保证存活的对象一定<2%或10%,所以当新生代中Survivor to区内存不够用时,就会触发老年代的担保机制进行分配担保,进行majorGC 或者 full GC。
垃圾回收器如何选择?
1、优先调整堆的大小让JVM自适应完成。
2、如果内存小于100M,使用串行收集器
3、如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
4、如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
5、如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
官方推荐G1,性能高。
现在互联网的项目,基本都是使用G1。可以使用-XX:+PrintCommandLineFlags查看默认的垃圾回收器。
截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
GC发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC
参考
https://blog.csdn.net/weixin_45525272/article/details/126370223
https://blog.csdn.net/sd_960614/article/details/126900380
导出JVM内存快照
首先,使用 jps 命令查看已启动的进程和对应的PID;jps
然后使用 jmap命令和查出的线程ID导出堆内存信息;jmap -dump:format=b,file=heap.bin [jpd_id]
然后可以使用MAT或者JProfiler性能分析工具分析JVM快照。
JVM参数和调优
jvm调优的本质?
回收垃圾,及时回收没有用的垃圾对象,及时释放掉内存空间。
需要垃圾回收的上线表现?
1、垃圾对象太多(对象沾满内存),内存不足,程序性能降低
2、垃圾回收线程过多,频繁回收垃圾,导致程序性能降低。
3、垃圾回收频繁导致STW
4、Heap内存(老年代)持续上涨达到设置的最大内存值;
5、Full GC 次数频繁;
6、GC 停顿时间过长(超过1秒);
7、应用出现OutOfMemory 等内存异常;
JVM调优原则
JVM调优是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,因此,在进行JVM调优时,我们要遵循一些原则:
1、大多数的Java应用不需要进行JVM优化;
2、大多数导致GC问题的原因是代码层面的问题导致的(代码层面);
3、上线之前,应先考虑将机器的JVM参数设置到最优;
4、减少创建对象的数量(代码层面);
5、减少使用全局变量和大对象(代码层面);
6、优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
7、分析GC情况优化代码比优化JVM参数更好(代码层面);
通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。
JVM运行参数设置
-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志
-XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况
-Xms 设置Java程序启动时初始化堆大小
-Xmx 设置Java程序能获得最大的堆大小
-Xmx20m -Xms5m -XX:+PrintCommandLineFlags 可以将隐式或者显示传给虚拟机的参数输出
-Xmn 可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右
-XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例。含义:-XX:SurvivorRatio=eden/from**/eden/to
HeapDumpOnOutOfMemoryError,使用该参数可以在内存溢出时导出整个堆信息,与之配合使用的还有参数-XX:HeapDumpPath,可以设置导出堆的存放路径(结合内存分析工具:Memory Analyzer或者JProfiler)
栈参数配置 Java虚拟机提供了参数-Xss来指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度。
虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升老年代-XX:MaxTenuringThreshold,默认情况下为15。
以8C32G的服务器为例,给出以下JVM参数模板(使用的cms垃圾回收器,使用时根据需要请修改)
-XX:+PrintGC
-XX:+PrintGCDetails
-Xms2048m
-Xmx2048m
-XX:+PrintCommandLineFlags
-Xmn1024m
-XX:SurvivorRatio=8:1:1
-XX:+UseConcMarkSweepGC
-XX:HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:/JVM/demo.dump
//晋升至老年代的年龄限制
-XX:MaxTenuringThreshold=15
//调整大对象标准,超过的直接分配到老年代,默认单位是字节
-XX:PretenureSizeThreshold=1000000
-Xms 默认情况下堆内存的64分之一
-Xmx 默认情况下对内存的4分之一
-Xmn 默认情况下堆内存的64分之一
-XX:NewRatio 默认为2
-XX:SurvivorRatio 默认为8
参考 https://baijiahao.baidu.com/s?id=1747377064336158073&wfr=spider&for=pc
并发编程
线程池
线程池的创建方式总共有以下 7 种:
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
线程池创建后会存储一定数量的线程和corePoolSize相等,有任务进入线程池时,首先是核心线程数进行工作如果所有核心线程数都在工作,多余的工作任务就排入workQUeue中,指导工作队列排满之后,创建新的工作线程直到maximumPoolSize,如果任务还继续增加且工作队列已经满了,就会执行相应的hadler拒绝策略。指导所有任务都处理完成后,如果线程池中的线程还多余核心线程数且线程空闲时间超过了keepAliveTime就会呗kill,直到线程个数减少至核心线程的数目。
线程池的拒绝策略有四种,也可以通过继承RejectedExecutionHandler接口,重写rejectedExecution方法实现自己的拒绝策略。
1、AbortPolicy
第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException的RuntimeException
2、DiscardPolicy
第2种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知
3、DiscardOldestPolicy
第3种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务
4、CallerRunsPolicy
第4种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务
综上,线程池在两种情况下会拒绝新任务:调用用了线程池的shudown方法和线程池的工作任务已经很饱和 的时候。
锁
锁升级与锁降级
synchronized会经历四个阶段:无锁状态、偏向锁、轻量级锁、重量级锁依次从耗费资源最少,性能最高,到耗费资源多,性能最差。
1、对于Synchronized的锁升级过程是无锁到偏向锁,偏向锁是对象头中的Marword中的线程ID存储为当前线程ID;
2、查看对象头中的MarkWord里的Lock Record指针指向的是否是当前线程的虚拟机栈,如果是,拿锁执行业务(轻量级锁);如果不是则进行CAS,尝试修改,若是修改几次都没有成功,再升级到重量级锁。
3、查看对象头中的MarkWord里的指向的ObjectMonitor,查看owner是否是当前线程,如果不是,扔到ObjectMonitor里的EntryList中排队,并挂起线程,等待被唤醒(重量级锁)。
一般情况下,新new出来的一个对象,暂时就是无锁状态。
https://www.jb51.net/article/281099.htm
对于读锁与写锁的转化来说:
锁降级: 获取写锁,获取读锁,释放之前获取的写锁。这时,写锁降级为读锁。
锁升级: 获取读锁,获取写锁,释放之前获取的读锁。这时,读锁升级为写锁。
https://blog.csdn.net/qq_55660421/article/details/123828778
锁粗化(锁膨胀)
锁膨胀是编译Java文件的时候,JIT帮我们做的优化,它会减少锁的获取和释放次数。
锁消除则是在一个加锁的同步代码块中,没有任何共享资源,也不存在锁竞争的情况,JIT编译时,就直接将锁的指令优化掉。
可重入锁
synchronized和ReentrantLock都是可重入锁。
可重入锁最多可以冲入的次数2^31-1次
https://blog.csdn.net/zjp_01/article/details/127324509
公平锁
Syhcronized是非公平锁,ReentrantLock可以使公平锁也可以是非公平锁。
ReentrantLock reentrantLock = new ReentrantLock(); //默认是非公平锁
ReentrantLock reentrantLock = new ReentrantLock(true); //传入参数true就是公平锁
读锁与写锁
读锁是支持重进入的共享锁。
共享锁和排他锁的区别?
共享锁又叫S锁,拍他锁又叫X锁。
线程终止的优雅方式:两阶段终止
先看一下基础:
interrupt()的作用:
1、给正常运行的线程打上interrupted的状态
2、唤醒处于sleep、wait和join这三个轻量级阻塞状态的线程,抛出InterruptedException异常。
3、需要注意:在以上第二点的情况下,interrupt抛出异常后,线程的打断标记会重置为false。
isInterrupted()和interrupted()的区别:
前者是非静态函数只适用于读取中断状态,不修改状态;后者返回读取的中断状态,还会重置中断标志位。
参考:https://blog.csdn.net/qq_57421630/article/details/130095865
两阶段终止的实现
实现其实还是以上提到的interrupt、interrupted和isInterrupted的特性:interrupt对运行中的线程仅仅设置中断状态并不中断线程,对轻量级阻塞状态的线程会发出InterruptedException异常,然后重置中断状态;interrupted会直接重置中断状态;isInterrupted直接读取中断状态不改变中断状态。
public class TestTwoStageTermination {
public static void main(String[] args) throws InterruptedException {
TwoStageTermination t1 = new TwoStageTermination();
t1.start();
Thread.sleep(3000);
t1.stop();
}
}
class TwoStageTermination {
private Thread monitor;
// 启动线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread currentThread = Thread.currentThread();
// 根据打断标记,退出循环,线程结束
if (currentThread.isInterrupted()) { // isInterrupted() 打断标记的状态
System.out.println("打断标记:true, 线程退出!");
break;
}
try {
Thread.sleep(1000); // 情况一:睡眠中打断,抛出InterruptedException异常,唤醒线程,清除打断标记:false,需要手动重置打断标记为true
System.out.println("线程运行中···"); // 情况二:线程正常运行,打断后,线程不会自动停止,打断标记置为:true,用打断标记写if判断
} catch (InterruptedException e) {
e.printStackTrace();
currentThread.interrupt(); // 再次打断:重置打断标记为true,使得循环退出
}
}
});
monitor.start();
}
// 打断线程
public void stop() {
monitor.interrupt(); // interrupt() 打断线程
}
}
两阶段终止模式是一种并发设计模式,它用于优雅地终止线程。它将终止过程分成两个阶段,第一阶段由线程T1向线程T2发送终止指令,第二阶段是由线程T2响应终止指令。这种模式通过将停止线程这个动作分解为准备阶段和执行阶段这两个阶段,提供了一种通用的用于优雅地停止线程的方法!(优雅地终止线程可以确保线程在终止之前完成它应该完成的任务并执行一些收尾工作,从而保证数据和业务的安全性。如果随意终止线程,可能会导致数据丢失或损坏,或者导致程序运行不稳定。)
数据库
MySQL
MySQL的MVCC机制是什么和怎么实现的?
数据库的两阶段提交是什么?
Redis
redis持久化
RDB 和AOP两种持久化方式。