juc/jvm基础笔记

第十二章 JUC

12.1 八锁
class Phone
{
 
 public  synchronized void sendSMS() throws Exception
 {
   
   System.out.println("------sendSMS");
 }
 public synchronized void sendEmail() throws Exception
 {
   System.out.println("------sendEmail");
 }
 
 public void getHello() 
 {
   System.out.println("------getHello");
 }
 
}
 
/**
 * 
 * @Description: 8锁
 * 
 1 标准访问,先打印短信还是邮件
 2 停4秒在短信方法内,先打印短信还是邮件
 3 新增普通的hello方法,是先打短信还是hello
 4 现在有两部手机,先打印短信还是邮件
 5 两个静态同步方法,1部手机,先打印短信还是邮件
 6 两个静态同步方法,2部手机,先打印短信还是邮件
 7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
 8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
 * ---------------------------------
 * 
 */
public class Lock_8
{
 public static void main(String[] args) throws Exception
 {
 
   Phone phone = new Phone();
   Phone phone2 = new Phone();
   
   new Thread(() -> {
    try {
     phone.sendSMS();
    } catch (Exception e) {
     e.printStackTrace();
    }
   }, "AA").start();
   
   Thread.sleep(100);
   
   new Thread(() -> {
    try {
     phone.sendEmail();
     //phone.getHello();
     //phone2.sendEmail();
    } catch (Exception e) {
     e.printStackTrace();
    }
   }, "BB").start();
 }
}

 synchronized是实现同步的基础:Java中的每一个对象都可以作为锁。
 具体表现为以下3种形式。
    作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
    作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
    作用于代码块,对括号里配置的对象加锁。


 *  1-2
 *   一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法, 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
 *  3-4
 *   加个普通方法后发现和同步锁无关,即都可以进行调用,普通方法不受同步锁的影响
 *   换成两个实例对象后,不是同一把锁了,情况立刻变化。
 *  5-6
 *  都换成静态同步方法后,情况又变化
 *  若是普通同步方法,new  this, 具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
 *  若是静态同步方法,static  Class ,唯一的一个模板
 *  7-8
 *  当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
 *  *
 *  *  所有的普通同步方法用的都是同一把锁——实例对象本身,,就是new出来的具体实例对象本身
 *  *  也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁,
 *  *  可是别的实例对象的普通同步方法因为跟该实例对象的普通同步方法用的是不同的锁,所以不用等待该实例对象已获取锁的普通
 *  *  同步方法释放锁就可以获取他们自己的锁。
 *  *
 *  *  所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
 *  *  具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的。
 *  *  但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁
 *  */
12.2 虚假唤醒
原因:在java多线程判断时,不能用if,程序出事出在了判断上面,
突然有一添加的线程进到if了,突然中断了交出控制权,之后唤醒的同一类线程进行了,导致本来的线程和原来的线程
没有进行验证,而是直接走下去了,加了两次,甚至多次
解决方法:
不能用if,改用while
12.3 callable
callable接口与runnable接口的区别?
 答:(1)是否有返回值
    (2)是否抛异常
    (3)落地方法不一样,一个是run,一个是call

使用:

class MyThread implements Runnable{

    @Override
    public void run() {

    }
}
class MyThread2 implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"come in callable");
        return 200;
    }
}


public class CallableDemo {
    public static void main(String[] args) throws Exception {
        //FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
        FutureTask<Integer> futureTask = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 1024;
        });
        FutureTask<Integer> futureTask2 = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+"  come in callable");
            TimeUnit.SECONDS.sleep(4);
            return 2048;
        });

        new Thread(futureTask,"zhang3").start();
        new Thread(futureTask2,"li4").start();

        //System.out.println(futureTask.get());
        //System.out.println(futureTask2.get());
        //1、一般放在程序后面,直接获取结果
        //2、只会计算结果一次

        while(!futureTask.isDone()){
            System.out.println("***wait");
        }
        System.out.println(futureTask.get());
        System.out.println(Thread.currentThread().getName()+" come over");
    }
}
12.4 ReentrantReadWriteLock读写锁
5个线程读,5个线程写入,操作同一个资源
* 1 不加锁                不可以,写的时候原子性被破坏
* 2 加ReentrantLock锁     写控制了,但是没有并发度,并发性能不好
* 3 加读写锁               规范写入,写唯一,读并发

