JavaNote:Java 集合、Java 并发、JVM

文章目录

Reference
Java SE 8
JavaGuide
[尚硅谷]


一、Java 集合框架

略,类图

二、Java 并发编程

在这里插入图片描述

java.util.concurrent.atomic
java.util.concurrent.locks


2.1 synchronized 与 volatile
  • synchronized 与 volatile (JavaGuide)
    • 区别
      • volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块;
      • volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好;
      • volatile 关键字能保证数据的可见性,但不能保证数据的原子性,而synchronized 关键字两者都能保证;
        • 对一个 volatile 变量的写操作, Happens-Before 于后续对这个volatile变量的读操作
      • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性;
    • 单例模式的双重校验锁( DCL,即 double-checked locking )实现,线程安全
      • volatile 禁止 JVM 的指令重排
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       // 先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            // 类对象加锁
            synchronized (Singleton.class) {
            	// 再次判断对象是否已经实例过
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

2.2 线程创建
  • 线程创建
    • 继承 Thread 类
    • 实现 Runable 接口
    • 实现 Callable 接口

Runable 与 Callable 的异同:

    • 均为接口
      • 均用于编写多线程程序
      • 均采用Thread.start()启动线程
    • Runnable 没有返回值,而 Callable 有返回值(泛型);
      • Callalble 接口支持返回执行结果,需要调用 FutureTask.get() 得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
    • Callable 接口的 call() 方法允许抛出异常,而 Runnable的run() 方法异常只能在内部消化,不能往上继续抛;

2.3 Future & Future Task
  • Future 接口
    在这里插入图片描述
  • FutureTask:Future 接口的基本实现类
    在这里插入图片描述
  • Callable 与 FutureTask 的使用
    在这里插入图片描述
    在这里插入图片描述

2.4 线程池创建
  • 线程池创建

    • Executors 创建
      • newFixedThreadPool():创建一个具有固定线程数的线程池
      • newCachedThreadPool():创建一个可缓存的线程池,调用 execute 将复用以前构造的线程(如果线程可用);如果没有可用线程则创建新的线程并加入到池中
      • newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行
      • newScheduledThreadPool():创建一个支持定时及周期性地执行任务的线程池,一般情况下可用来替代Timer类。
    • 使用 ThreadPoolExecutor 这个类自定义创建线程池
    public ThreadPoolExecutor(
    						  // the number of threads to keep in the pool --> 保留在线程池的线程数(核心线程数)
    						  int corePoolSize, 
    						  // the maximum number of threads to allow in the pool ---> 线程池允许的最大线程数
                              int maximumPoolSize, 
                              // when the number of threads is greater than the core, 
                              // this is the maximum time that excess idle threads will wait for new tasks before terminating 
                              // --> 当线程数大于corePoolSize, 多余的空闲的线程的存活时间
                              long keepAliveTime, 
                              // the time unit for the argument {keepAliveTime} --> 存活时间单位
                              TimeUnit unit, 
                              // the queue to use for holding tasks before they are executed --> 任务被执行前使用的队列
                              BlockingQueue<Runnable> workQueue, 
                              // the factory to use when the executor creates a new thread --> 创建线程使用的线程工厂
                              ThreadFactory threadFactory, 
                              // the handler to use when execution is blocked because the thread bounds and queue capacities are reached 
                              // --> 由于线程池的maximumPoolSize个线程均在忙且工作队列满了导致的执行被阻塞的处理方式
                              RejectedExecutionHandler handler 
                              ){}
    
    • 使用 Executors 创建线程的弊端
      • newFixedThreadPool() 与 newSingleThreadExecutor() 的工作队列长度允许为 Integer.MAX_VALUE,可能导致OOM
      • newCachedThreadPool() 与 newScheduledThreadPool() 允许的创建线程数量为 Integer.MAX_VALUE,可能导致OOM
    • ThreadPoolExecutor 的 handler 参数
      在这里插入图片描述
      • CallerRunsPolicy:提交任务的线程自己去执行该任务
      • AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException
        • 注意 catch 拒绝任务时抛出的异常
      • DiscardPolicy:直接丢弃任务,没有任何异常抛出
      • DiscardOldestPolicy:丢弃最老的任务,把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
  • 使用线程池的好处(《Java 并发编程的艺术》)

    • 降低资源消耗:
      • 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
    • 提高响应速度:
      • 当任务到达时,任务可以不需要的等到线程创建就能立即执行
    • 提高线程的可管理性:
      • 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

2.5 Atomic 原子类
  • Atomic 原子类:具有原子/原子操作特征的类,操作不可中断
    • 基本类型(使用原子的方式更新基本类型)
      • AtomicInteger:整形原子类
      • AtomicLong:长整型原子类
      • AtomicBoolean:布尔型原子类
    • 数组类型(使用原子的方式更新数组里的某个元素)
      • AtomicIntegerArray:整形数组原子类
      • AtomicLongArray:长整形数组原子类
      • AtomicReferenceArray:引用类型数组原子类
    • 引用类型
      • AtomicReference:引用类型原子类
      • AtomicStampedReference:原子更新带有版本号的引用类型
        • 该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
      • AtomicMarkableReference :原子更新带有标记位的引用类型
    • 对象的属性修改类型
      • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
      • AtomicLongFieldUpdater:原子更新长整形字段的更新器
      • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
    • 原子累加器类型
      • DoubleAccumulator
      • DoubleAdder
      • LongAccumulator
      • LongAdder

2.6 ThreadLocal
  • ThreadLocal 使用
    • 如果创建了一个ThreadLocal变量,那么修改这个变量的每个线程都会有这个变量的本地副本(线程本地存储,TLS);
package multithread;

/**
 * @author 320983
 */

/**
 * @author 32098
 */
public class ThreadLocalTest {
    static class ClassWithThreadLocalVar{
        static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"abc");

        static String get(){
            return threadLocal.get();
        }
    }

    static class ThreadClass implements Runnable{
        @Override
        public void run() {
            System.out.println("Before modify:"+ClassWithThreadLocalVar.get());
            ThreadLocalTest.ClassWithThreadLocalVar.threadLocal.set(Thread.currentThread().getName());
            System.out.println("After modify:"+ClassWithThreadLocalVar.get());
        }
    }

    public static void main(String[] args) {
        Thread tA = new Thread(new ThreadClass());
        Thread tB = new Thread(new ThreadClass());
        tA.start();
        tB.start();
    }
}

在这里插入图片描述

  • ThreadLocal 原理:略,如果想了解可阅读源码
  • ThreadLocal 内存泄漏
    • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。(JavaGuide)

2.7 AQS
  • AQS 介绍

    • AQS 提供了一个实现阻塞锁与依赖于FIFO等待队列的相关同步器,如信号量、事件等
    • 对于大多数依赖于一个单一的原子 int 值来表示状态的同步器来说,AQS 被设计成一个有用的 basis(大概意思是 通过继承 AQS 可以简化同步器的设计)
    • AQS 子类可以维护其他状态字段,但是对于同步,只有使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法操作的原子更新的 int 值被跟踪
    • AQS 支持默认的独占模式和共享模式,或者两者都支持:Usually, AQS implementation subclasses support only one of these modes, but both can come into play for example in a ReadWriteLock. Subclasses that support only exclusive or only shared modes need not define the methods supporting the unused mode.
      • tryAcquire & tryRelease for exclusive modes
      • tryAcquireShared & tryReleaseShared for shared modes
    • 静态内部类 ConditionObject
      在这里插入图片描述
      • ConditionObject class that can be used as a Condition implementation by subclasses supporting exclusive mode for which method isHeldExclusively() reports whether synchronization is exclusively held with respect to the current thread.
  • AQS 可重写的方法(不重写就调用的话抛出 UnsupportedOperation 异常)
    在这里插入图片描述

    • protected boolean tryAcquire(int arg)
      • 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态
    • protected boolean tryRelease(int arg)
      • 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
    • protected int tryAcquireshared(int arg)
      • 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
    • protected boolean tryReleaseShared(int arg)
      • 共享式释放同步状态
    • protected boolean isHeldExclusively()
      • 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
  • AQS 提供的模板方法(同步组件通过 AQS 提供的模板方法实现自己的同步语义( AQS 模板方法又依赖于被 AQS 的子类所重写的方法),见下文 AQS 使用案例(Java SE 8))

    • void acquire(int arg)
      • 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
    • void acquireInterruptibly(int arg)
      • 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回
    • boolean tryAcquireNanos(int arg, long nanos)
      • 在acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回 true
    • void acquireShared(int arg)
      • 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
    • boolean tryAcquireSharedNanos(int arg, long nanos)
      • 在 acquireSharedInterruptibly(int arg) 基础上增加了超时限制
    • boolean release(int arg)
      • 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
    • boolean releaseShared(int arg)
      • 共享式的释放同步状态
    • Collection<Thread> getQueuedThreads()
      • 获取等待在同步队列上的线程集合
  • AQS 使用案例(Java SE 8):一个使用 0 代表非锁定状态、1代表锁定状态的不可冲入互斥锁

