SomeThing
书籍推荐
深入理解Java虚拟机
Java并发编程的艺术
JUC多线程及高并发
JMM(Java内存模型)
volatile的理解
- volatile是JVM提供的轻量级的同步机制
- 保证内存可见性
- 不保证原子性
- 禁止指令重排
线程内存可见性
不保证原子性
对任意单个volatile变量的读写具有原子性
对类似volatile++这种复合操作布局有原子性
i++ 实际分成两步:
复合两步一起操作不具有原子性
- 读 读取内存中i (单独的读具有原子性)
- 写 i+1 (单独的写具有原子性)
禁止指令重排
何为有序性
指令重排
Volatile禁止指令重排小结
线程安全性获得保证
-
工作内存与主内存颜值现象导致的可见性问题
可以使用synchronized或者volatile关键解决,他们都可以使一个线程修改的变量立即对其他线程可见
-
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
volatile使用
单例模式DCL机制分析
CAS
-
CAS(CompareAndfSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止
-
CAS应用
CAS有三个操作数,内存值V,旧得预期值,要修改的更新值B。
当且仅当预期值A和主内存值V相同时,将内存值V修改为B,否则什么都不做 -
synchronized关键字保证一致性并发性下降,CAS没有加锁使用natice+CAS思想既保证了一致性也保证了并发性
atomicInteger.getAdnCrement() 源码分析
CAS底层原理
CAS缺点
-
循环时间长开销大
(比较旧的预期值和主内存最新的值是一个循环比较过程,如果长时间比较不成功,CPU开销大) -
只能保证一个共享变量的原子操作
对于多个变量操作时,循环CAS无法保证操作的原子性,这个时候就可以用锁来保证原子性
-
引出来ABA问题
ABA问题
-
ABA问题的产生:
CAS算法实现一个重要的前提需要提取出主内存中的某个时刻的数据值并在当下时刻工作内存中的值比较替换,那么在这个时间差类会导致数据的变化。
比如:一个线程T1从主内存位置V中取出A,这时候另一个线程T2从主内存中读取A,并且线程T2进行了一些操作讲述职变成B后更显到主内存中,然后线程T2有将V位置的数据变成A,这个时候线程T1进行CAS操作发现内存中仍然是A,然后线程T1操作成功。尽管线程T1操作成功,但并不代表这个过程就没有问题,相当于T1遗漏掉T2更新的一个中间变量值的过程
原子引用
AtomicReference<User> atomicReference = new AtomicReference<User>();
集合类不安全问题
//ArrayList 并发安全是示例 java.util.ConcurrentModificationException
public static void main(String[] args) {
final List<String> list = new ArrayList();
for (int i = 1; i < 100; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
list.add(UUID.randomUUID().toString());
System.out.println(list);
}
});
thread.setName("线程" + i);
thread.start();
}
}
//并发问题原因: 并发争抢修改导致,一个线程正在读,另一个进行写 导致数据不一致
//解决
// 1.List<String> list = new Vector<String>() 使用加锁线程安全的类
// 2.List<String> list = Collections.synchronizedList(new ArrayList()); 包一层安全
// 3.List<String> list = new CopyOnWriteArrayList<String>();
CopyOnWriteArrayList 原理分析
HashSet 底层是 HashMap
锁相关
公平锁
指多个线程按照申请锁的顺序来获取锁,类似排队排序
非公平锁
指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,
在高并发的情况下,有可能造成优先级反转或者饥饿等待现象
synchronized:是非公平锁
公平、非公平锁两者区别
可重入锁(递归锁)
指的是同一线程外层函数获取锁之后,内层递归函数仍然能获取该锁的代码
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
即:线程可以进入任何一个已经拥有的锁所同步这的代码块
synchronized 、ReentrantLock都是可重入锁,可重入锁最大的作用就是避免死锁
多个lock.lock() 只要unlock() 匹配对应上即可
自旋锁(CAS实现)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
//自旋原理代码示例
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//自旋锁示例
public class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
public void myLock() {
Thread thread = new Thread();
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
Thread thread = new Thread();
while (!atomicReference.compareAndSet(thread, null)) {
}
}
}
独占锁(写)、共享锁(读)、互斥锁
- 独占锁
指该锁一次只能被一个线程所持有
对ReentrantLock和Synchronized而言是独占锁 - 共享锁
指该锁可被多个线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁
读锁的共享锁可以保证并发读是非常高效的。
读写,写读,写写的过程是互斥的
JUC(java.util.concurrent)
CountDownLatch
让一些线程阻塞知道另外一些线程完成一系列的操作后才被唤醒
CountDownLatch主要有两个方法,当一个或多个线程代用await方法时调用此方法的线程会被阻塞。其他线程调用countDown()方法,会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用waait方法被阻塞的线程被唤醒继续执行
public static void main(String[] args) throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i <= 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "over");
countDownLatch.countDown();
}
}, String.valueOf(i));
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"over");
//等待前六个线程结束后 执行main线程
}
CyclicBarrier
CyclicBarrier字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是让一组线程达到一个屏障(也可叫做同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法
public static void main(String[] args) throws InterruptedException {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(7, new Runnable() {
@Override
public void run() {
System.out.println("main 主线程执行");
}
});
for (int i = 0; i <= 7; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "执行");
} catch (InterruptedException e) {
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}, String.valueOf(i));
}
}
Semaphore(信号量)
信号量主要两个目的:
- 用于多个共享资源的互斥使用
- 并发线程数的控制
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(3); //设置三个资源
for (int i = 0; i <= 6; i++) { //6个并发量
new Thread(new Runnable() {
@Override
public void run() {
new Thread(new Runnable() {
@Override
public void run() {
try {
//占有资源
semaphore.acquire();
System.out.println("抢到");
TimeUnit.SECONDS.sleep(3);
System.out.println("停3秒离开");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放资源
semaphore.release();
}
}
});
}
}, String.valueOf(i));
}
}
阻塞队列
BlockingQueue的核心方法
结构介绍
-
ArrayBlockingQueue:数组结构组成的有界阻塞队列
-
LinkedBlockingQueue:链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
-
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
-
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
每个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
-
LinkedTransferQueue:链表结构组成的无界阻塞队列
-
LinkedBlockingDeque:链表结构组成的双向阻塞队列
多线程的判断用while
将if替换为while,解决线程虚假唤醒的问题
Lock、synchronized区别
- synchronized,wait,notify
- lock、await、singal
- 原始结构
- synchronized 属于JVM层面、是Java的关键字。
- Lock属于API层面Java5以后出现的类
- 使用方法
- synchronized 不需要用户去手动释放锁,当synchronized 代码执行完后系统会自动让线程释放对锁的占用
- ReentrantLock需要用户去手动释放锁,若没有主动释放锁就可能导致出现死锁现象,
需要lock()和unLock()方法配合try/finally语句块来完成
- 等待是否可中断
- synchronized 不可中断,除非抛出异常或者正常运行完成
- ReentrantLock可中断,
- 设置超时方法trylock(long time,TimeUnit unit)
- lockInterruptibly()放代码块中,调用Interrupt()方法可中断
- 加锁是否公平
- synchronized 是非公平锁
- ReentrantLock 两者都可以,默认非公平锁。构造方法可以传入boolean值,true为公平锁,false是非公平锁
- 绑定多个条件condition
- synchronized 没有
- ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized 要么唤醒一个线程要么唤醒全部线程
生产、消费者(三个版本)
BlockingQuene版本
package com.yjn.test.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ProjectName: com.yjn.test.thread
* @Description:
* @Author: yuanjunnan
* @CreateDate: 21/05/2019
* @Version: v1.0
*/
public class ProduceConsumeBlockingQuene {
public static void main(String[] args) {
final MyResourceBlockingQuene resourceBlockingQuene = new MyResourceBlockingQuene(new ArrayBlockingQueue<String>(10));
new Thread(new Runnable() {
@Override
public void run() {
try {
resourceBlockingQuene.addNum();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "生产者").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
resourceBlockingQuene.reduceNum();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
}
class MyResourceBlockingQuene {
BlockingQueue<String> blockingQueue = null;
private volatile boolean FLAG = true;
private AtomicInteger atomicInteger = new AtomicInteger();
public MyResourceBlockingQuene(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
public void addNum() throws Exception {
String data = null;
boolean retValue;
while (FLAG) {
data = atomicInteger.incrementAndGet() + "";
retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "成功生产数据" + data);
} else {
System.out.println(Thread.currentThread().getName() + "失败生产数据" + data);
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "生产停止");
}
public void reduceNum() throws Exception {
String result = null;
while (FLAG) {
result = blockingQueue.poll(2, TimeUnit.SECONDS);
if (result == null || result.equalsIgnoreCase("")) {
FLAG = false;
System.out.println(Thread.currentThread().getName() + "超时消费停止");
return;
}
System.out.println(Thread.currentThread().getName() + "消费数据成功" + result);
}
}
public void stop() {
FLAG = false;
}
}
Synchronized版本
package com.yjn.test.thread;
/**
* @ProjectName: com.yjn.test.thread
* @Description:
* @Author: yuanjunnan
* @CreateDate: 21/05/2019
* @Version: v1.0
*/
public class ProduceConsumeSynchronized {
public static void main(String[] args) {
final MyResourceSynchronized myResource = new MyResourceSynchronized(0);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
myResource.addNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "生产者--").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
myResource.reduceNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "消费者--").start();
}
}
class MyResourceSynchronized {
private int num;
public MyResourceSynchronized(int num) {
this.num = num;
}
public synchronized void addNum() throws InterruptedException {
while (num != 0) {
wait();
}
Thread.sleep(1000);
num++;
System.out.println(Thread.currentThread().getName() + "生产");
notify();
}
public synchronized void reduceNum() throws InterruptedException {
while (num != 1) {
wait();
}
Thread.sleep(1000);
num--;
System.out.println(Thread.currentThread().getName() + "消费");
notify();
}
}
Lock版本
package com.yjn.test.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ProjectName: com.yjn.test.thread
* @Description:
* @Author: yuanjunnan
* @CreateDate: 21/05/2019
* @Version: v1.0
*/
public class ProduceConsumeLock {
public static void main(String[] args) {
final MyResourceLock myResource = new MyResourceLock(0);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
myResource.addNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "生产者--").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
myResource.reduceNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "消费者--").start();
}
}
class MyResourceLock {
private int num;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public MyResourceLock(int num) {
this.num = num;
}
public void addNum() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
TimeUnit.SECONDS.sleep(1);
num++;
System.out.println(Thread.currentThread().getName() + "生产");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void reduceNum() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
while (num != 1) {
condition.await();
}
TimeUnit.SECONDS.sleep(1);
num--;
System.out.println(Thread.currentThread().getName() + "消费");
condition.signalAll();
}
} finally {
lock.unlock();
}
}
}
线程实现的三种方法
实现Runable接口
继承Thread
实现Callable
public class CallableTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new CallableTest());
new Thread(futureTask).start();
while (futureTask.isDone()) {
Object result = futureTask.get();
}
}
}
线程池(第四种使用线程)
线程池的优势
线程池的使用
Java中的线程池通过Executor框架实现的
该框架中用到了Executor、Executors(辅助工具类)、ExecutorService、ThreadPoolExecutor这几个类
newFixedThreadPool(int)
- 执行长期的任务,性能好很多
newSingleThreadExecutor()
- 一个任务一个任务执行的场景
newCachedThreadPool()
- 执行很多短期异步的向程序或者负载较轻的服务器
newSingleThreadExecutor()
newWorkStealingPool(int)
线程池的参数
corePoolSize
线程池中的常驻核心线程数
- 在创建了线程后,当请求任务来了之后,就会安排线程池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数据达到corePoolSize之后就会把到达的任务放到缓存队列中
maximumPoolSize
线程池能够容纳同事执行的最大线程数,此值必须大于等于1
keepAliveTime
多余的空闲线程的存活时间,当前线程池数量超过corePoolSize时,
当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁知道直到只剩下corePoolSize个线程为止
默认的情况下只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
unit
keepAliveTime的时间单位
workQueue
任务队列,被提交但尚未被执行的任务
threadFactory
表示生成线程池中工作线程的线程工厂,用于创建线程一般默认的即可
handler
拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的Runnable的策略
- AbortPolicy(默认):直接抛出异RejectedExecutionException异常阻止系统正常运行
- DiscardPolicy:"调用者运行"一种调节机制,改策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- CallerRunsPolicy:直接丢弃任务,不予任务处理也不抛出异常,如果任务允许丢失,这是最好的一种方案
线程池的底层原理
阿里巴巴Java开发手册线程池规约
- Executors.newCachedThreadPool();
- Executors.newSingleThreadExecutor();
- Executors.newWorkStealingPool(int)
以上三种创建线程池的方法都不用
推荐:
ExecutorService threadPool = new ThreadPoolExecutor(
2, //corePoolSize
5, //maximumPoolSize
1L, //keepAliveTime
TimeUnit.SECONDS, //TimeUnit
new LinkedBlockingQueue<Runnable>(3), //指定阻塞队列的大小
Executors.defaultThreadFactory(), //线程工厂方法
new ThreadPoolExecutor.AbortPolicy()); //决绝策略
合理配置线程池的线程数
CPU密集型
线程数:CPU的核数+1个线程的线程池
IO密集型
死锁编码及定位分析
死锁产生
产生死锁原因
-
系统资源不足
-
资源分配不当
-
进程运行推进的顺序不合适
死锁code示例
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "持有"
+ lockA + ",申请" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "持有"
+ lockB + ",申请" + lockA);
}
}
}
}
public class DeadlockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB), "AAA").start();
new Thread(new HoldLockThread(lockB, lockA), "BBB").start();
}
}
jsp、jstack查看死锁
- jps命令定位进程号
- jstack找到死锁查看
JVM、GC解析
JVM体系结构
- Java8以后JVM
GC作用域
GC垃圾回收算法
引用计数法
复制算法
标记清除
标记整理
GC相关分析
垃圾定位、GCroots
- 垃圾:内存中已经不再被使用到的空间就是垃圾
- 判断一个对象可否被回收
- 引用计数法
- 枚举根节点做可达性分析(根搜索路径)
枚举根节点做可达性分析(GCRoots)
Java中可以作为GCRoots的对象
-
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
-
方法区中的类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI(Native方法)引用的对象
//t1 可以叫 虚拟机栈中引用的对象 GCRootsDemo t1 = new GCRootsDemo(); //方法区中的类静态属性引用的对象 private static GCRootsDemo2 t2 = new GCRootsDemo2(); //方法区中常量引用的对象 private static final GCRootsDemo3 t3 = new GCRootsDemo3();
JVM调优、参数设置
JVM参数类型
XX参数
-
Boolean类型
-
使用格式:
“-XX : + 参数名称”,表示开启
“-XX : - 参数名称”,表示关闭
-
示例
-XX : +PrintGCDetails 开启GC打印详情 / -XX : -PrintGCDetails 关闭GC打印详情
-XX : +UseSerialGC 开启串行垃圾回收器 / -XX : -UseSerialGC 关闭串行垃圾回收器
-
-
KV设值类型
-
使用格式:
-XX:属性key = 属性value
-
示例
-XX:Metaspace=128m
-XX:MaxTenuringThreshold=15
-
-
jinfo举例,查看当前运行程序的JVM参数配置
jinfo -flag 配置项 进程编号
例: jinfo -flag PrintGCDetails 13632
jinfo -flags 进行编号 查看全部参数值
X参数
- -Xint 解释执行
- -Xcomp 第一次使用就编译成本地代码
- -Xmixed 混合模式
标配参数
- -version
-version就是查看当前机器的java是什么版本,是什么类型的JVM(Server/Client),采用的是什么执行模式 - -help
- java -showversion
-showversion的作用是在运行一个程序的时候首先把JVM的版本信息打印出来,这样便于问题诊断。
JVM默认参数值查看命令
查看JVM默认参数值
java -XX:+PrintFlagsInitial
查看JVM修改更新参数值
java -XX:+PrintFlagsFinal
其中 = 显示是默认值,:= 修改值
java -XX:+PrintCommandLineFlags
可以让在程序运行前打印出用户手动设置或者JVM自动设置的XX选项,建议加上这个选项以辅助问题诊断。
JVM常用基本配置参数
-Xms
最小堆内存 默认是物理内存的1/64,等价于 -XX:InitialHeapSize
-Xmx
最大堆内存 默认是物理内存的1/4,等价于 -XX:MaxHeapSize
-Xss
设置单个线程栈的大小,一般默认为512k—1024k,等价于 -XX:ThreadStackSize
-Xmn
设置年轻代大小
-XX:MetaspaceSize
设置元空间大小
元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代直接最大的区别在于:元空间并不在虚拟机中,而是在本地内存,因此默认情况下元空间的大小仅售本地内存限制
-XX:+PrintGCDetails
输出纤细GC收集日志
-XX:SurvivorRatio
设置新生代中Eden和S0/S1空间比例
默认 -XX:SurvivorRatio=8,Eden::S0:S1=8:1:1
假如 -XX:SurvivorRatio=4,Eden::S0:S1=4:1:1 SurvivorRatio的值设置Eden的比例,S0/S1 相同
-XX:NewRatio
配置年轻代与老年代在堆结构的比例
默认 -XX:NewRatio=2 新生代占1,老年代占2,年轻代占整个堆的1/3
假如 -XX:NewRatio=4 新生代占1,老年代占4,年轻代占整个堆的1/5
NewRatio值就是设置老年代的占比,剩下的1给新生代
垃圾最大年龄
-XX:MaxTenuringThreshold
强、软、弱、虚引用是什么
引用结构
强引用(默认模式)
public static void main(String[] args) {
Object obj1 = new Object(); // 默认就是强引用
Object obj2 = obj1;
obj1 = null;
System.gc();
System.out.println(obj2); //obj2依旧不被回收
}
软引用
弱引用
WeakHashMap
public static void main(String[] args) {
WeakHashMap<String, String> weakHashMap = new WeakHashMap<String, String>();
HashMap<String, String> hashMap = new HashMap<String, String>();
String weakHashMapKey = new String("weakHashMapKey");
String hashMapKey = new String("hashMapKey");
weakHashMap.put(weakHashMapKey, "weakHashMap");
hashMap.put(hashMapKey, "hashMap");
System.out.println("hashMap:"+hashMap);
System.out.println("weakHashMap:"+weakHashMap);
System.out.println("==============================================");
weakHashMapKey = null;
hashMapKey = null;
System.out.println("hashMap:key=null"+hashMap);
System.out.println("weakHashMap:key=null"+weakHashMap);
System.out.println("==============================================");
System.gc();
System.out.println("key=null,hashMap手动GC后:" + hashMap);
System.out.println("key=null,weakHashMap手动GC后:" + weakHashMap);
}
//打印
// hashMap:{hashMapKey=hashMap}
// weakHashMap:{weakHashMapKey=weakHashMap}
//==============================================
// hashMap:key=null{hashMapKey=hashMap}
// weakHashMap:key=null{weakHashMapKey=weakHashMap}
//==============================================
// key=null,hashMap手动GC后:{hashMapKey=hashMap}
// key=null,weakHashMap手动GC后:{}
虚引用
public static void main(String[] args) {
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
Object obj1 = new Object();
WeakReference<Object> objectWeakReference = new WeakReference<Object>(obj1, referenceQueue);
System.out.println("强引用正常对象:" + obj1);
System.out.println("弱引用正常对象:" + objectWeakReference.get());
System.out.println("referenceQueue:" + referenceQueue.poll());
obj1 = null;
System.gc(); //手动GC 虚引用被GC后会放到ReferenceQueue 中
System.out.println("强引用正常对象:" + obj1);
System.out.println("弱引用GC后对象:" + objectWeakReference.get());
System.out.println("referenceQueue:" + referenceQueue.poll());
// 强引用正常对象:java.lang.Object@4617c264
// 弱引用正常对象:java.lang.Object@4617c264
// referenceQueue:null
// 强引用正常对象:null
// 弱引用GC后对象:null
// referenceQueue:java.lang.ref.WeakReference@36baf30c
ReferenceQueue<Object> referenceQueue2 = new ReferenceQueue<Object>();
Object obj2 = new Object();
PhantomReference<Object> objectPhantomReference = new PhantomReference<Object>(obj2, referenceQueue2);
System.out.println("强引用正常对象:" + obj2);
System.out.println("虚引用正常对象:" + objectPhantomReference.get());
System.out.println("referenceQueue:" + referenceQueue2.poll());
obj2 = null;
System.gc(); //手动GC 虚引用被GC后会放到ReferenceQueue 中
System.out.println("强引用正常对象:" + obj2);
System.out.println("虚引用GC后对象:" + objectPhantomReference.get());
System.out.println("referenceQueue:" + referenceQueue2.poll());
// 强引用正常对象:java.lang.Object@7a81197d
// 虚引用正常对象:null
// referenceQueue:null
// 强引用正常对象:null
// 虚引用GC后对象:null
// referenceQueue:java.lang.ref.PhantomReference@5ca881b5
}
Java提供了4中引用类型,在垃圾回收的时候,都有各自的特点
ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行
创建引用的时候,可以指定关联的队列,当GC释放对象内存的时候,会将引用加到引用队列
如果程序当先某个虚引用已经被加入到引用队列,那就可以在所引用的队形的内训被回收之前采取必要的行动,相当于通知机制
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的队形被回收,通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情
GCRoots和四大引用总结
OOM 的认识
java.lang.StackOverflowError
栈溢出错误
java.lang.OutOfMemoryError
OOM-Java heap space
堆内存溢出错误
OOM-GC overhead limit exceeded
GC回收时间过长会抛出OutOfMemroyError----GC overhead limit exceeded
过长的定义:超过98%的时间用来做GC并且回收不到2%的堆内存,连续多次的GC都只回收不到2%的极端情况下才会抛出
假如不抛出GC overhead limit 错误会发生什么情况呢?
那就是GC清理这么点内存很快就被再次填满,迫使GC在再次执行,这样就形成了恶性循环,CPU使用率一直是100%,且GC却没有任何成果
OOM-Direct buffer memory
OOM-unable to create new native thread
高并发请求服务器时,经常出现 java.lang.OutOfMemoryError:nable to create new native thread
准确的讲应该native thread 异常与对应平台有关
- 导致原因:
①应用创建了太多的线程,一个应用进程创建多个线程,超过系统承载极限
②服务器并不允许你的应用程序创建那么多线程,Linux系统默认允许单个进程可以创建的线程数是1024个(非root)
当应用创建线程超过就报错java.lang.OutOfMemoryError:nable to create new native thread - 解决方案:
①想办法降低应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是需要应用应降低线程数量
②对于有的应用,确实需要创建很多线程,远超Linux默认的1024个线程的限制,可以通过修改Linux的默认配置,扩大线程限制数(/etc/security/limits.d/20-nproc.conf)
OOM-MetaSpace
GC算法、垃圾回收器
GC算法
- 引用计数
- 复制
- 标记清除
- 标记整理
垃圾回收器是垃圾回收算法的落地实现
GC垃圾回收器
目前为止没有完美的垃圾收集器出现,更没有万能的收集器,只是针对具体应用最适合的收集器,进行分代垃圾收集
- 四种垃圾收集器
- 一些术语
串行垃圾回收器(Serial)
为单线程环境设计且只是用一个线程进行垃圾回收
会暂停所有用户线程
不适合服务器环境
并行垃圾回收器(parallel)
多个垃圾收集器并行工作,此时用户线程是暂停的
适用于科学计算、大数据处理等若交互场景
并发垃圾回收器(CMS)
用户线程和垃圾收集器同事执行(不一定是并行,可能是交替执行),不需要停顿用户线程
互联网公司多用CMS垃圾收集器,适用于对相应时有要求的场景
G1垃圾收集器
G1垃圾回收器将堆内存分割成不同的无语然后并发对其进行垃圾回收
垃圾回收器查看、配置、理解
垃圾收集器查看
JVM参数:
java -XX:+PrintCommandLineFlags -version
默认的垃圾回收器有哪些
垃圾收集器的适用性
参数说明
JVM Server、Client模式说明
新生代
指新生代定垃圾回收器,会自动激活老年代对应的垃圾回收器
串行GC:Serial、Serial Coping
并行GC:ParNew
并行回收GC:Parallel、Parallel Scavenge
老年代
串行GC:Serial Old、Serial MSC
并行GC:Parallel Old、Parallel MSC
并发标记清楚GC(CMS)
初始标记(CMS initial mark)
只是标记一下GCRoots能直接关联的对象,速度很快,但仍然需要展厅所有的工作线程
并发标记(CMS concurrent mark)和用户线程一起进行
进行GCRoots跟踪过程,和用户线程一起工作,不需要暂停工作线程,主要标记过程,标记全部对象
重新标记(CMS remark)
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动(可能不需要回收的对象)的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
由于并发标记时,用户线程依然运行,因此正式清理前,再做修正及确认
并发清除(CMS concurrent sweep)和用户线程一起进行
清楚GCRoots不可达的对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户工作线程一起并发工作
所以总体上来看CMS垃圾收集器的内存回收线程和用户线程是一起并发执行的
优点
并发收集、地停顿
缺点
- 并发执行,对CPU资源压力大
由于是并发进行,CMS在手机与应用线程会同时并发工作会增加对堆内存的占用,也就是说CMS必须在老年代对内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间 - 采用的标记清除算法会导致大量的碎片
标记清除算法无法整理空间碎片,老年代空间会随着应用时间长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩 Compaction(默认0,即每次进行内存整理)来指定多少次CMS之后进行一次压缩的FullGC
验证示例
-
code
public class GCDemo { public static void main(String[] args) { System.out.println("===========GCDemo Test======="); try { String str = "abc"; while (true){ str +=str+new Random(777777777).nextInt()+new Random(888888888).nextInt(); str.intern(); } } catch (Exception e) { e.printStackTrace(); } } }
-
JVM 参数及指定垃圾回收器
- -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC (DefNew+Tenured)
- -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC (ParNew+Tenured)
- -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC (PSYounGen+ParOldGen)
- -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC (PSYounGen+ParOldGen)
-
Java8 不加垃圾回收器,默认就是USeParalleGC(效果同上)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC (PSYounGen+ParOldGen) -
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC (par new generation +concurrent)
-
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
-
(理论知道即可,实际Java8中已经被优化掉)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC (PSYounGen+ParNew)
-
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC (par new generation +concurrent)
-
同时指定新生代垃圾收集器、老生代垃圾收集器
繁琐配置仅供学习,生产上不会这样配置,因为 指定新生代、老生代垃圾收集器,对应会自动激活对应的垃圾收集器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC -XX:+UseParallelOldGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC -XX:+UseParallelOldGC
如何选择垃圾回收器
G1垃圾收集器
已有五个垃圾收集器的特点
- 新生代、老年代是各自独立且连续的内存块
- 年轻代收集使用单eden+S0+S1进行复制算法
- 老年代收集必须扫描整个老年代
- 都是尽可能少而且快速执行GC为原则设计
G1是什么
G1底层原理
Region区域垃圾收集器
最大好处是化整为零,避免全内存扫描,只需要按照区域进行扫描即可
G1垃圾收集步骤
G1常用配置
G1和CMS对比优势
-
G1不会产生内存碎片
-
是可以精确控制停顿、G1收集器是把整个堆(新生代,老生代)划分多个固定大小的区域
每次停顿允许停顿的时间内去收集垃圾最多的区域
生产服务器慢:诊断思路、性能评估
整机:top
top 精简版:uptime
CPU:vmstat
查看CPU核信息
mpstat -p -ALL 2
每个进程使用CPU的用量分解信息
pidstat -u 1 -p 进程编号
内存:free
- 查看额外
pidstat -p 进程编号 -r 采样间隔秒数
硬盘:df
查看硬盘剩余空间数 :df -h
磁盘IO:isstat
- 磁盘IO性能评估
-
查看额外
pidstat -d 采样间隔秒数 -p 进程编号
网络IO:ifstat
CUP占用过高分析思路
-
Linux和JDK命令一块分析
-
top命令找到CPU占比最高的进程
-
ps -ef 或者jps 进一步定位,得知是怎样的一个后台程序出问题
-
ps -mp 进程id -o THREAD,tid,time
-m :显示所有的线程
-p :进程使用cpu的时间
-o :该参数后是用户自定义格式
-
-
定位到具体线程或者代码
-
将需要的线程ID转换为16进制格式(英文小写格式)
printf “%x\n” 有问题的线程ID
-
jstack进程ID|grep tid(16进制线程ID英文小写格式) -A60
jstack 5101 |grep 13ee -A60
JDK自带的JVM监控性能分析工具
GitHub
限制搜索范围
- 关键字xxx in:name 项目名包含xxx
- 关键字xxx in:description 项目描述包含xxx
- 关键字xxx in:readme 项目的readme文件中包含xxx
- 组合使用 关键字xxx in:name,readme
stars或者fork数量关键字去查找
-
关键字XXX stars 通配符 :> 或者 :>= , … 区间
查找stars 数大于5000的 springboot项目 springboot stars :>=5000
查找forks 数大于5000的 springboot项目 springboot forks :>=5000
-
组合使用
查找fork在100到200 直接 并且stars在80 到100之间的springboot项目
springboot forks :100…200 stars :80…100
加强搜索-awesome
awesome系列一般是用来收集学习,工具、书籍类相关的项目
github高亮显示代码
地址#L行号
例:
//单行高亮
https://github.com/JeffLi1993/springboot-learning-example/blob/master/chapter-1-spring-boot-quickstart/src/main/java/demo/springboot/QuickStartApplication.java#L12
//多行高亮
https://github.com/JeffLi1993/springboot-learning-example/blob/master/chapter-1-spring-boot-quickstart/src/main/java/demo/springboot/QuickStartApplication.java#L12-L15
github项目内搜索
英文字母 t 按键
搜索某一地区用户
location:beijing language:java