* 读写锁
* 读 读 可共享
* 读 写 不共享
* 写 写 不共享
class MyCache //资源类
{
    volatile Map<String,String> map = new HashMap<>();
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void write(String key,String value)
    {
        readWriteLock.writeLock().lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 准备写入");
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"\t 完成写入-----:"+value);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void read(String key)
    {
        readWriteLock.readLock().lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t 准备读取");
            String result = map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t 完成读取-----:"+result);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.readLock().unlock();
        }
    }
}

public class ReadWriteLockDemo
{
    public static void main(String[] args)
    {
        MyCache myCache = new MyCache();

        for (int i = 1; i <=5; i++) {
            final int tmpI = i;
            new Thread(() -> {
                myCache.write(tmpI+"",tmpI+"");
            },String.valueOf(i)).start();
        }

        for (int i = 1; i <=5; i++) {
            final int tmpI = i;
            new Thread(() -> {
                myCache.read(tmpI+"");
            },String.valueOf(i)).start();
        }

    }
}
12.5 CountDownLatch
原理:
 * CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
 * 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
 * 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
/**
 * 
 * @Description:
 *  *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
 *  解释:6个同学陆续离开教室后值班同学才可以关门。
 *  main主线程必须要等前面6个线程完成全部工作后,自己才能开干 
 */
public class CountDownLatchDemo{
   public static void main(String[] args) throws InterruptedException{
         CountDownLatch countDownLatch = new CountDownLatch(6);
       
       for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
       {
          new Thread(() -> {
              System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
              countDownLatch.countDown();
          }, String.valueOf(i)).start();
       }
       countDownLatch.await();
       System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");    
   }
}
12.6 CyclicBarrier
原理
 * CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。
 * 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
 * 线程进入屏障通过CyclicBarrier的await()方法。
/**
 * CyclicBarrier
 * 集齐7颗龙珠就可以召唤神龙
 */
public class CyclicBarrierDemo{
  private static final int NUMBER = 7;
  
  public static void main(String[] args){
     //CyclicBarrier(int parties, Runnable barrierAction) 
     
     CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ;
     
     for (int i = 1; i <= 7; i++) {
       new Thread(() -> {
          try {
            System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 ");
            cyclicBarrier.await();
          } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
          }
       
       }, String.valueOf(i)).start();
     }
  }
}
12.7 Semaphore
原理:
 在信号量上我们定义两种操作:
 * acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
 * release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
 
 * 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
//模拟3个停车位
public class SemaphoreDemo{
  public static void main(String[] args){
     Semaphore semaphore = new Semaphore(3);//模拟3个停车位
     
     for (int i = 1; i <=6; i++) //模拟6部汽车
     {
       new Thread(() -> {
          try 
          {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
            TimeUnit.SECONDS.sleep(new Random().nextInt(5));
            System.out.println(Thread.currentThread().getName()+"\t------- 离开");
          } catch (InterruptedException e) {
            e.printStackTrace();
          }finally {
            semaphore.release();
          }
       }, String.valueOf(i)).start();
     }
     
  }
}
12.8 阻塞队列
种类分析:
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。

BlockingQueue核心方法

在这里插入图片描述

* 抛出异常	当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full,当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
* 特殊值	插入方法,成功ture失败false移除方法,成功返回出队列的元素,队列里没有就返回null
* 一直阻塞	当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出,当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
* 超时退出	当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
/**
 * 阻塞队列
 */