package cn.edu.neu.aqs;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author JavaSE 8 doc
 */
public class Mutex implements Lock, java.io.Serializable {

    // Our internal helper class
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        @Override
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                // 设置获取锁的线程为当前线程, 即改变AQS的成员变量exclusiveOwnerThread为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() { return new ConditionObject(); }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }

        /**
         * self add method to official site example
         * @return lock owner thread
         */
        protected Thread getOwnerThread(){
            return getExclusiveOwnerThread();
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();

    @Override
    public void lock()                { sync.acquire(1); }
    @Override
    public boolean tryLock()          { return sync.tryAcquire(1); }
    @Override
    public void unlock()              { sync.release(1); }
    @Override
    public Condition newCondition()   { return sync.newCondition(); }
    public boolean isLocked()         { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    /**
     * self add method to official site example for test
     */
    public String getOwnerThread(){
        return sync.getOwnerThread().getName();
    }

    /**
     * self add method to official site example for test
     */
    public List<String> getQueuedThreadNames(){
        List<String> threadNames = new ArrayList<>();
        for (Thread queuedThread : sync.getQueuedThreads()) {
            threadNames.add(queuedThread.getName());
        }
        return threadNames;
    }
}

package cn.edu.neu.aqs;

/**
 * @author 32098
 */
public class MutexTest {
    public static void main(String[] args) {
        Mutex mutex = new Mutex();
        for(int i=0; i<10; i++){
            Thread thread = new Thread(() -> {
                // 官方文档建议的锁的使用形式:
                mutex.lock();
                try {
                    System.out.println("Mutex Owner Thread:"+mutex.getOwnerThread());
                    System.out.print("QueuedThreads:");
                    mutex.getQueuedThreadNames().forEach(threadName-> System.out.print(threadName+"\t"));
                    System.out.println();
                } finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}

在这里插入图片描述


2.8 Lock
  • Lock 接口
    • Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.(与使用同步方法和语句获得的锁操作相比,Lock 的实现类提供了更广泛的锁操作,它们允许更灵活的结构,可能具有完全不同的属性,并可能支持多个关联的Condition对象)
    • Lock 接口的方法
      在这里插入图片描述
  • Lock 接口的实现类 ReentrantLock

三、JVM

在这里插入图片描述
在这里插入图片描述

3.1 Java 运行时数据区
3.1.0 Java 运行时数据区组成

  Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中一些会随着虚拟机启动而创建、随着虚拟机退出而销毁;另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始而创建、随着线程的结束而销毁。

  • JDK 1.8 之前
  • JDK 1.8

    注:直接内存非运行时数据区的一部分
    图源:JavaGuide
3.1.1 程序计数器(PC 寄存器)——线程私有
  • JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟
  • 程序计数器用来存储指向下一条指令的地址,它是一块很小的内存空间(几乎可以忽略不记),也是速度最快的存储区域
  • 程序计数器是唯一一个在Java虚拟机规范中没有规定任何 OOM 情况的区域
  • 任何一个 Java 线程都有它自己的程序计数器,即程序计数器是线程私有的(其生命周期与线程的生命周期保持一致)
  • 程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 程序计数器常见问题
    • 为什么要使用PC寄存器存储字节码指令地址?
      • 因为 CPU 需要不停的切换各个线程,这时候切换回来以后,就得知道应该从哪开始继续执行,PC寄存器存储字节码指令地址就指示了字节码解释器下一条应该执行什么样的字节码指令
      • 使用PC寄存器存储字节码指令地址使得线程切换回来后能恢复到正确的执行位置
    • PC寄存器为什么被设定为私有的?
      • 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令,而这样必然导致经常性的中断与恢复,那么就需要每个线程创建后产生自己的程序计数器以保证分毫无差(因为私有的程序计数器在各个线程之间互不影响)
3.1.2 虚拟机栈——线程私有
3.1.2.1 概述
  • Java 虚拟机栈介绍

    • 出现背景?

      • Java 由于跨平台性的设计,其指令架构都是根据栈来设计的(由于不同平台CPU架构不同,所以指令架构不能设计为基于寄存器的)

      栈式指令架构:优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令

    • 什么是 Java 虚拟机栈?

      • 每个 Java 线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),每个栈帧对应着一次 Java 方法调用(每一次方法调用都会有一个对应的栈帧被压入 Java 栈,每一个方法调用结束即 return 后,都会有一个栈帧被弹出,抛出异常也会弹出栈帧)
  • 虚拟机栈是一种快速有效的存储分配方式,访问速度仅次于程序计数器

  • 虚拟机栈不存在垃圾回收问题,Java 对 虚拟机栈 的直接操作只有进栈与出栈

  • 如何设置 Java 虚拟机栈的内存大小(栈内存的大小决定了方法调用的深度)?

    • -Xss:设置线程的最大栈空间
  • Java 虚拟机栈可能出现错误有什么?

    • StackOverFlowError:如果采用固定大小的 Java 虚拟机栈,那么线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。
    • OutOfMemoryError:如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常

    注:Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的

  • 虚拟机栈的存储单位:栈帧(Stack Frame)

    • 栈帧是一个内存区块,线程上正在执行的每个方法都各自对应一个栈帧(一一对应)
    • 栈帧组成(栈帧的大小主要是由局部变量表和操作数栈决定的)
      • 局部变量表(Local Variables)
      • 操作数栈(operand Stack)
      • 动态链接(DynamicLinking)
      • 方法返回地址(Return Address)
      • 附加信息:与 Java 虚拟机实现相关的一些附加信息,如对程序调试提供支持的信息
    • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame)(与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类)
      • 如果在方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
    • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
3.1.2.2 局部变量表
  • 局部变量表定义为一个数字数组(局部变量数组),主要用于存储方法参数定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型
  • 局部变量表建立在线程的栈上,是线程的私有数据,不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中,方法运行期间是不会改变局部变量表的大小的
  • 局部变量表与栈、栈帧
    • 方法嵌套调用的次数由栈的大小决定,一般来说,栈越大方法嵌套调用次数越多
    • 对一个方法而言,若它的参数和局部变量越多,那么它的栈帧就会越大(局部变量表膨胀导致),以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
  • 局部变量表中的变量只在当前方法调用中有效
    • 在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程
    • 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
  • 局部变量表与 Slot
    • 局部变量表,最基本的存储单元是 Slot (变量槽)
    • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量
    • 在局部变量表里,32位以内的类型只占用一个 slot (包括returnAddress类型),64位的类型(long和double)占用两个 slot
      • byte、short、char 在存储前被转换为int,boolean也被转换为int(0表示false,非0表示true)
    • JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
    • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上
      • 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可(比如:访问 long 或 double 类型变量)
    • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序继续排列
    • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
  • 其它说明
    • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
    • 局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
package cn.edu.neu.jvm.runtime_area;

import java.util.Date;

/**
 * @author shkstart
 */
public class LocalVariablesTest {
    private int count = 0;

    public LocalVariablesTest(){
        this.count = 1;
    }

    public static void testStatic(){
        LocalVariablesTest test = new LocalVariablesTest();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        // this变量不存在于当前方法的局部变量表中!!
        // System.out.println(this.count);
    }

    public void testA() {
        Date date = new Date();
        String name = "com";
        testB(date, name);
        System.out.println(date + name);
    }

    public String testB(Date date, String name) {
        date = null;
        name = "shk";
        // 占据两个 slot
        double weight = 130.5;
        char ch = 'm';
        return date + name;
    }

    public void testC() {
        this.count++;
    }

    public void testD() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        // 变量 c 使用之前已经销毁的变量 b 占据的 slot 的位置
        int c = a + 1;
    }

    public void testE(){
        // 局部变量在使用前,必须要进行显式赋值; 否则,编译不通过
        int num;
        // System.out.println(num); //错误信息:Variable 'num' might not have been initialized
    }

    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        test.testA();
    }
}
3.1.2.3 操作数栈
  • 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈,如求和操作
public static void main(String[] args) {
	int a = 12;
	int b = 13;
	int c = a + b;
	System.out.println(c);
}
 0 bipush 12
 2 istore_1
 3 bipush 13
 5 istore_2
 6 iload_1
 7 iload_2
 8 iadd
 9 istore_3
10 getstatic #2 <java/lang/System.out>
13 iload_3
14 invokevirtual #3 <java/io/PrintStream.println>
17 return
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

  • 操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的

  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为max_stack 的值

  • 栈中的任何一个元素都是可以任意的Java数据类型

    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证

  • i++与++i的区别

public void add(){
    // 第1类问题:
    int i1 = 10;
    i1++;

    int i2 = 10;
    ++i2;

    // 第2类问题:
    int i3 = 10;
    int i4 = i3++;

    int i5 = 10;
    int i6 = ++i5;

    // 第3类问题:
    int i7 = 10;
    i7 = i7++;

    int i8 = 10;
    i8 = ++i8;

    // 第4类问题:
    int i9 = 10;
    int i10 = i9++ + ++i9;
}
  • 栈顶缓存(Top Of Stack Cashing)技术
    • 由于操作数存储在内存中,因此频繁地执行内存读/写操作必然会影响执行速度,为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
3.1.2.4 动态链接
  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),如 invokedynamic 指令
  • 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里
    • 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
  • 常量池的作用,就是为了提供一些符号和常量,便于指令的识别
3.1.2.5 方法返回地址

