目录
1 Java基础
2 Java集合
3 Java多线程
4 JVM
5 常见问题汇总参考资料
·《Java编程思想》
·《Java Web 技术内幕》
·《Java 并发编程实战》
3 Java多线程
3.1 基本线程机制
使用线程可以把占据时间长的程序中的任务放到后台去处理,程序的运行速度可能加快。进程是所有线程的集合,每一个线程是进程中的一条执行路径。
底层机制是切分CPU时间。
3.1.1 定义线程的方式
(1)实现Runable接口,实现run()方法。
class CreateRunnable implements Runnable {
@Override
public void run() {
for (inti = 0; i< 10; i++) {
System.out.println("i:" + i);
}
}
}
publicclass ThreadDemo2 {
public static void main(String[] args) {
System.out.println("-----多线程创建开始-----");
// 1.创建一个线程
CreateRunnable createThread = new CreateRunnable();
// 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
System.out.println("-----多线程创建启动-----");
Thread thread = new Thread(createThread);
thread.start();
System.out.println("-----多线程创建结束-----");
}
}
(2)继承Thread类,重写run()方法
public class ThreadDemo01 extends Thread{
public ThreadDemo01(){
//编写子类的构造方法,可缺省
}
public void run(){
//编写自己的线程代码
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args){
ThreadDemo01 threadDemo01 = new ThreadDemo01();
threadDemo01.setName("我是自定义的线程1");
threadDemo01.start();
System.out.println(Thread.currentThread().toString());
}
}
(3)通过Callable和FutureTask创建线程
a:创建Callable接口的实现类 ,并实现Call方法
b:创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
c:使用FutureTask对象作为Thread对象的target创建并启动线程
d:调用FutureTask对象的get()来获取子线程执行结束的返回值
public class ThreadDemo03 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Callable
FutureTask
oneTask = new FutureTask (oneCallable); Thread t = new Thread(oneTask);
System.out.println(Thread.currentThread().getName());
t.start();
}
}
class Tickets
implements Callable { //重写call方法
@Override
public Object call() throws Exception {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
return null;
}
}
(4)通过线程池创建线程
public class ThreadDemo05{
private static int POOL_NUM = 10; //线程池数量
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 0; i
{
RunnableThread thread = new RunnableThread();
//Thread.sleep(1000);
executorService.execute(thread);
}
//关闭线程池
executorService.shutdown();
}
}
class RunnableThread implements Runnable
{
@Override
public void run()
{
System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");
}
}
·总结:
前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果
后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中
· 注意事项
· 创建线程,优先选择Runable,接口可实现扩展。
· 开启线程需要调用start()方法,若直接调用run(),则为普通方法,在main函数中执行。
3.1.2 Thread类常用API
常用线程api方法
· start() 启动线程
· currentThread() 获取当前线程对象
· getID() 获取当前线程IDThread-编号 该编号从0开始
· getName() 获取当前线程名称
· sleep(long mill) 休眠线程static方法
· Stop() 停止线程
常用线程构造函数
· Thread()分配一个新的Thread 对象
· Thread(String name)分配一个新的Thread对象,具有指定的name正如其名。
· Thread(Runable r)分配一个新的Thread对象
· Thread(Runable r, String name)分配一个新的Thread对象
3.1.3 线程状态
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
(1)新建状态
当用new操作符创建一个线程时。
(2)就绪状态
当start()方法返回后,线程就处于就绪状态,等待CPU的线程调度。
(3)运行状态
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
(4)阻塞状态
线程运行过程中,可能由于各种原因进入阻塞状态:
· 线程通过调用sleep方法进入睡眠状态;
· 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
· 线程试图得到一个锁,而该锁正被其他线程持有;
· 线程在等待某个触发条件;
(5)死亡状态
· run方法正常退出而自然死亡
· 一个未捕获的异常终止了run方法而使线程猝死。
isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
3.2 线程无处不在
(1)每个Java应用程序都使用线程
· JVM(GC、终结操作)创建后台线程。
· 创建主线程执行main方法。
(2)需要注意线程安全的框架
· Timer类,需要确保TimerTask访问的对象本身是线程安全的。
· Servlet和JSP,需要保证ServletContext、HttpSession等容器中保存的对象线程安全。
· RMI 远程方法调用,确保被调用的对象线程安全
· Swing和AWT
3.3 多线程同步
3.3.1 加锁机制
3.3.1.1 内置锁
synchronized同步代码块
· 每个Java对象都可以用做一个实现同步的锁,与对象头的Mark Word有关。
· 由于每次只能有一个线程执行内置锁保护的代码块,因此同步代码块会以原子的方式执行。
动态高并发时为什么推荐重入锁而不是Synchronized? -
参考:深入理解synchronized底层原理,一篇文章就够了! - 知乎
共有三种使用方式:
(1)同步代码块
使用一个Java对象作为锁。
private Object mutex = new Object();// 自定义多线程同步锁
public void sale() {
synchronized (mutex) {
if (trainCount > 0) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
trainCount--;
}
}
}
(2)同步方法
使用this对象作为锁。
public synchronized void sale() {
if (trainCount > 0) {
try {
Thread.sleep(40);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
trainCount--;
}
}
(3)静态同步方法
当方法被static关键字修饰,锁使用class对象,即当前类的字节码文件
synchronized (ThreadTrain.class) {
System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
trainCount--;
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
3.3.1.2 可重入锁
(1)概念:某个线程试图获得一个已经由它自己持有的锁。重入锁的锁粒度是“线程”。
(2)重入锁为每个锁关联一个获取计数值和一个所有者线程。同一个线程每次获取锁,计数值加1,退出同步代码块时,计数值减1.当计数值为0时,这个锁被释放。
(3)synchronized是可重入锁。
3.3.2 显示锁
3.3.2.1 Lock与ReentrantLock
(1)Lock接口
提供了一种无条件的、可轮询的、定时的以及可以中断的锁获取操作。
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(Long timeout, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
(2)ReetrantLock 实现了Lock接口
· 提供了可重入锁
· 配合try/finally使用,必须在finally块中,使用unlock()方法释放锁。
(3)Lock接口的其他特性
· 轮询锁与定时锁
由tryLock()方法实现,可以避免死锁的问题。它会释放已经获得的锁,然后重新尝试获取所有锁
· 可中断锁
实现可取消的任务
· 非块结构的加锁
锁粒度会比synchronized细,可以是代码中的某几行。
3.3.2.2 公平性
(1)分类
· 公平锁
按照发出请求顺序获得锁。如果没有获取到锁,则直接加入队列尾部。
· 非公平锁(默认)
如果线程在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。尝试获取锁会进行两次,两次都失败则直接加入CLH队列尾部,后面流程和公平锁一致。
(2)特性
在激烈竞争时,非公平锁性能高于公平锁的性能。
(3)对比
· synchronized 只能是非公平锁
· ReetrantLock 两者都可
3.3.3 Volatile
(1)作用
是一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。
(2)禁止“重排序”
· 重排序:编译器和处理器以及运行时等可能对操作的执行顺序进行一些意想不到的调整。
· volatile声明的变量,不会将该变量上的操作与其他内存操作一起重排序。
(3)典型应用
用作状态标记或者条件判断。
(4)使用条件
· 只有单个线程更新变量值或者不依赖当前值。
· 访问时不需要加锁
(5)对比synchronized
· volatile不会造成阻塞
· synchronized可以保证原子性和可见性,Volatile只能保证可见性。
· volatile用来保证可见,synchronized用来同步
3.3.4 原子类
(1)java.util.concurrent.atomic包中的原子类
AtomicInteger、AtomicLong等。
private final AtomicLong count = new AtomicLong(0);
(2)注意事项
当有多个原子变量时,若涉及到相互依赖,需要保证在单个原子操作中更新所有相关的状态变量。
(3)原理分析
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提
供“⽐较并替换”的作⽤)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger类主要利用CAS(compare and swap)+ volatile和native方法(objectFieldOffset)来保证原子操作。
3.3.5 ThreadLocal
3.3.5.1 线程封闭
如果仅在单线程内访问数据,就不需同步共享的可变数据,称之为线程封闭。
3.3.5.2 ThreadLocal
(1)作用:根除对可变的单实例变量或全局变量共享。
(2)方式:
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
(3)相关方法:
· void set(Object value)设置当前线程的线程局部变量的值。
· public Object get()该方法返回当前线程所对应的线程局部变量。
· public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
· protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public static ThreadLocal
threadLocal = new ThreadLocal () { protected Integer initialValue() {
return 0;
};
};
(4)底层原理
利用ThreadLocal.ThreadLocalMap
3.3.6 活跃性与性能
(1)不良并发
可同时调用的用户请求数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。
(2)解决方案
· 缩小同步代码块的作用范围。 尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。
· 尽量避免不同的同步机制一起使用
· 当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台IO),一定不要持有锁。
3.4 线程三大特性
3.4.1 原子性
即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
3.4.2 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.4.3 有序性
程序执行的顺序按照代码的先后顺序执行。 程序在运行时,为了优化可能对执行顺序重排序。
3.5 Java内存模型(JMM)
(1)概念
· JMM决定一个线程对共享变量的写入时,能对另一个线程可见。
· JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
· 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
(2)模型图
当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的值刷新到主内存中,随后,线程B到主内存中去读取线程A更新后的值。
如果线程本地内存没有及时刷新到主内存中,则可能发生线程安全问题。
(3)意义
为Java程序员提供内存可见性保证。
3.6 多线程通讯
3.6.1 Object类中的相关方法
(1)相关方法
wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。这三个方法最终调用的都是JVM级的native方法。随着jvm运行平台的不同可能有些许差异。
这些方法可以配合synchronized关键字一起使用。
· 如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。
· 如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。
· 如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。
(2)注意事项
·notify和notifyAll的区别
·如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
·当有线程调用了对象的notifyAll()方法(唤醒所有 wait 线程)或notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
·优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
· Thead.sleep()方法与wait()方法区别
sleep()不会释放锁,wait()方法会释放锁。
3.6.2 Condition对象
3.6.2.1 条件队列
它使得一组线程(等待线程集合)能够通过某种方式等待特定的条件变为真。
2.6.2.2 Conditon是Lock的广义条件队列
(1)常用方法
void await() throws InterruptedException
void signal()
void signalAll()
(2)对比synchronized
· synchronized只能有一个关联的条件队列。
· Lock可以由任意数量的Condition对象
3.6.3 线程中的异常
异常不能跨线程捕捉,必须在线程内部处理。
3.6.4 守护线程
(1)当进程不存在或主线程停止,守护线程也会被停止。
(2)意义及应用场景
当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
如:Java垃圾回收线程就是一个典型的守护线程;内存资源或者线程的管理,但是非守护线程也可以
通过thread.setDaemon(true)设置线程为守护线程(后台线程)。
public class DaemonThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception e) {
}
System.out.println("我是子线程...");
}
}
});
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (Exception e) {
}
System.out.println("我是主线程");
}
System.out.println("主线程执行完毕!");
}
}
3.6.5 join()方法
join作用是让其他线程变为等待。
class JoinThread implements Runnable {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---i:" + i);
}
}
}
public class JoinThreadDemo {
public static void main(String[] args) {
JoinThread joinThread = new JoinThread();
Thread t1 = new Thread(joinThread);
Thread t2 = new Thread(joinThread);
t1.start();
t2.start();
try {
//其他线程变为等待状态,等t1线程执行完成之后才能执行。
t1.join();
} catch (Exception e) {
}
for (int i = 0; i < 100; i++) {
System.out.println("main ---i:" + i);
}
}
}
3.6.6 优先级
现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。
在JAVA线程中,通过一个int priority变量来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法,如getPriority(),setPriority(int)。
public class ThreadDemo4 {
public static void main(String[] args) {
PrioritytThread prioritytThread = new PrioritytThread();
Thread t1 = new Thread(prioritytThread);
Thread t2 = new Thread(prioritytThread);
t1.start();
// 注意设置了优先级, 不代表每次都一定会被执行。 只是CPU调度会有限分配
t1.setPriority(10);
t2.start();
}
}
3.6.7 yield()方法
(1)Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
(2)yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。
(3)实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
3.7 死锁
(1)概念
多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
同步中嵌套同步,导致锁无法释放。
(2)死锁条件
· 互斥:任意时刻资源只能由一个线程占用
· 请求与保持:对已获得的资源保持不放
· 不剥夺:获得资源后不能被其他线程抢占
· 循环等待: 若干进程之间形成一种头尾相接的循环等待资源关系。
破坏其中的任意一个条件即可:
· 一次申请完所有资源
· 主动释放资源
· 按序申请资源
3.8 线程池
3.8.1 概念
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。
3.8.2 优点
(1)降低资源消耗
(2)提高响应速度
(3)提高线程的可管理性
3.8.3 Executors工具类
(1)通过Executors.callable(Runable task)实现Runable对象和Callable对象的相互转换。
(2)ExecutorService的两个方法
通过Executors工具类创建ExecutorService对象。
· execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功
· submit()方法用于提交需要返回值的任务,线程池返回Future对象。
3.8.4 创建线程池的方式
3.8.4.1 传统方式:利用Executors类
下列四种方式的方法内部,实际上都是调用了ThreadPoolExecutor的构造方法。
(1)newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "---" + index);
}
});
}
(2)newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
因为线程池大小为3,每个任务输出index后sleep 1秒,所以每两秒打印3个数字。
// 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
final ExecutorService newCachedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
System.out.println("i:" + index);
}
});
}
(3)newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
(4)newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
}
}
});
}
3.8.4.2 阿里推荐:TheadPoolExecutor
(1)Executors框架的弊端
图3-5 Executors弊端
(2)TheadPoolExecutor构造方法
图3-6 ThreadPoolExecutor构造方法
(3)构造参数分析
/**
* ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(
int corePoolSize, //核心线程数,最小可以同时运行的线程数量
int maximumPoolSize, // 队列满容时,可以同时运行的最大线程数
long keepAliveTime, // 核心线程外的线程,等待销毁的时间
TimeUnit unit, // keepAliveTime的单位
BlockingQueue
workQueue, //超出核心线程数后,存放任务的队列 ThreadFactory threadFactory, // executor创建新线程使用
RejectedExecutionHandler handler // 饱和策略
){
//... ....
}
· 饱和策略:当前同时运行线程数量达到最大线程数量maximumPoolSize,并且队列满容时,对线程的淘汰策略。
·ThreadPoolExecutor.AbortPolicy (默认饱和策略)
抛出RejectedExecutionException异常拒绝新任务
· ThreadPoolExecutor.CallerRunsPolicy
调用执行自己的线程运行任务。不丢弃任何一个任务请求。
· ThreadPoolExecutor.DiscardPolicy
不处理新任务,直接丢弃
· ThreadPoolExecutor.DiscardOldestPolicy
丢弃最早的未处理的任务请求
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使⽤阿⾥巴巴推荐的创建线程池的⽅式
//通过ThreadPoolExecutor构造函数⾃定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接⼝)
Runnable worker = new MyRunnable("" + i);
//执⾏Runnable
executor.execute(worker);
}
//终⽌线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
(4)原理分析
3.9 悲观锁与乐观锁
3.9.1 悲观锁
(1)概念
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
(2)常见悲观锁
synchronized和ReentrantLock等独占锁是悲观锁思想的典型实现。
3.9.2 乐观锁
3.9.2.1 概念
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
3.9.2.2 常见应用场景
(1)乐观锁适用于多读的应用类型,这样可提高吞吐量。
(2)java.util.concurrent.atomic中的原子变量类是乐观锁的CAS实现的。
3.9.2.3 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
3.9.2.4 CAS算法
(1)概念
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
(2)原理
· 三个操作数:需要读写的内存值 V,进行比较的值 A,拟写入的新值 B。
· 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。(结合JMM内存模型理解)
(3)CAS算法缺点
· ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的"ABA"问题。
解决方法:在变量前面添加版本号,每次变量更新的时候都将版本号加1,比如juc的原子包中的AtomicStampedReference类。
· 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
· 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
3.10 AQS
3.10.1 概述
AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。
3.10.2 AQS原理
(1)AQS 核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
(2)源码分析
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过 protected 类型的getState,setState,compareAndSetState方法进行操作。
//返回同步状态的当前值
protected final int getState(){
return state;
}
// 设置同步状态的值
protected final void setState(int newState){
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(intexpect,intupdate){
return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}
(3)资源的共享方式
·Exclusive(独占)
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
·Share(共享)
多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
(4)公平锁和非公平锁
概念介绍:见3.3.1节
源码分析:深入锁和并发集合的核心——AQS(1)_只会写Bug的Java程序员的博客-CSDN博客
3.10.3 Semaphore(信号量)
Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
它默认构造AQS的state为permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。 如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。
执行 acquire 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300); // 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {
// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
3.10.4 CountDownLatch(倒计时器)
(1)概念
CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。
当线程使用countDown方法时,其实使用了tryReleaseShared方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。
当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。
(2)Demo
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i; threadPool.execute(() -> {
// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
}
3.10.5 CyclicBarrier(循环栅栏)
(1)概念
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CycliBarrier是基于 ReentrantLock(ReentrantLock也属于AQS同步器)和 Condition 的.
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
(2)Demo
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000); threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
(3)对比CountDownLatch
· CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。
· CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
4 JVM
4.1 Javac编译原理
4.1.1 概述
(1)作用:将Java的源代码转化为class字节码的。(编译器)
(2)将Java语言规范转化为Java虚拟机规范。
4.1.2 解析步骤
(1)读取源代码
按字节读取,一个字节一个字节读进来。
(2)词法分析
找出Java中定义的语法关键词,如if、else等,找出这些Token流。
(3)语法分析
对Token流进行语法分析,形成一个符合Java语言规范的抽象语法树。
(4)语义分析
形成一个注解过后的抽象语法树。
(5)字节码生成器
· 调用javac.jvm.Gen类遍历语法树,生成最终的Java字节码
· JVM的所有操作都是基于栈的
· 将字节码输出到.class文件中。
4.2 class文件结构
(1)class文件头
第一行:标识符,表示这个文件为标准的class文件
第二和第三行:Java最小版本和最大版本范围。
(2)常量池
(3)类信息
如final类、接口、抽象类等
(4)Fields和Methods定义
(5)类属性描述
· 可以使用javap命令生成class的结构信息到文件。 javap -verbose Message > result.txt (Message为Java类名,Message.java)
4.3 ClassLoader类加载器
4.3.1 作用
(1)将class文件加载到JVM中
(2)审查每个类应该由谁加载,它是一种父优先的等级加载机制
(3)将Class字节码重新解析成JVM统一要求的对象格式
4.3.2 类结构(方法分析)
ClassLoader是抽象类,其中的常用接口方法如下:
(1)defineClass(byte[], int, int)
将byte流解析成JVM能够识别的Class对象。
(2)findClass(String)
直接覆盖ClassLoader父类的findClass方法实现类的加载规则。
(3)resolveClass(Class>)
链接(Link),结合JVM运行时环境,准备执行该类或接口。
(4)loadClass(String)
在运行时加载指定类,获取类的Class对象。
Class> class = this.getClass().getClassLoader().loadClass(String)
(5)getResourceAsStream(xmlPath)
获取当前classpath
(6)getResource(..)
获取当前classpath
4.3.3 等级加载机制
4.3.3.1 JVM平台提供三层ClassLoader
· Bootstrap ClassLoader 启动类加载器,加载JVM自身工作需要的类
· Extension ClassLoader 扩展类加载器,加载java.ext.dirs目录下的类
· Application ClassLoader 应用程序类加载器,加载classpath下的类
4.3.3.2 双亲委派模型
(1)概念
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。
(2)每个类加载都有一个父类加载器
AppClassLoader的父类加载器为ExtClassLoader ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader。
类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定
(3)源码分析
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在ClassLoader的loadClass()方法中。
private final ClassLoader parent;
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {
//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
(4)优点
· 双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
· 保证了 Java 的核心 API 不被篡改
(5)如何不适用双亲委派模型
自定义加载器的话,需要继承 ClassLoader 。
如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
但是,如果想打破双亲委派模型则需要重写 loadClass() 方法
4.3.4 加载class文件过程
4.3.4.1 加载
· 通过全类名获取定义此类的二进制字节流
· 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
· 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
.class文件 ---》 findClass() ---》 defineClass()
4.3.4.2 链接(Link)
(1)验证
(2)准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
·这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
· 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
(3)解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
4.3.4.3 初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法
对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
(1)当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
(2)使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname("..."),newInstance()等等。 ,如果类没初始化,需要触发其初始化。
(3)初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
(4)当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
(5)MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, 就必须先使用findStaticVarHandle来初始化要调用的类。
4.3.5 常见加载类错误
(1)ClassNotFoundException
发生在显示加载类时
(2)NoClassDefFoundError
发生在隐式加载类时
(3)UnsatisfiedLinkError
解析native方法时
(4)ClassCastException
(5)ExceptionInInitializerError
4.4 内存
(1)分类
· 物理内存 RAM,调用操作系统接口访问。
· 虚拟内存,物理内存临时存储在磁盘文件
(2)划分
· 内核空间,操作系统程序逻辑
· 用户空间,每一次系统调用,都存在两个空间的内存切换
4.5 Java内存区域
还有很多叫法,如JVM内存结构,运行时数据区域。(一定要与Java内存模型JMM分开)。
(1)程序计数器(PC寄存器)
· 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
· 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
(2)Java虚拟机栈
Java 虚拟机栈是由一个个栈帧组成,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
(3)本地方法栈
本地方法栈则为虚拟机使用到的 Native 方法服务。
(4)堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
(5)方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法
4.6 Java对象创建过程
4.6.1 类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
4.6.2 分配内存
对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
共有两种内存分配方式,选择两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
(1)指针碰撞
· 使用场合:堆内存规整
· 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
· GC收集器:Serial、ParNew
(2)空闲列表
· 使用场合:堆内存不规整
· 原理:虚拟机会维护一个列表,该列表中会记录那些内存块是可用的,在分配的时候,找一块足够大的内存块来划分给对象实例,最后更新列表记录。
· GC收集器:CMS
在创建对象过程中,需要注意线程安全问题,采用下列两种方法保证线程安全:
(1)CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
(2)TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
4.6.3 初始化零值
虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.6.4 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
4.6.5 init方法
执行 new 指令之后会接着执行
4.7 Java内存分配和回收
Java显式的内存申请有静态内存分配和动态内存分配两种。
4.7.1 静态内存
(1)静态内存分配,指Java被编译时就已经能够确定需要的内存空间,当加载到内存时,一次性分配。
(2)基本数据类型、对象的引用。在栈上分配
(3)回收:在代码运行结束时回收。
4.7.2 动态内存
(1)动态内存分配,指在程序执行时才知道要分配的存储空间大小,而不是在编译器就确定大小。
(2)new等指令在堆上创建
(3)回收:检测垃圾回收
· 引用计数法:引用计数为0时,等待回收
· 可达性分析:为不可达集合时,等待回收
4.8 垃圾收集算法
4.8.1 标记-清除算法
首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
会产生大量不连续的碎片。
4.8.2 复制算法
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
4.8.3 标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4.8.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
(1)堆内存分配策略
· 对象优先在eden区分配
· 大对象直接进入老年代
· 长期存活的对象将进入老年代
(2)GC步骤
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。
(3)GC分类
· 部分收集 (Partial GC):
· 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
· 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
· 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
· 整堆收集 (Full GC):收集整个 Java 堆和方法区。关于系统频繁Full GC的排查:排查CPU100%(FUll GC导致)、GC执行步骤和排查频繁FULL GC
·常见问题1:频繁FULL GC的原因:
·代码中一次获取了大量的对象,导致内存溢出,此时可以通过 Eclipse 的 Mat 工具查看内存中有哪些对象比较多。
·内存占用不高,但是 Full GC 次数还是比较多,此时可能是显示的 System.gc() 调用导致 GC 次数过多,这可以通过添加 -XX:+DisableExplicitGC 来禁用 JVM 对显示 GC 的响应。
其他排查FGC问题的实践指南:
1. 清楚从程序角度,有哪些原因导致FGC?
· 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
· 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
· 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
· 程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
· 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
· JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。
2. 清楚排查问题时能使用哪些工具
· 公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。
· JDK的自带工具,包括jmap、jstat等常用命令:# 查看堆内存各区域的使用率以及GC情况jstat -gcutil -h20 pid 1000# 查看堆内存中的存活对象,并按空间排序jmap -histo pid | head -n20# dump堆内存文件jmap -dump:format=b,file=heap pid
· 可视化的堆内存分析工具:JVisualVM、MAT等
3. 排查指南
· 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
· 了解该时间点之前有没有程序上线、基础组件升级等情况。
· 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
· 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
· 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
· 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
·常见问题2:线上可能出现的导致系统缓慢的情况及处理:
简要的说,我们进行线上日志分析时,主要可以分为如下步骤:
①通过 top 命令查看 CPU 情况,如果 CPU 比较高,则通过 top -Hp 命令查看当前进程的各个线程运行情况。
找出 CPU 过高的线程之后,将其线程 id 转换为十六进制的表现形式,然后在 jstack 日志中查看该线程主要在进行的工作。
这里又分为两种情况:
· 如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗 CPU。
· 如果该线程是 VM Thread,则通过 jstat -gcutil 命令监控当前系统的 GC 状况。
然后通过 jmap dump:format=b,file= 导出系统当前的内存数据。
导出之后将内存情况放到 Eclipse 的 Mat 工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码。
②如果通过 top 命令看到 CPU 并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。
具体的可以根据具体情况分析:
· 如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过 jstack 查看堆栈信息,找到阻塞点。
· 如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出 jstack 日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程。
· 如果通过 jstack 可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。
4.9 常见垃圾收集器
4.9.1 Serial Collector
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程("Stop The World"),直到它收集结束。
优点:它简单而高效(与其他收集器的单线程相比)
缺点:用户体验差,有停顿时间
新生代采用复制算法,老年代采用标记-整理算法
4.9.2 Parallel Collector
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
它是许多运行在 Server 模式下的虚拟机的首要选择。
新生代采用复制算法,老年代采用标记-整理算法
并行,不是并发,用户使用仍然有停顿。
4.9.3 CMS
(1)CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
(2)步骤
CMS是“标记-清除”算法实现的。
· 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
· 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
· 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
· 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
(3)优缺点
·优点:并发收集、低停顿。
· 缺点:
·对 CPU 资源敏感;
·无法处理浮动垃圾;
·它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
4.9.4 G1收集器
(1)概念
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
(2)特点
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
·并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
·分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
·空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
·可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
(3)运行步骤
G1 收集器的运作大致分为以下几个步骤:
·初始标记
·并发标记
·最终标记
·筛选回收
**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
4.10 JVM参数
4.10.1 堆内存相关
4.10.1.1 显示指定堆内存
-Xms
-Xmx
·heap size表示要初始化内存的具体大小。
·unit表示要初始化内存的单位。单位为*“ g”*** (GB) 、*“ m”*(MB)、**“ k”(KB)。
-Xms2G -Xmx5G
4.10.1.2 显示指定新生代内存大小
默认情况下,YG 的最小大小为 1310MB,最大大小为无限制。一共有两种指定 新生代内存(Young Ceneration)大小的方法。
(1)通过-XX:NewSize和-XX:MaxNewSize指定
-XX:NewSize=
-XX:MaxNewSize=
-XX:NewSize=256m-XX:MaxNewSize=1024m
(2)通过-Xmn
-Xmn256m
还可以通过**-XX:NewRatio=**来设置新生代和老年代内存的比值。
4.10.1.3 显示指定永久代/元空间的大小
(1)JDK1.8之前
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
(2)JDK 1.8
方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小
4.10.2 垃圾收集相关
JVM具有四种类型的GC实现:
·串行垃圾收集器
·并行垃圾收集器
·CMS垃圾收集器
·G1垃圾收集器
可以使用以下参数声明这些实现:
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC
4.10.3 GC调优策略
4.10.3.1 GC 调优原则
多数的 Java 应用不需要在服务器上进行 GC 优化;
多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题;
在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合);
减少创建对象的数量;
减少使用全局变量和大对象;
GC 优化是到最后不得已才采用的手段;
在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
4.10.3.2 GC 调优目的
将转移到老年代的对象数量降低到最小;
减少 GC 的执行时间。
4.10.3.3 GC 调优策略
(1)策略 1
将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
(2)策略 2
大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。-XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。
(3)策略 3
合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
(4)策略 4
设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx 最大堆大小。
(5)策略5
注意: 如果满足下面的指标,则一般不需要进行 GC 优化:
MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。