public class BlockingQueueDemo {
    public static void main(String[] args) throws InterruptedException {
//        List list = new ArrayList();

        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //第一组
//        System.out.println(blockingQueue.add("a"));
//        System.out.println(blockingQueue.add("b"));
//        System.out.println(blockingQueue.add("c"));
//        System.out.println(blockingQueue.element());

        //System.out.println(blockingQueue.add("x"));
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//    第二组
//        System.out.println(blockingQueue.offer("a"));
//        System.out.println(blockingQueue.offer("b"));
//        System.out.println(blockingQueue.offer("c"));
//        System.out.println(blockingQueue.offer("x"));
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//    第三组        
//         blockingQueue.put("a");
//         blockingQueue.put("b");
//         blockingQueue.put("c");
//         //blockingQueue.put("x");
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
        
//    第四组        
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
    }
}
12.9 线程池
线程池几个重要参数:
1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
4、unit:keepAliveTime的单位 
5、workQueue:任务队列,被提交但尚未被执行的任务
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
	拒绝策略:(均实现RejectedExecutionHandle接口)
	* AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
	* CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
	* DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
	* DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
原理:
1、在创建了线程池后,开始等待请求。
2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
  2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
  2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
    所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

第十三章 JVM

13.1 JVM规范

在这里插入图片描述

13.1.1 虚拟机栈
Java虚拟机栈是线程私有的,生命周期与线程相同。
Java虚拟机栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。
由于栈帧的进出栈,显而易见的带来了空间分配上的问题。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;如果虚拟机栈可以扩展,扩展时无法申请到足够的内存,将会抛出OutOfMemoryError,这种情况大多数是由于循环调用或递归调用带来的。