  方法返回地址存放的是调用者的 PC 寄存器的值。
一个方法的结束,有两种方式:

  • 正常执行完成
  • 出现未处理的异常,非正常退出
      无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
3.1.3 本地方法栈——线程私有
3.1.3.1 本地方法(Native Method)介绍
  • 本地方法是其实现由非Java语言实现的方法
  • 在定义一个Native Method时,并不提供实现体,因为其实现体是由非 Java 语言在外面实现的
  • 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序
  • 标识符 Native 用于定义本地方法,它可以与其它 Java 标识符连用,但是 abstract 除外
3.1.3.2 使用本地方法的原因

尽管 Java 使用起来非常方便,然而有些层次的任务用 Java 实现起来不容易,或者我们对程序的效率很在意时,就需要使用本地方法

  • 与Java环境的交互
    • 本地方法存在的主要原因是 Java 应用有时需要与 Java 外面的环境交互(操作系统、某些硬件等),本地方法为我们提供了一个非常简洁的接口使得我们无需去了解 Java 应用之外繁琐的细节
  • 与操作系统的交互
    • JVM 支持着 Java 语言本身和运行时库,它是 Java 程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成,由于 JVM 不是一个完整的系统,它经常依赖于底层系统的支持。
    • 通过使用本地方法,我们得以用Java实现了的jre与底层系统的交互,并且如果要使用一些 Java 语言本身没有提供封装的操作系统的特性时我们也需要使用本地方法
3.1.3.3 本地方法栈(Native Method Stack)介绍
  • 本地方法栈允许被实现成固定的或者是可动态扩展的内存大小
    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 StackOverflowError 异常。
    • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常
  • 本地方法栈中登记着 native 方法,在 Execution Engine 执行时加载本地方法库,当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界,和虚拟机拥有同样的权限
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 本地方法可以直接从本地内存的堆中分配任意数量的内存
    • 本地方法甚至可以直接使用本地处理器中的寄存器
  1. 不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等,如果JVM产品不打算支持 native 方法,也可以无需实现本地方法栈
  2. 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一
3.1.4 堆——线程共享
3.1.4.1 堆的核心概述
  • 核心概述
    • 一个JVM实例只存在一个堆内存,堆也是 Java 内存管理的核心区域
    • Java 堆区在 JVM 启动的时候即被创建,其空间大小也就确定了,Java 堆是JVM管理的最大的一块内存空间,其大小是可以调节的
    • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
    • 所有的线程共享Java堆,Java 堆还可以划分出线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)
    • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
    • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
    • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
  • 内存细分
    • Java 7:堆内存逻辑上分为三部分:新生区+养老区+永久区
      • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
      • Tenure generation space 养老区 Old/Tenure
      • Permanent Space 永久区 Perm
    • Java 8:堆内存逻辑上分为三部分:新生区+养老区+元空间
      • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
      • Tenure generation space 养老区 Old/Tenure
      • Meta Space 元空间 Meta
        在这里插入图片描述
3.1.4.2 堆内存的大小设置与OOM
  • 堆空间(年轻代+老年代)大小的设置
    • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,我们可以通过选项"-Xmx"和"-Xms"来进行设置。
      • “-Xms” 用于表示堆区的起始内存,等价于-XX:InitialHeapSize
      • “-Xmx” 用于表示堆区的最大内存,等价于-XX:MaxHeapSize
    • 一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常
    • 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
    • 默认情况下
      • 初始内存大小:物理电脑内存大小 / 64
      • 最大内存大小:物理电脑内存大小 / 4
package cn.edu.neu.jvm.runtime_area;

/**
 * 1. 设置堆空间大小的参数
 * -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
 *      -X 是jvm的运行参数
 *      ms 是memory start
 * -Xmx 用来设置堆空间(年轻代+老年代)的最大内存大小
 *
 * 2. 默认堆空间的大小
 *    初始内存大小:物理电脑内存大小 / 64
 *             最大内存大小:物理电脑内存大小 / 4
 * 3. 手动设置:-Xms600m -Xmx600m
 *     开发中建议将初始堆内存和最大的堆内存设置成相同的值。
 *
 * 4. 查看设置的参数
 *      方式一:
 *          jps
 *          jstat -gc 进程id
 *      方式二:-XX:+PrintGCDetails
 *
 * @author shkstart  shkstart@126.com
 *
 */
public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");

        System.out.println("System memory:" + initialMemory * 64.0 / 1024 + "G");
        System.out.println("System memory:" + maxMemory * 4.0 / 1024 + "G");

//        try {
//            Thread.sleep(1000000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    }
}

在这里插入图片描述

57 57.5

  • OutofMemoryError 举例
package cn.edu.neu.jvm.runtime_area;

import java.util.ArrayList;
import java.util.Random;

/**
 * -Xms600m -Xmx600m
 * @author shkstart  shkstart@126.com
 */
public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture{
    private byte[] pixels;

    public Picture(int length) {
        this.pixels = new byte[length];
    }
}

在这里插入图片描述

3.1.4.3 年轻代与老年代
  • 存储在 JVM 中的 Java 对象可以被划分为两类:

    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致
  • Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)

    • 其中年轻代又可以划分为Eden空间(伊甸园区)、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
      在这里插入图片描述
  • 配置新生代与老年代在堆结构的占比:

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
  • 在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1

    • 开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例,比如-xx:SurvivorRatio=8
  • 几乎所有的Java对象都是在Eden区被new出来的,绝大部分的Java对象的销毁都在新生代进行的。

IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的
可以使用选项"-Xmn"设置新生代最大内存大小,这个参数一般使用默认值

package cn.edu.neu.jvm.runtime_area.heap;

/**
 * -Xms600m -Xmx600m
 *
 * -XX:NewRatio : 设置新生代与老年代的比例,默认值是2.
 * -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例,默认值是8
 * -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略(暂时用不到)
 * -Xmn:设置新生代的空间的大小(一般不设置)
 *
 * @author shkstart  shkstart@126.com
 *
 */
public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("我只是来打个酱油~");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.1.4.4 对象分配过程

  为新对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。

  1. new的对象先放伊甸园区(此区有大小限制)
  2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(YGC/MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到伊甸园区
  3. 然后将伊甸园中的剩余对象移动到幸存者0区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区
  5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区
  6. 啥时候能去养老区呢?可以设置次数,默认是15次
    • 可以设置参数:进行设置-Xx:MaxTenuringThreshold = N

在养老区,相对悠闲:
当养老区内存不足时,再次触发GC(Major GC),进行养老区的内存清理
若养老区执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常
在这里插入图片描述

在这里插入图片描述

总结:

  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to
  • 关于垃圾回收:频繁在新生代收集,很少在老年代收集,几乎不再永久代和元空间进行收集
  • 其它:常用调优工具
    • JDK命令行
    • Eclipse:Memory Analyzer Tool
    • Jconsole
    • VisualVM
    • Jprofiler
    • Java Flight Recorder
    • GCViewer
    • GC Easy
3.1.4.5 GC
  • VM在进行GC时,并非每次都对上面三个内存(新生代、老年代:方法区)区域一起回收的,大部分时候回收的都是指新生代的回收。
  • 针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)
    • 部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集,其中又分为:
      • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
      • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集
        • 目前,只有CMS GC会有单独收集老年代的行为
        • 注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收
      • 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集
        • 目前,只有G1 GC会有这种行为
    • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集

最简单的分代式GC策略的触发条件

  • 年轻代GC(Minor GC)触发机制
    • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC(每次Minor GC会清理年轻代的内存)
    • 由于Java对象大多都具备朝生夕灭的特性,故 Minor GC 非常频繁,一般回收速度也比较快
    • Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 老年代GC(Major GC / Full GC)触发机制
    • 指发生在老年代的GC,对象从老年代消失时,我们说 Major GC 或 Full GC 发生了
    • 出现了 Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)
      • 也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
    • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长
    • 如果Major GC后,内存还不足,就报OOM了
  • Full GC触发机制
    • 触发Full GC执行的情况有如下五种:
      • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
      • 老年代空间不足
      • 方法区空间不足
      • 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
      • 由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

说明:Full GC 是开发或调优中尽量要避免的

package cn.edu.neu.jvm.gc;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试MinorGC 、 MajorGC、FullGC
 * -Xms9m -Xmx9m -XX:+PrintGCDetails
 * @author shkstart  shkstart@126.com
 *
 */
public class GCTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "atguigu.com";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }

        } catch (Throwable t) {
            t.printStackTrace();
            System.out.println("遍历次数为:" + i);
        }
    }
}

在这里插入图片描述

3.1.4.6 堆空间分代思想

  为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同,70%-99%的对象是临时对象。
  Java堆分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储"朝生夕死"对象的区域进行回收,这样就会腾出很大的空间出来。

3.1.4.7 内存分配策略(对象提升规则)

  如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代,对象晋升老年代的年龄阀值,可以通过选项-XX:MaxTenuringThreshold来设置。

  针对不同年龄段的对象分配原则如下所示:

  • 优先分配到Eden
  • 大对象直接分配到老年代
    • 尽量避免程序中出现过多的大对象
  • 长期存活的对象分配到老年代
  • 动态对象年龄判断:如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
  • 空间分配担保: -XX:HandlePromotionFailure
3.1.4.8 TLAB(Thread Local Allocation Buffer)
  • 为什么要有TLAB(Thread Local Allocation Buffer)?
    • 堆区是线程共享区域,任何线程都可以访问到堆区的共享数据
    • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
    • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
  • 什么是TLAB?
    • 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
    • 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略

More:

  • 不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选
  • 在程序中,开发人员可以通过选项-XX:UseTLAB设置是否开启TLAB空间
  • 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
  • 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

在这里插入图片描述

3.1.4.9 堆空间的参数设置
// 查看所有的参数的默认初始值
-XX:+PrintFlagsInitial
// 查看所有的参数的最终值(可能会存在修改,不再是初始值)
-XX:+PrintFlagsFinal
// 初始堆空间内存(默认为物理内存的1/64)
-Xms
// 最大堆空间内存(默认为物理内存的1/4)
-Xmx
// 设置新生代的大小(初始值及最大值)
-Xmn
// 配置新生代与老年代在堆结构的占比
-XX:NewRatio
// 设置新生代中Eden和S0/S1空间的比例
-XX:SurvivorRatio
// 设置新生代垃圾的最大年龄
-XX:MaxTenuringThreshold  
// 输出详细的GC处理日志
-XX:+PrintGCDetails 
// 打印gc简要信息
-Xx:+PrintGC
// 是否设置空间分配担保
-XX:HandlePromotionFalilure
  • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
    • 如果大于,则此次Minor GC是安全的
    • 如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允担保失败。
      • 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。
        • 如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;
        • 如果小于,则改为进行一次Full GC。
      • 如果HandlePromotionFailure=false,则改为进行一次Full Gc。

在JDK6 Update24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC。

Note

3.1.4.10 堆是分配对象的唯一选择么?***

  在《深入理解Java虚拟机》中关于Java堆内存有这样一段描述:随着JIT编译的发展与逃逸分析技术逐渐成熟,栈上分配标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

  在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过**逃逸分析(Escape Analysis)**后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

逃逸分析
  如何将堆上的对象分配到栈,需要使用逃逸分析手段。这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域:

  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其他地方中

  没有发生逃逸的对象,则可以分配到栈上(每个栈里面包含了很多栈帧),随着方法执行的结束,栈空间就被移除:

// 逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}
// 非逃逸
public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}
package cn.edu.neu.jvm.runtime_area;

/**
 * 逃逸分析
 *
 *  如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用。
 * @author shkstart
 *
 */
public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis对象,发生逃逸
     */
    public EscapeAnalysis getInstance(){
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    为成员属性赋值,发生逃逸
     */
    public void setObj(){
        this.obj = new EscapeAnalysis();
    }
    //思考:如果当前的obj引用声明为static的?仍然会发生逃逸。

    /*
    对象的作用域仅在当前方法中有效,没有发生逃逸
     */
    public void useEscapeAnalysisA(){
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成员变量的值,发生逃逸
     */
    public void useEscapeAnalysisB(){
        EscapeAnalysis e = getInstance();
        // getInstance().xxx()同样会发生逃逸
    }
}

逃逸分析:参数设置

  在JDK 6u23 版本之后,HotSpot中默认就已经开启了逃逸分析。如果使用的是较早的版本,开发人员则可以通过:

  • 选项"-XX:+DoEscapeAnalysis"显式开启逃逸分析
  • 通过选项"-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果

  结论:开发中能使用局部变量的,就不要使用在方法外定义。

逃逸分析:代码优化

  使用逃逸分析,编译器可以对代码做如下优化:

一、栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配。

JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,而无须进行垃圾回收。
常见的栈上分配的场景有给成员变量赋值、方法返回值、实例引用传递

二、同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

public void f() {
    Object hellis = new Object();
    synchronized(hellis) {
        System.out.println(hellis);
    }
}

  代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:

public void f() {
    Object hellis = new Object();
	System.out.println(hellis);
}

三、分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

标量(scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

public static void main(String args[]) {
    alloc();
}
private static void alloc() {
    Point point = new Point(1,2);
    System.out.println("point.x" + point.x + ";point.y" + point.y);
}
class Point {
    private int x;
    private int y;
}

  以上代码,经过标量替换后,就会变成:

private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x = " + x + "; point.y=" + y);
}

  可以看到,Point这个聚合量经过逃逸分析后,发现它并没有逃逸,就被替换成两个标量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。
  标量替换为栈上分配提供了很好的基础。

-XX:EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配到栈上

package cn.edu.neu.jvm.runtime_area;

/**
 * 标量替换测试
 * -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 *
 * 参数-server:启动Server模式,因为在server模式下,才可以启用逃逸分析。
 * @author shkstart  shkstart@126.com
 *
 */
public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }

    public static void alloc() {
        // 未发生逃逸
        User u = new User();
        u.id = 5;
        u.name = "www.atguigu.com";
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("Time: " + (end - start) + " ms");
    }
}

/*
class Customer{
    String name;
    int id;
    Account acct;

}

class Account{
    double balance;
}

 */

  上述代码在主函数中进行了1亿次alloc。调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就必然会发生GC。

逃逸分析小结:逃逸分析并不成熟

  关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

3.1.5 方法区——线程共享
3.1.5.1 栈、堆、方法区的交互关系

在这里插入图片描述

3.1.5.2 什么是方法区?
  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展,其大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace
  • HotSpot 方法区的演进
    在这里插入图片描述
3.1.5.3 设置方法区大小与OOM
  • 设置方法区内存的大小

    • JDK 7 及以前
      • 通过 -XX:Permsize 设置永久代初始分配空间,默认值是20.75M
      • 通过 -XX:MaxPermsize 来设定永久代最大可分配空间,32位机器默认是64M,64位机器模式是82M
      • 当 JVM 加载的类信息容量超过了这个值,会报异常 OutOfMemoryError:PermGen space
    • JDK 8 以后
      • 元数据区大小使用参数 -XX:MetaspaceSize(设置初始的元空间大小) 和 -XX:MaxMetaspaceSize 指定,默认值依赖于平台(windows下,-XX:MetaspaceSize=21M -XX:MaxMetaspaceSize=-1(即没有限制))
      • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
      • 对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
  • OOM 的解决

    1. 要解决 OOM 异常或 heap space 的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
    2. 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与 GCRoots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及 GCRoots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
    3. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
3.1.5.4 方法区的内部结构
  • 方法区存储的是什么
    • 方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
  • 方法区的内部结构
    • 类型信息
      • 对每个加载的类型(类Class、接口Interface、枚举Enum、注解Annotation),JVM必须在方法区中存储以下类型信息:
        • 这个类型的完整有效名称(全名=包名.类名)
        • 这个类型直接父类的完整有效名(对于 Interface 或是 java.lang.Object,都没有父类)
        • 这个类型的修饰符(public,abstract,final 的某个子集)
        • 这个类型直接接口的一个有序列表
    • 域信息
      • JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序,域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
    • 方法信息
      • JVM 必须保存所有方法的以下信息,且同域信息一样需包括声明顺序:
        • 方法名称
        • 方法的返回类型(或void)
        • 方法参数的数量和类型(按顺序)
        • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
        • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
        • 异常表(abstract和native方法除外)
          • 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
  • 常量池
    • 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用
    • 一个 Java 源文件中的类、接口,编译后产生一个字节码文件,而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。
    • 常量池中有什么
      • 数量值
      • 字符串值
      • 类引用
      • 字段引用
      • 方法引用
  • 运行时常量池
    • 运行时常量池(Runtime Constant Pool)是方法区的一部分
    • 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
    • JVM 为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的
    • 运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用
    • 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性
    • 运行时常量池类似于传统编程语言中的符号表(symboltable),但是它所包含的数据却比符号表要更加丰富一些
    • 当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常