在这里插入图片描述

 	1.局部变量表:就是用来存储方法中的局部变量(包括在方法中声明的非静 态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
	2.操作数栈:想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
   3.指向运行时常量池的引用:因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
   4.方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

  由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。
13.1.2 本地方法栈
本地方法栈是线程私有的。
Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,但是通常我们指的一般为C或者C++,因此这个栈也有着C栈这一称号。一个不支持本地方法执行的JVM没有必要实现这个数据区域。本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java虚拟机栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。其大小也是可以设置为固定值或者动态增加,因此也会对应抛出StackOverflowError和OutOfMemoryError错误。
13.1.3 程序计数器
程序计数器是线程私有的,生命周期与线程相同。
	程序计数器(Program Counter Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。
	虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。
	由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。
	在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
	由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
13.1.4 方法区
方法区是线程共享区域,生命周期与JVM相同。
  方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
  在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
  在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
  在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。HotSpot虚拟机中JDK1.8开始使用元空间取代永久代。
① 方法区的特点:
  • 1、方法区是线程安全的,由于所有的线程都共享方法区,所以方法区里的数据访问必须被设计成线程安全的。
  • 2、方法区的大小不必是固定的,JVM可根据应用需要动态调整。
  • 3、方法区也可被垃圾收集,当某个类不在被使用(不可触及)时,JVM将卸载这个类,进行垃圾收集。
HotSpot 虚拟机,很多人愿意把方法区称为“永久代”(Permanent Generation)。本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机来说是不存在永久代的概念的。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
③ 方法区内存大小设置:
JDK8中永久代被移除了,取而代之的是元数据区。
不同的是元数据区是堆外直接内存,与永久代区不同,在不指定大小的情况下,虚拟机会耗尽所有可用的系统内存。
元数据区发生溢出,虚拟机一样抛出异常:java.lang.OutOfMemoryError Metaspace
13.1.5 堆
堆是线程共享区域,生命周期与JVM相同。
	对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
	Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代又可以细分为Eden空间、From Survivor空间(S0)和To Survivor空间(S1)。
	根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
	如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
	堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
	默认情况下,老年代 ( Old ) 与新生代 ( Young ) 的比例的值为 2 :1( 该值可以通过参数 –XX:NewRatio来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

在这里插入图片描述

通过-XX:NewRatio参数指定老年代/新生代的堆内存比例。在HotSpot虚拟机中,堆内存 = 新生代 + 老年代。如果 -XX:NewRatio = 4,表示老年代与新生代所占比值为 4 :1,新生代占整个堆内存的1/5,老年代占整个堆内存的4/5。在设置了-XX:MaxNewSize的情况下,-XX:NewRatio的值会被忽略,老年代的内存 = 堆内存 - 新生代内存。老年代的最大内存 = 堆内存 - 新生代最大内存。
13.1.6 新生代
程序新创建的对象都是从新生代分配内存(新生代中绝大部分对象都是“朝生夕死”),新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成。
可通过-Xmn参数来指定新生代的大小;也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。

	新生代的大小可以通过参数-XX:NewSize和-XX:MaxNewSize(-Xmn)指定JVM启动时分配的新生代内存大小和新生代最大内存大小;
	新生代中Eden和From Survivor空间(S0)或To Survivor空间(S1)的比例默认为8:1,也即 Eden:From Survivor:To Survivor = 8:1:1 (该比例可以通过参数-XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,From Survivor(S0) = To Survivor (S1) = 1/10 的新生代空间大小。 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor 区域是空闲着的。新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间
13.1.7 老年代
老年代主要存放比较大的对象或者JVM认为生命周期比较长的对象(经过几次新生代的垃圾回收后仍然存在),垃圾回收也相对没有那么频繁。老年代的垃圾回收(又称Major GC)通常使用“标记-清除”或“标记-整理”算法(依据垃圾回收器而定)。整个堆空间(新生代和老年代)的垃圾回收称为Full GC
13.1.8 小结:

在这里插入图片描述

13.2 分代垃圾回收过程
13.2.1 新生代垃圾回收过程
新生代的垃圾回收称为Minor GC,采用复制算法。新创建的对象优先进去新生代的Eden区,当Eden区满了之后再使用From Survivor区,当From Survivor也满了之后就进行Minor GC(新生代GC),将Eden和From Survivor中存活的对象复制到To Survivor,然后清空Eden和From Survivor,这个时候原来的From Survivor成了新的To Survivor,原来的To Survivor成了新的From Survivor。复制的时候,如果To Survivor无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。
如果创建的对象比较大,则直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

Minor GC触发的条件:
1、Eden区域满;
2、新创建的对象大小大于Eden区所剩空间大小;
13.2.2 老年代垃圾回收过程
长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

Full GC触发条件:
1、老年代所剩空间不足;
2、方法区空间不足;
3、调用System.gc( ) 方法;
4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
5、由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小;
13.3 对象“已死”的判定算法
由于程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,其占用的内存也是随线程生而生、随线程结束而回收。而Java堆和方法区则不同,在堆中几乎存在着所有对象,GC之前需要考虑哪些对象还活着不能回收,哪些对象已经死去可以回收。
有两种算法可以判定对象是否存活:
 (1)引用计数算法:给对象中添加一个引用计数器,每当一个地方应用了对象,计数器加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收。但是它很难解决两个对象之间相互循环引用的情况。
 (2)可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即对象到GC Roots不可达),则证明此对象已死、可回收。
 JVM是通过可达性分析算法来判定对象是否存活的
13.4 垃圾回收算法

JVM的垃圾回收算法有三种:标记-清除、复制、标记-整理

13.4.1 标记-清除
这种算法是最简单最直接的算法,也是其它算法的一些最初思路。标记清除算法其实就是对内存中的对象依次的进行判断,如果对象需要回收那么就打一个标记,如果对象仍然需要使用,那么就保留下来。这样经过一次扫描之后,所有的对象都会被筛选判断一次。紧接着会对内存中已经标记的对象依次进行清除。 这个算法比较简单粗暴,实现起来比较简单。
标记-清除算法有两个缺点:
(1). 效率问题:标记和清除需要两遍循环内存中的对象,标记和清除操作都是比较麻烦的工作,因此这种算法的效率不高。
(2). 空间问题:对于分配的内存来说,往往是连续的比较好,因为这样有利于分配大数据的对象。标记清除之后会产生大量不连续的内存碎片(类似于我们电脑的磁盘碎片),空间碎片太多导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作,也就是空间不足而导致频繁GC和性能下降。

在这里插入图片描述

13.4.2 复制
为了解决效率问题,出现了“复制”算法,他将可用内存按容量划分为大小相等的两块,每次只需要使用其中一块。当一块内存用完了,将还存活的对象复制到另一块上面,然后再把刚刚用完的内存空间一次清理掉。这样就解决了内存碎片问题,但是代价就是可以用内容就缩小为原来的一半。
算法使用了空间换取时间的思想,因此需要一块空白的区域作为内存对象要粘贴的区域。这无疑会造成一种浪费。只有在有效对象占据总回收内存是非常小的时候,这种算法的性价比才会达到最高。否则大量的复制操作所浪费的时间可能要远远大于空间换取时间得到的收益。因此这种算法在JVM中,也只被用来作为初级的对象回收。因为这时的有效对象比例最低,算法的性价比是最高的。
复制算法的优点:
1.如果垃圾对象较多的情况下,该算法效率比较高;
2.垃圾清理之后,内存不会出现碎片化;
复制算法的缺点:
1.并不适用在垃圾较少的情况下适用,例如老年代中;
2.分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低;

在这里插入图片描述

13.4.3 标记-整理
复制算法在对象存活率较高时就会进行频繁的复制操作,效率将降低。因此又有了标记-整理算法,标记过程同标记-清除算法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动,然后直接清理掉边界以外的内存。
复制算法需要一块额外的内存空间,用于存放幸存的内存对象。这无疑造成了内存的浪费。我们还可以在原有的标记清除算法的基础上,提出了优化方案。也就是标记到的可用对象整体向一侧移动,然后直接清除掉可用对象边界意外的内存。这样既解决了内存碎片的问题。又不需要原有的空间换时间的硬件浪费。由于老年代中的幸存对象较多,而且对象内存占用较大。这就使得一旦出现内存回收,需要被回收的对象并不多,碎片也就相对的比较少。所以不需要太多的复制和移动步骤。因此这种方法常常被应用到老年代中。
标记整理算法的缺点:
标记整理算法由于需要不断的移动对象到另外一侧,而这种不断的移动其实是非常不适合杂而多的小内存对象的。每次的移动和计算都是非常复杂的过程。因此在使用场景上,就注定限制了标记整理算法的使用不太适合频繁创建和回收对象的内存中。

在这里插入图片描述

13.5 垃圾收集器
垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)如下:

在这里插入图片描述

13.5.1 Serial收集器
Serial收集器是最基本、历史最久的收集器,曾是新生代手机的唯一选择。它是单线程的,只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它在收集的时候,必须暂停其它所有的用户线程,直到它结束,即“Stop the World”。停掉所有的用户线程,对很多应用来说难以接受。比如你在做一件事情,被别人强制停掉,用户的体验非常差
尽管如此,它仍然是虚拟机运行在client模式下的默认新生代收集器:简单而高效(这是与其它收集器的单个线程相比,因为没有线程切换的开销)。

在这里插入图片描述

13.5.2 ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同Serial收集器一样。
是许多运行在Server模式下的JVM中首选的新生代收集器,其中一个很重还要的原因就是除了Serial之外,只有他能和老年代的CMS收集器配合工作。

在这里插入图片描述

13.5.3Parallel Scavenge收集器
新生代收集器,并行的多线程收集器。它的目标是达到一个可控的吞吐量(就是CPU运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=行用户代码的时间/[行用户代码的时间+垃圾收集时间]),这样可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

在这里插入图片描述

13.5.4 Serial Old收集器
Serial 收集器的老年代版本,单线程,采用“标记-整理”算法,主要是给Client模式下的虚拟机使用。
另外还可以在Server模式下:
JDK 1.5之前的版本中与Parallel Scavenge 收集器搭配使用
可以作为CMS的后背方案,在CMS发生Concurrent Mode Failure是使用

在这里插入图片描述

13.5.5 Parallel Old收集器
Parallel Scavenge的老年代版本,多线程,采用“标记-整理”算法,JDK 1.6才出现。在此之前Parallel Scavenge只能同Serial Old搭配使用,由于Serial Old的性能较差导致Parallel Scavenge的优势发挥不出来。
Parallel Old收集器的出现,使“吞吐量优先”收集器终于有了名副其实的组合。在吞吐量和CPU敏感的场合,都可以使用Parallel Scavenge/Parallel Old组合

在这里插入图片描述

13.5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
基于“标记-清除”算法,并发收集、低停顿,运作过程复杂。CMS收集器的垃圾回收过程分为如下四步:
   1. 初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”;
   2. 并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行;
   3. 重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”;
   4. 并发清除:清除标记为可以回收对象,可以和用户线程并发执行;
由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

在这里插入图片描述

CMS是一款优秀的收集器,来分别看看它的优点和缺点。
优点:
1. 并发收集
2. 低停顿
缺点:
1. 对CPU资源非常敏感
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。
CMS的默认收集线程数量是 = (CPU数量 + 3) / 4;当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
2. 无法处理浮动垃圾(在并发清除时,用户线程新产生的垃圾叫浮动垃圾)
并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;
3. 产生大量内存碎片
CMS收集器基于"标记-清除"算法,清除后不进行整理压缩操作而产生大量不连续的内存碎片,这样会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
13.5.7 G1收集器
G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服务端应用的垃圾收集器(JDK9默认的收集器是G1)。它的使命是未来可以替换掉CMS收集器。
G1收集器特性:
   1. 并行与并发:G1收集器能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The World停顿时间。部分其他收集器原本需要暂停Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。【能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行】
   2. 分代收集:虽然G1收集器可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,熬过多次GC的旧对象以获取更好的收集效果。
   3. 空间整合:与CMS收集器的“标记-清除”算法不同,G1收集器整体上看采用“标记-整理“算法,局部看采用“复制”算法(两个Region之间),不会有内存碎片,不会因为大对象找不到足够的连续空间而提前触发GC,这点优于CMS收集器;
   4. 可预测的停顿:这是G1收集器相对于CMS收集器的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。

为什么能做到可预测的停顿 ?
是因为可以有计划的避免在整个Java堆中进行全区域的垃圾收集。
G1收集器将内存分大小相等的独立区域(Region),新生代和老年代概念保留,但是已经不再物理隔离。
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
这就保证了在有限的时间内可以获取尽可能高的收集效率。

对象被其他Region的对象引用了怎么办?
判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器,也存在这样的问题(而G1更突出):新生代回收的时候不得不扫描老年代?
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:
每个Region都有一个对应的Remembered Set;
每次Reference类型数据写操作时,都会产生一个Write Barrier 暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象);
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
进行垃圾收集时,在GC根节点的枚举范围加入 Remembered Set ,就可以保证不进行全局扫描,也不会有遗漏。
不计算维护Remembered Set的操作,G1收集器回收过程可以分为四个步骤(与CMS较为相似):
   1. 初始标记:仅仅标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象,需要“Stop The World”;
   2. 并发标记:从GC Roots开始进行可达性分析,找出存活对象,耗时长,可与用户线程并发执行;
   3. 最终标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录。并发标记时虚拟机将对象变化记录在线程Remember Set Logs里面,最终标记阶段将Remember Set Logs整合到Remember Set中,比初始标记时间长但远比并发标记时间短,需要“Stop The World”;
   4. 筛选回收:首先对各个Region的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来定制回收计划,最后按计划回收一些价值高的Region中垃圾对象。回收时采用复制算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;可以并发进行,降低停顿时间,并增加吞吐量;

在这里插入图片描述

G1收集器相对于CMS收集器的区别在:
	G1在压缩空间方面有优势
	G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
	Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
	G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
	G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(Stop The World)的时候做
	G1会在整个堆(新生代和老年代)中使用、而CMS只能在老年代中使用

就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:
	服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
	应用在运行过程中会产生大量内存碎片、需要经常压缩空间
	想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值