3.1.5.5 HotSpot 虚拟机方法区的演进细节

在这里插入图片描述

  • 为什么永久代要被元空间替代
    • 随着Java8的到来,HotSpot VM中再也见不到永久代了,但是这并不意味着类的元数据信息也消失了,实际上,这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)
    • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间
    • 这项改动是很有必要的,原因有:
      • 为永久代设置空间大小是很难确定的
        • 在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM,比如某个实际Web工 程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
      • 对永久代进行调优是很困难的
  • StringTable为什么要调整位置?
    • JDK7 中将 StringTable 放到了堆空间中,是因为永久代的回收效率很低,在full gc的时候才会触发,而full gc是在老年代的空间不足、永久代不足时才会触发。
    • 这就导致StringTable回收效率不高,而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,而放到堆里,能及时回收内存。
3.1.5.6 方法区的垃圾回收
  • 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型
  • 先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收,其回收废弃常量与回收Java堆中的对象非常类似
  • 判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了,因为需要同时满足下面三个条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
    • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • 在大量使用反射、动态代理、CGLib 等字节码框架,动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
3.2 类加载子系统

在这里插入图片描述

3.2.1 类加载器子系统的作用
  • 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
  • 加载的类信息存放于一块称为方法区的内存空间:除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
3.2.2 类加载器(ClasLoader)的角色
  • class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例
  • class file加载到JVM中,被称为DNA元数据模板,放在方法区
  • 在.class文件 → \rightarrow JVM → \rightarrow 元数据模板的过程中,需要一个运输工具(类加载器Class Loader),扮演一个快递员的角色
    在这里插入图片描述
3.2.3 类加载过程

在这里插入图片描述
代码运行加载流程:

public class HelloLoader {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

在这里插入图片描述

3.2.3.1 加载
  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

More: 加载class文件的方式

  • 本地系统中直接加载
  • 通过网络获取,典型场景:Web Applet
  • 从zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 其他文件生成,典型场景:JSP应用
  • 从专有数据库中提取.class文件,比较少见
  • 加密文件中获取,典型的防Class文件被反编译的保护措施
3.2.3.2 链接
  • 验证
    • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
    • 主要包括四种验证方式
      • 文件格式验证
      • 元数据验证
      • 字节码验证
      • 符合应用验证
  • 准备
    • 为类变量分配内存并且设置该类变量的默认初始值,即零值
    • 不包含用final修饰的static的类变量,因为final在编译的时候就会分配了,准备阶段会显式初始化;
    • 不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 解析
    • 将常量池内的符号引用转换为直接引用的过程
      • 符号引用就是一组符号来描述所引用的目标(符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中)
      • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
    • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
    • 事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行
3.2.3.3 初始化
  • 初始化阶段就是执行**类构造器方法<clinit>()**的过程
    • <clinit>()方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
  • 类构造器方法中指令按语句在源文件中出现的顺序执行
  • <clinit>()不同于类的构造器
    • 关联:构造器是虚拟机视角下的<init>()
  • 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
3.2.4 类加载器的分类

在这里插入图片描述

这里的四者之间的关系是包含关系,不是上层下层,也不是子父类的继承关系。

3.2.4.1 虚拟机自带的类加载器
  • 启动类加载器(引导类加载器,Bootstrap ClassLoader)

    • 启动类加载器是使用C/C++语言实现的,嵌套在JVM内部,它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
    • 启动类加载器并不继承自 java.lang.ClassLoader ,且没有父加载器
    • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
    • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  • 扩展类加载器(Extension ClassLoader)

    • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,
    • 扩展类加载器派生于ClassLoader类
    • 扩展类加载器的父类加载器为启动类加载器
    • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库
      • 如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
  • 应用程序类加载器(系统类加载器,AppClassLoader)

    • java语言编写,由sun.misc.Launcher$AppClassLoader实现
    • 应用程序类加载器派生于ClassLoader类
    • 应用程序类加载器的父类加载器为扩展类加载器
    • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
    • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
    • 通过ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器
3.2.4.1 用户自定义的类加载器
  • 为什么要自定义类加载器?

    • 隔离加载类
    • 修改类加载的方式
    • 扩展加载源
    • 防止源码泄漏
  • 用户自定义类加载器实现步骤

    1. 开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
    2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中
    3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
3.2.5 双亲委派机制***

  Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象;而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

在这里插入图片描述

为什么采用双亲委派模式

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    • 自定义类:java.lang.String
      • 沙箱安全机制
        • 自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类,这样可以保证对java核心源代码的保护,这就是沙箱安全机制
    • 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
3.2.6 其它
3.2.6.1 如何判断两个class对象是否相同*

  在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
    • 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
3.2.6 对类加载器的引用

  JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的:如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中;当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

3.2.7 类的主动使用和被动使用
  • Java程序对类的使用方式分为:主动使用和被动使用。

  • 主动使用,又分为七种情况:

    • 创建类的实例
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射(比如:Class.forName())
    • 初始化一个类的子类
    • Java虚拟机启动时被标明为启动类的类
    • JDK 7 开始提供的动态语言支持:
      java.lang.invoke.MethodHandle实例的解析结果
      REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
  • 除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

3.3 垃圾回收
3.3.1 垃圾回收概述
  • 什么是垃圾?
    • 垃圾指的是在运行程序中没有任何指针指向的对象
    • 如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出
  • 为什么需要垃圾回收?
    • 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。除了释放没用的对象,垃圾回收也可以清除内存里的碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
    • 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。
  • 垃圾回收主要关注的区域:堆和方法区
3.3.2 垃圾回收算法
3.3.2.1 判断对象是否是垃圾的算法
  • 引用计数法
    • 引用计数算法(Reference Counting)比较简单,它对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
    • 优缺点
      • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
      • 缺点:由于单独的字段存储计数器而增加了存储空间的开销;由于每次赋值都需要更新计数器,且伴随着加法和减法操作,而增加了时间开销;引用计数器无法处理循环引用的情况
  • 可达性分析算法
    • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain),如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
    • GC Roots 有哪些
      • 虚拟机栈中引用的对象
      • 本地方法栈内 JNI(本地方法) 引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • Java 虚拟机内部的引用:基本数据类型对应的 Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器
3.3.2.2 垃圾回收算法
  • 标记-清除算法

    • 当堆中的可用内存空间(available memory)被耗尽的时候,就会停止整个程序(被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
      • 标记:Collector 从引用根节点处开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象
      • 清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收
    • 缺点:
      • 标记清除算法的效率不算高
      • 在进行GC的时候,需要停止整个应用程序,用户体验较差
      • 标记清除算法清理出来的空闲内存是不连续的,会产生内碎片,需要维护一个空闲列表
  • 复制算法

    • 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收,一般用于新生代的垃圾回收,因为新生代经常发生存活对象少、垃圾对象多的情况
    • 优缺点
      • 优点:不会出现"碎片"问题
      • 缺点:需要两倍的内存空间
  • 标记-整理算法

    • 标记-整理算法的第一阶段和标记清除算法一样,即从根节点开始标记所有被引用对象;而第二阶段将所有的存活对象压缩到内存的一端,按顺序排放;之后,清理边界外所有的空间
    • 优缺点:
      • 优点:消除了标记-清除算法当中,内存区域分散的缺点,当需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可;消除了复制算法当中,内存减半的高额代价
      • 缺点:标记-整理算法效率要低于复制算法;移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址;移动过程中,需要全程暂停用户应用程序
3.3.2.3 分代垃圾回收算法
	分代垃圾回收算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
3.3.2.4 垃圾回收器
#概述
  • 垃圾回收器的分类

    • 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器
      • 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束
      • 并行回收可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“Stop-the-World”机制
    • 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
      • 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间
      • 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束
    • 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器。
      • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片
      • 非压缩式的垃圾回收器不进行存活对象的压缩整理
    • 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器
  • 垃圾回收器发展史:略

  • 7 款经典的垃圾回收器

    • 串行回收器:Serial、Serial Old
    • 并行回收器:ParNew、Parallel Scavenge、Parallel old
    • 并发回收器:CMS(Concurrent Mark Sweep GC)、G1
  • 7 款经典收集器与垃圾分代之间的关系
    在这里插入图片描述

    • 新生代收集器:Serial、ParNew、Parallel Scavenge;
    • 老年代收集器:Serial Old、Parallel Old、CMS;
    • 整堆收集器:G1;
# Serial 回收器 & Serial Old 回收器:串行回收
  • Serial 收集器是最基本、历史最悠久的垃圾收集器,是JDK1.3之前回收新生代唯一的选择
  • Serial收集器采用复制算法、串行回收和"Stop-The-World"机制的方式执行内存回收
  • 除了年轻代之外,Serial 收集器还提供用于执行老年代垃圾收集的 Serial Old 收集器,Serial Old收集器同样也采用了串行回收和"Stop the World"机制,只不过内存回收算法使用的是标记-压缩算法。

在这里插入图片描述
  Serial 回收器 Serial Old 回收器是单线程的收集器,但“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集结束。

# ParNew 回收器 & Parallel Old 收集器:并行回收
  • 如果说Serial GC是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 Serial 收集器的多线程版本。ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别(同样也是采用复制算法、"Stop-the-World"机制)。
  • -XX:+UseParNewGC:指定使用ParNew收集器执行新生代的垃圾回收任务
  • -XX:ParallelGCThreads:限制线程数量,默认开启和CPU核数相同的线程数
    在这里插入图片描述
  • Parallel 收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。
  • Parallel Old收集器采用了标记-整理算法,但同样也是基于并行回收和"Stop-the-World"机制。
# Parallel Scavenge 收集器
  • Hotspot 的年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge 收集器同样也采用了复制算法、并行回收和"Stop the World"机制。
  • Parallel Scavenge 收集器和 ParNew 收集器不同,ParallelScavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器,自适应调节策略也是 ParallelScavenge 与 ParNew 一个重要区别。
  • 高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务,例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
# CMS 回收器
  • CMS(Concurrent-Mark-Sweep) 是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作,是一款在强交互应用中几乎可认为就有划时代意义的垃圾回收器

  • CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,因为停顿时间越短(低延迟)就越适合与用户交互,程序良好的响应速度能提升用户体验

  • CMS的垃圾收集算法采用标记-清除算法,并且也会"Stop-the-World"

  • CMS 回收器的整个过程比之前的回收器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

    • 初始标记(Initial-Mark)阶段:
      • 在这个阶段中,程序中所有的工作线程都将会因为"Stop-the-World"机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出 GCRoots 能直接关联到的对象,一旦标记完成之后就会恢复之前被暂停的所有应用线程。
      • 由于直接关联对象比较小,所以这里的速度非常快。
    • 并发标记(Concurrent-Mark)阶段:
      • 从 GC Roots 直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以并发运行。
    • 重新标记(Remark)阶段:
      • 由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
    • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
      在这里插入图片描述
  • CMS 垃圾回收器的优缺点

    • 优点:
      • 并发收集
      • 低延迟
    • 缺点:
      • 会产生内存碎片:在无法分配大对象的情况下,不得不提前触发FullGC
      • CMS收集器对CPU资源非常敏感:在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
      • CMS收集器无法处理浮动垃圾:在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收
  • CMS 垃圾回收器参数设置

    • -XX:+UseConcMarkSweepGC
      • 使用CMS收集器执行内存回收任务,开启该参数后会自动将-XX:+UseParNewGC打开,即:ParNew(Young区用)+CMS(Old区用)+ Serial Old(CMS运行期间预留的内存无法满足程序需要而导致"Concurrent Mode Failure"时使用)的组合。
    • -XX:CMSInitiatingOccupanyFraction
      • 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
        • 如果内存增长缓慢,则可以设置一个稍大的值,大的阀值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。
        • 如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
    • -XX:+UseCMSCompactAtFullCollection
      • 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生
    • -XX:CMSFullGCsBeforeCompaction
      • 设置在执行多少次Full GC后对内存空间进行压缩整理
    • -XX:ParallelcMSThreads
      • 设置CMS的线程数量,默认是(ParallelGCThreads+3)/4,其中 ParallelGCThreads 是年轻代并行收集器的线程数
# G1 回收器:区域化分代式
  • G1(Garbage-First)垃圾回收器是在 Java7 update 4之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。
  • G1 出现的原因在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求。
  • G1 是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
  • G1 回收器有计划地避免在整个 Java 堆中进行全区域的垃圾收集。
    • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,也是 Garbage-First 名称的由来。
  • G1 回收器的特点
    • 并行与并发
      • 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程STW
      • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
    • 分代收集
    • 空间整合
      • G1 将内存划分为一个个的 Region,内存的回收是以 Region 作为基本单位的
      • Region 之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片
      • G1 的这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC
    • 可预测的停顿时间模型
      • G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
      • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制
      • G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
  • G1 回收器的缺点
    • 在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高
    • 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间
  • G1 回收器的参数设置
    • -XX:+UseG1GC
      • 使用G1垃圾收集器执行内存回收任务
    • -XX:G1HeapRegionSize
      • 设置每个Region的大小,值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域
    • -XX:MaxGCPauseMillis
      • 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200ms,即人的平均反应速度
  • G1 回收器的过程***
    • 年轻代GC
      • JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程,年轻代垃圾回收只会回收Eden区和Survivor区。
      • 首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段;然后开始如下回收过程:
        • 第一阶段,扫描根
          • 根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。
        • 第二阶段,更新RSet
          • 处理 dirty card queue 中的card,更新RSet,此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用。
        • 第三阶段,处理RSet
          • 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
        • 第四阶段,复制对象
          • 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到 Survivor 区中空的内存分段,Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 Old 区中空的内存分段;如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
        • 第五阶段,处理引用
          • 处理Soft,Weak,Phantom,Final,JNI Weak 等引用,最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
    • 老年代并发标记过程
      • 初始标记阶段:标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次年轻代GC。
      • 根区域扫描(Root Region Scanning):G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在YoungGC 之前完成。
      • 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YoungGC中断
        • 在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收
        • 同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
      • 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果,是STW的
        • G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)
      • 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫,是STW的,这个阶段并不会实际上去做垃圾的收集
      • 并发清理阶段:识别并清理完全空闲的区域。
    • 混合回收
      • 当越来越多的对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。
      • 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。
3.3.2.5 其它相关概念
  • 内存溢出(OutOfMemory)与内存泄漏(Memory Leak)

    • 内存溢出:指的是没有空闲内存,并且垃圾收集器也无法提供更多内存的情况
    • 内存泄漏:指的是对象不会再被程序使用到且对象又不能被回收的情况
    • 内存泄漏与内存溢出的关系:内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现内存溢出错误,导致程序崩溃
    • 导致内存泄露的情况
      • 单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生
      • 数据库连接、网络连接和 IO 连接必须手动 close(),否则是不能被回收的,会导致内存泄漏
  • Stop-the-World

    • Stop-the-World,即STW,指的是GC事件发生过程中,会产生应用程序的停顿,停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW
  • 并发与并行

    • 并发:并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行(并发的多个任务之间是互相抢占资源的)
    • 并行:当系统有一个以上 CPU 或一个 CPU 有多核时,那么一个 CPU 核心在执行一个进程时,另一个 CPU 核心可以执行另一个进程,且两个进程互不抢占对方的 CPU 资源,即可以并行执行(并行的多个任务之间是不互相抢占资源的)
  • 安全点与安全区域

    • 安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点
    • 安全区域:在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的,这个区域被称为安全区域
  • 引用

    • 强引用(Strong Reference):不回收
      • 在Java程序中,最最常见的引用类型就是强引用,强引用是默认的引用类型
      • 当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用
      • 无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
      • 强引用是造成 Java 内存泄漏的主要原因之一
    • 软引用(Soft Reference):内存不足即回收
      • 软引用是用来描述一些还有用,但非必需的对象
      • 只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
      • 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用:如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存
    • 弱引用(Weak Reference):发现即回收
      • 弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止
      • 当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象;但是由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象,在这种情况下,弱引用对象可以存在较长的时间
      • 弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收
      • 弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况
    • 虚引用(Phantom Reference):用于对象回收跟踪
      • 一个对象是否有虚引用的存在,完全不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收
      • 虚引用不能单独使用,也无法通过虚引用来获取被引用的对象,当试图通过虚引用的 get() 方法取得对象时,总是 null
      • 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程,比如:能在这个对象被收集器回收时收到一个系统通知
      • 虚引用必须和引用队列一起使用,它在创建时必须提供一个引用队列作为参数:当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况
// 声明强引用
Object obj = new Object();
// 引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
3.4 Class 文件结构与字节码指令集

3.5 对象实例化
3.5.1 对象实例化

在这里插入图片描述

3.5.1.1 创建对象的方式
  • new
  • Xxx/XxxBuilder/XxxFactory的静态方法
  • Class的newInstance方法:反射的方式,只能调用空参的构造器,权限必须是public
  • Constructor的newInstance(XXX):反射的方式,可以调用空参、带参的构造器,权限没有要求
  • 使用clone():不调用任何的构造器,要求当前的类需要实现 Cloneable 接口,实现 clone()
  • 使用反序列化:从文件中、从网络中获取一个对象的二进制流并反序列化
3.5.1.2 创建对象的步骤***
  1. 判断对象对应的类是否加载、链接、初始化
      虚拟机遇到一条 new 指令,首先去检查这个指令的参数能否在 Metaspace 的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化(即判断类元信息是否存在)。如果没有被加载、解析和初始化,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的 .class文件,若没有找到文件,则抛出ClassNotFoundException异常;否则进行类加载,并生成对应的Class对象
  2. 为对象分配内存
      首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
  3. 处理并发安全问题
  4. 属性的默认初始化(零值初始化):所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
  5. 设置对象的对象头:将对象的所属类(即类的元数据信息)、对象的 HashCode 和对象的 GC 信息、锁信息等数据存储在对象的对象头中
  6. 执行init方法进行初始化(属性的显示初始化)
      初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量
3.5.2 对象内存布局

在这里插入图片描述

public class Customer{
    int id = 1001;
    String name;
    Account acct;

    {
        name = "匿名客户";
    }

    public Customer() {
        acct = new Account();
    }
}

public class CustomerTest{
    public static void main(string[] args){
        Customer cust=new Customer();
    }
}

在这里插入图片描述

3.5.3 对象的访问定位
  • 句柄访问
    在这里插入图片描述
    reference 中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。

  • 直接指针(HotSpot)
    在这里插入图片描述
    直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。

3.6 直接内存
  • 概述

    • 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
    • 直接内存是在Java堆外的、直接向系统申请的内存区间
    • 来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
    • 通常,访问直接内存的速度会优于Java堆,即读写性能高,因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
    • Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
    • 由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
  • 非直接缓存区

    • 使用IO读写文件,需要与磁盘交互,需要由用户态切换到内核态,在内核态时,需要两份内存存储重复数据,效率低
      在这里插入图片描述
  • 直接缓存区

    • 使用NIO时,操作系统划出的直接缓存区可以被 Java 代码直接访问,只有一份,NIO适合对大文件的读写操作
      在这里插入图片描述
3.7 性能监控与调优
3.7.1 性能监控与调优概述
  • 监控依据

    • 运行日志
    • 异常堆栈
    • GC 日志
    • 线程快照
    • 堆转储快照
  • 调优原因

    • 防止出现 OOM
    • 解决 OOM
    • 减少 Full GC 出现的频率
  • 调优大方向

    • 合理地编写代码
    • 充分并合理的使用硬件资源
    • 合理地进行 JVM 调优
  • 性能优化的步骤

    • 性能监控
      • GC 频繁
      • CPU 负载过高
      • OOM
      • 内存泄漏
      • 死锁
      • 程序响应时间较长
    • 性能分析
      • GC 日志
      • jstack、jmap、jinfo
      • MAT 与堆文件 dump
    • 性能调优
      • 适当增加内存
      • 根据业务背景选择垃圾回收器
      • 优化代码,控制内存使用
      • 增加机器,分散节点压力
      • 合理设置线程池线程数量
      • 使用中间件提高程序效率,比如缓存、消息队列等
  • 性能评价、测试的指标

    • 响应时间:提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间
      • 在 GC 中:执行垃圾收集时,程序的工作线程被暂停的时间,可通过 -XX:MaxGCPauseMillis 设置
    • 吞吐量:单位时间内完成的工作量(请求)的量度
      • 在 GC 中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间+内存回收的时间),若设置 -XX::GCTimeRatio=n,则吞吐量为1-1/(1+n)
    • 并发数:同一时刻,对服务器有实际交互的请求数
    • 内存占用:Java 堆区所占的内存大小
3.7.2 性能监控与调优工具之命令行
  • jps [options]:Java Process Status,用于查询正在运行的虚拟机进程
    • -q:仅输出 Java 进程id
    • -l:输出应用程序主类的全类名 或 jar包名
    • -m:输出虚拟机进程启动时传递给主类main()的参数
    • -v:列出虚拟机进程启动时的JVM参数
  • jstat:JVM Statistics Monitoring Tool,用于监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,常用于检测垃圾回收问题以及内存泄漏问题
  • jinfo
  • jmap
  • jhat
  • jstack
  • jcmd
  • jstatd:远程主机信息收集
3.7.3 性能监控与调优工具之GUI
  • Jconsole
  • Visual VM
  • Eclipse MAT
  • Jprofiler
3.7.4 JVM 运行时参数

&&&

  • 标准参数选项:java -help
  • X
[root@localhost ~]# java -X
    -Xmixed           mixed mode execution (default)
    -Xint             interpreted mode execution only
    -Xbootclasspath:<directories and zip/jar files separated by :>
                      set search path for bootstrap classes and resources
    -Xbootclasspath/a:<directories and zip/jar files separated by :>
                      append to end of bootstrap class path
    -Xbootclasspath/p:<directories and zip/jar files separated by :>
                      prepend in front of bootstrap class path
    -Xdiag            show additional diagnostic messages
    -Xnoclassgc       disable class garbage collection
    -Xincgc           enable incremental garbage collection	启用增量垃圾收集
    -Xloggc:<file>    log GC status to a file with time stamps
    -Xbatch           disable background compilation
    -Xms<size>        set initial Java heap size	设置初始 Java 堆大小
    -Xmx<size>        set maximum Java heap size	设置最大 Java 堆大小
    -Xss<size>        set java thread stack size	设置 Java 线程堆栈大小
    -Xprof            output cpu profiling data
    -Xfuture          enable strictest checks, anticipating future default
    -Xrs              reduce use of OS signals by Java/VM (see documentation)
    -Xcheck:jni       perform additional checks for JNI functions
    -Xshare:off       do not attempt to use shared class data
    -Xshare:auto      use shared class data if possible (default)
    -Xshare:on        require using shared class data, otherwise fail.
    -XshowSettings    show all settings and continue
    -XshowSettings:all
                      show all settings and continue
    -XshowSettings:vm
                      show all vm related settings and continue
    -XshowSettings:system
                      (Linux Only) show host system or container
                      configuration and continue
    -XshowSettings:properties
                      show all property settings and continue
    -XshowSettings:locale
                      show all locale related settings and continue

The -X options are non-standard and subject to change without notice.

  • XX
-XX:+<option>  启用option属性
-XX:-<option>  禁用option属性
-XX:<option>=<number>  设置option数值,可以带单位如k/K/m/M/g/G
-XX:<option>=<string>  设置option字符值

&&&

  • jar 包运行:java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -jar demo.jar
  • war 包运行(tomcat)
# linux下 catalina.sh 添加
JAVA_OPTS="-Xms512M -Xmx1024M"
# windows下 catalina.bat 添加
set "JAVA_OPTS=-Xms512M -Xmx1024M"

&&& 常用的 JVM 参数选项

a.

-XX:+PrintCommandLineFlags 程序运行时JVM默认设置或用户手动设置的XX选项
-XX:+PrintFlagsInitial 打印所有XX选项的默认值
-XX:+PrintFlagsFinal 打印所有XX选项的实际值
-XX:+PrintVMOptions 打印JVM的参数

b.

# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置线程栈的大小为128K

# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置JVM初始堆内存为2048M
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置JVM最大堆内存为2048M
-Xmn2g <==> -XX:NewSize=2g -XX:MaxNewSize=2g 设置年轻代大小为2G
-XX:SurvivorRatio=8 设置Eden区与Survivor区的比值,默认为8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为15
-XX:TargetSurvivorRatio 设置MinorGC结束后Survivor区占用空间的期望比例

# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置Klass Metaspace的大小,默认1G

# 直接内存
-XX:MaxDirectMemorySize 指定DirectMemory容量,默认等于Java堆最大值

c.

-XX:+HeapDumpOnOutMemoryError 内存出现OOM时生成Heap转储文件
-XX:+HeapDumpBeforeFullGC 出现FullGC时生成Heap转储文件
-XX:HeapDumpPath=<path> 指定heap转储文件的存储路径,默认当前目录
-XX:OnOutOfMemoryError=<path> 指定可行性程序或脚本的路径,当发生OOM时执行脚本

d.

-XX:+PrintGC <==> -verbose:gc  打印简要日志信息
-XX:+PrintGCDetails            打印详细日志信息
-XX:+PrintGCTimeStamps  打印程序启动到GC发生的时间,搭配-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps  打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC  打印GC前后的堆信息,如下图
-Xloggc:<file> 输出GC导指定路径下的文件中
3.7.5 GC 日志分析:略
3.8 执行引擎
3.8.1 执行引擎概述
  • 执行引擎属于JVM的下层,里面包括解释器、即时编译器、垃圾回收器,是Java虚拟机核心的组成部分之一。

  • "虚拟机"是一个相对于"物理机"的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

  • JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。

  • 如果想要让一个Java程序运行起来,就需要执行引擎(Execution Engine)将字节码指令解释/编译为对应平台上的本地机器指令才可以,简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

  • 执行引擎的工作过程
    在这里插入图片描述

  1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
  2. 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
  3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
3.8.2 Java代码编译和执行过程

在这里插入图片描述
  大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤。
  Java代码编译是由Java源码编译器来完成,流程图如下:
在这里插入图片描述  Java字节码的执行是由JVM执行引擎来完成,流程图如下:
在这里插入图片描述

3.8.2.1 什么是解释器(Interpreter)?什么是JIT编译器?

  解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
  JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

3.8.2.2 为什么Java是半编译半解释型语言?

  JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
  现在的 JVM 在执行 Java 代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

3.8.3 机器码、指令、汇编语言、高级语言

在这里插入图片描述

  • 机器码
    • 各种用二进制编码方式表示的指令,叫做机器码,用它来编写的程序,就是机器语言
    • 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错
  • 指令
    • 由于机器码是有0和1组成的二进制序列,可读性实在太差,于是人们发明了指令
    • 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好
    • 不同的硬件平台,各自支持的指令,是有差别的,因此每个平台所支持的指令,称之为对应平台的指令集,如常见的x86指令集、ARM指令集
  • 汇编语言
    • 由于指令的可读性还是太差,于是出现了汇编语言
    • 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令
  • 高级语言
    • 为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言,高级语言比机器语言、汇编语言更接近人的语言
    • 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码,完成这个过程的程序就叫做解释程序或编译程序

在这里插入图片描述

3.8.4 解释器

  JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
在这里插入图片描述

3.8.4.1 解释器的工作机制
  • 解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
  • 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
3.8.4.2 解释器的分类

  在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器以及现在普遍使用的模板解释器。字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下;而模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
  在HotSpot VM中,解释器主要由 Interpreter 模块和Code模块构成。其中,Interpreter模块实现了解释器的核心功能;而Code模块用于管理HotSpot VM在运行时生成的本地机器指令。

3.8.4 JIT编译器
3.8.4.1 Java代码的执行分类
  • 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码文件转为机器码执行
  • 第二种是编译执行(即直接编译成机器码,但是要知道不同机器上编译的机器码是不一样,而字节码是可以跨平台的),现代虚拟机为了提高执行效率,会使用即时编译技术(JIT,Just In Time)将方法编译成机器码后再执行

  HotSpot VM是目前市面上高性能虚拟机的代表作之一,它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
  既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
  首先明确:当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高。
  所以:尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。

3.8.4.2 几种编译器

  Java 语言的"编译器"其实是一段"不确定"的操作过程,因为它可能是指一个前端编译器,把 .java 文件转变成 .class 文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把.java文件编译成本地机器代码的过程。

  • 前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)
  • JIT编译器:HotSpot VM的C1、C2编译器
  • AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET
3.8.4.3 热点代码及探测技术
  • 热点代码是什么

    • 是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
    • 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换,或简称为 OSR(On Stack Replacement) 编译
  • 热点探测

    • 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行,而这主要依靠热点探测功能。
    • 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
    • 采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter,用于统计方法的调用次数)和回边计数器(Back Edge Counter,用于统计循环体执行的循环次数)
      • 方法调用计数器
        • 方法调用计数器用于统计方法被调用的次数。当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过阀值(-XX:CompileThreshold)。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
      • 回边计数器
        • 回边计数器用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。
  • 热度衰减

    • 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
    • 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
    • 另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
3.8.4.3 HotSpotVM
#程序执行方法设置

  缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行:

  • -Xint:完全采用解释器模式执行程序
  • -Xcomp:完全采用即时编译器模式执行程序,如果即时编译出现问题,解释器会介入执行
  • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序
#HotSpotVM JIT 分类:C1 & C2

  JIT的编译器分为了两种,分别是C1和C2。在HotSpot VM中内嵌有两个JIT编译器,分别为 Client Compiler 和 Server Compiler,但大多数情况下我们简称为 C1 编译器 和 C2 编译器。
  一般来讲,JIT编译出来的机器码性能比解释器高。C2编译器启动时长比C1慢,系统稳定执行以后,C2编译器执行速度远快于C1编译器。
  开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:

  • -client:指定Java虚拟机运行在Client模式下,并使用C1编译器;C1编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。
  • -server:指定Java虚拟机运行在server模式下,并使用C2编译器;C2进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。
#分层编译

  不开启性能监控时,程序解释执行可以触发 C1 编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2 编译会根据性能监控信息进行激进优化。
  在 Java7 版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。

#C1 和 C2 编译器的优化策略
  • C1 编译器的优化策略:
    • 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
    • 去虚拟化:对唯一的实现类进行内联
    • 冗余消除:在运行期间把一些不会执行的代码折叠掉
  • C2 编译器的优化策略:主要是在全局层面,逃逸分析是优化的基础
    • 标量替换:用标量值代替聚合对象的属性值
    • **栈上分配:对于未逃逸的对象分配对象在栈而不是堆
    • 同步消除:清除同步操作,通常指 synchronized

附:More

A. Java 的 8 种基础数据类型
  • boolean、byte、char、short、int、long、float、double
B. Java 的方法调用
  • 在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接与动态链接

    • 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
    • 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接

    静态链接与动态链接对应的方法的绑定机制分别为:早期绑定(Early Binding)和晚期绑定(Late Binding)

  • 早期绑定与晚期绑定

    • 早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
    • 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定

    绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次

package com.atguigu.java2;

/**
 * 早期绑定和晚期绑定的例子
 * @author shkstart
 */
class Animal{
    public void eat(){
        System.out.println("动物进食");
    }
}
interface Huntable{
    void hunt();
}
class Dog extends Animal implements Huntable{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,多管闲事");
    }
}

class Cat extends Animal implements Huntable{

    public Cat(){
        super(); // 表现为:早期绑定
    }

    public Cat(String name){
        this(); // 表现为:早期绑定
    }

    @Override
    public void eat() {
        super.eat(); // 表现为:早期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {
        System.out.println("捕食耗子,天经地义");
    }
}
public class AnimalTest {
    public void showAnimal(Animal animal){
        animal.eat(); // 表现为:晚期绑定
    }
    public void showHunt(Huntable h){
        h.hunt(); // 表现为:晚期绑定
    }
}

  • 虚方法与非虚方法

    • 非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法,其他方法称为虚方法
      • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
  • Java 虚拟机的方法调用指令

    • 普通调用指令:
      • invokestatic:调用静态方法,解析阶段确定唯一方法版本
      • invokespecial:调用<init>方法、私有方法及父类方法,解析阶段确定唯一方法版本
      • invokevirtual:调用所有虚方法
      • invokeinterface:调用接口方法
    • 动态调用指令:
      • invokedynamic:动态解析出需要调用的方法,然后执行

    前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本,其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

    • 关于 invokedynamic 指令
      • JVM字节码指令集一直比较稳定,直到 Java7 中才增加了一个 invokedynamic指令,这是 Java 为了实现「动态类型语言」支持而做的一种改进,但是在Java7中并没有提供直接生成 invokedynamic 指令的方法,需要借助ASM这种底层字节码工具来产生 invokedynamic 指令,直到 Java8 的 Lambda 表达式的出现,invokedynamic 指令的生成在 Java 中才有了直接的生成方式
      • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器

      静态类型语言和动态类型语言:
        动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。说的再直白一点就是,静态类型语言是判断变量自身的类型信息(Java\C\C++);动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征(Python\JS)。

      // java
      String s = "hello"
      s = "world"
      s = 1 // Error
      
      # python
      s = "hello"
      s = "world"
      s = 1 # 可随意赋值,无论是什么类型都可以
      

    在这里插入图片描述

    package com.atguigu.java2;
    
    /**
     * 
     * invokestatic指令和invokespecial指令调用的方法称为非虚方法
     * @author shkstart
     */
    class Father {
        public Father() {
            System.out.println("father的构造器");
        }
    
        public static void showStatic(String str) {
            System.out.println("father " + str);
        }
    
        public final void showFinal() {
            System.out.println("father show final");
        }
    
        public void showCommon() {
            System.out.println("father 普通方法");
        }
    }
    
    public class Son extends Father {
        public Son() {
            // invokespecial
            super();
        }
        public Son(int age) {
            // invokespecial
            this();
        }
        // 不是重写的父类的静态方法,因为静态方法不能被重写
        public static void showStatic(String str) {
            System.out.println("son " + str);
        }
        private void showPrivate(String str) {
            System.out.println("son private" + str);
        }
    
        public void show() {
            // invokestatic
            showStatic("atguigu.com");
            // invokestatic
            super.showStatic("good!");
            // invokespecial
            showPrivate("hello!");
            // invokespecial
            super.showCommon();
    
            // invokevirtual
            showFinal(); // 由于此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法
            // invokevirtual
            showCommon();
            info();
    
            MethodInterface in = null;
            // invokeinterface
            in.methodA();
        }
    
        public void info(){
    
        }
    
        public void display(Father f){
            f.showCommon();
        }
    
        public static void main(String[] args) {
            Son so = new Son();
            so.show();
        }
    }
    
    interface MethodInterface{
        void methodA();
    }
    
C.
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值