天天看点

厚积薄发——Java1. JVM2. 并发

1. JVM

1.1 Java运行时数据区

1.1.1 线程私有区域

  1. 程序计数器:线程所执行的字节码的行号指示器(唯一没有oom异常的区域),记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
  2. Java虚拟机栈:描述Java方法执行的内存模型。
  3. 本地方法栈:描述Native方法执行的内存模型。

1.1.2 线程共享区域

  1. 堆:存放所有的实例对象(-Xms设置堆最小值,-Xmx设置堆最大值)。
  2. 方法区(JDK8后的元空间):存储已加载的类信息、常量、静态变量、即时编译器编译后的代码以及运行时常量池。
  3. 直接内存: NIO类中引入了一种基于通道与缓存区 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

1.2 内存分配与回收

1.2.1 内存分配

  1. 对象优先在Eden区分配。
  2. 大对象直接在老年代分配。
  3. 长期存活对象进入老年代。

1.2.2 对象访问定位

  1. 句柄:Java 堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  2. 直接指针:Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

1.2.3 垃圾收集(GC)

1.2.3.1 哪些内存需要回收

Minor GC 的触发条件:Eden区域没有足够的空间。

Full GC(Major GC)的触发条件:老年代没有足够空间。

  1. 引用计数算法(无法处理循环引用)
  2. 可达性分析算法

    思想:从GC Roots开始分析引用链,不可达的对象可以回收。

    GC Roots:虚拟机栈中引用的对象、本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。

Java中的引用类型

强引用(new):只要强引用还存在,垃圾收集器就不会回收被引用的对象。

软引用(SoftReference):在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围进行二次回收。

弱引用(WeakReference):只能存活到下次垃圾收集发生之前。

虚引用(PhantomReference):一个对象是否有虚引用存在,不对其生存时间构成影响。

拯救对象:可以用finalize()方法拯救一次对象。

1.2.3.2 垃圾回收算法

Java堆分为新生代和老年代,默认比例为新生代:老年代 = 1:2,即新生代占总堆内存的1/3,老年代占2/3。
  1. 标记清除(先标记后清除):标记和清除步骤效率低,容易产生内存碎片。
  2. 复制算法(将内存分块):每次只能使用一部分内存造成浪费,适合新生代(Eden:Survivor = 8:1)。
  3. 标记整理(标记移动清理):适合老年代。
  4. 分代收集算法:新生代和老年代采用不同回收算法。

1.2.3.3 垃圾收集器

  1. Serial/Serial Old收集器
  2. Parallel/Parallel Old收集器
  3. CMS(Concurrent Mark Sweep):初始标记、并发标记、重新标记、并发清除
  4. G1:初始标记、并发标记、最终标记、筛选回收

1.3 性能监控与故障处理工具

1.3.1 jps(Jvm Process Status):虚拟机进程状态工具

jps的功能类似于Linux的ps命令,可以列出正在运行的虚拟机进程, 并显示虚拟机执行主类名称,以及这些进程的本地虚拟机唯一ID(LVMID)。

jps [options] [hostid]

options:

-l 输出主类全名,如果执行的是jar包则输出jar路径

-v 输出虚拟机进程启动时JVM参数

hostid:开启了RMI服务的远程虚拟机主机名

1.3.2 jstat(Jvm Statistic):虚拟机统计信息监视工具

jstat用于监视虚拟机的各种运行状态信息,包括类装载、垃圾收集以及运行期编译状况等。

jstat [ option vmid [ interval[s|ms] [count] ] ]

interval与count代表查询间隔和次数

vmid是虚拟机进程ID

option:

-gc:监视Java堆状况,包括容量、已用空间以及GC信息等

-gcutil:与gc选项基本相同,但关注点主要是已使用空间占总空间的百分比

1.3.3 jinfo(Configuration Info):Java配置信息工具

jinfo的作用是实时查看和调整虚拟机各项参数。

jinfo [option] pid

1.4 类加载机制

1.4.1 类的生命周期

加载、连接(验证、准备、解析)、初始化、使用、卸载

  1. 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的

    java.lang.Class

    对象作为方法区这个类的各种数据的访问入口。
  2. 验证:确保Class文件字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证以及符号引用验证。
  3. 准备:为类变量分配内存并设置类变量初始值,这里的类变量指static变量。
  4. 解析:将常量池内的符号引用替换为直接引用。
  5. 初始化:准备阶段变量已经赋过一次系统要求的初始值,初始化阶段则根据程序员的要求进行变量初始化,即执行类构造器的

    <clinit>()

    方法。
  6. 使用
  7. 卸载

1.4.2 类加载器

类加载器在层次上由上至下(由父至子)分别为:

  1. 启动类加载器(Bootstrap):加载

    %JAVA_HOME%\lib

    目录下的类库。
  2. 扩展类加载器(Extension):加载

    %JAVA_HOME%\lib\ext

    目录下的类库。
  3. 应用程序类加载器(Applicaton):加载用户类路径上指定的类库。
  4. 自定义类加载器(User):继承ClassLoader,并覆盖findClass()方法。

双亲委派模型:若一个类加载器收到类加载请求,它首先不会尝试自己加载这个类,而是把这个请求委托给它的父加载器去完成,每一层次类加载器都是如此,只有当父类加载无法加载时,子加载器才会尝试自己加载。

破坏双亲委派机制:自定义类加载器,重写loadClass()方法

2. 并发

2.1 进程与线程

2.1.1 概念理解

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

2.1.2 线程生命周期

线程创建之后处于NEW(新建)状态,调用start()方法后线程处于READY(可运行)状态。可运行状态的线程获得CPU时间片后就处于RUNNING(运行)状态。当线程执行wait()方法之后,线程进入WAITING(等待)状态,进入等待状态的线程需要依靠其他线程的通知才能够返回到READY状态。sleep(long millis)或wait(long millis)方法会让线程变为TIMED_WAITING状态,TIME_WAITING(超时等待) 在等待状态的基础上增加了超时限制,当超时时间到达后Java线程将会返回到REDAY状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到BLOCKED(阻塞)状态。线程在执行完的run()方法之后将会进入到TERMINATED(终止)状态。

2.1.3 线程死锁

什么是死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁的四个必要条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁:破坏请求与保持条件(一次性申请所有的资源)、破坏不剥夺条件(占用部分资源的线程进一步申请其他资源时,如果申请不到主动释放它占有的资源)、破坏循环等待条件(按某一顺序申请资源,释放资源则反序释放)。

2.1.4 常用方法

wait()与sleep()区别: sleep方法没有释放锁,而wait方法释放了锁 。

start()与run():调用start()方法,会启动一个线程并使其进入就绪状态,当分配到时间片后就可以自动执行run()方法的内容,这是真正的多线程工作。 而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程。

2.2 线程安全

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized实现单例模式:

public class Singleton {
    //volatile禁止指令重排序
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getSingleton() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (instance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
           

synchronized同步语句块实现原理:

synchronized同步语句块使用的是monitorenter和monitorexit指令,执行monitorenter指令时,线程试图获取monitor(monitor对象存在于每个Java对象的对象头中,因此Java中任意对象可以作为锁) 锁的持有权。当计数器为0,则可以成功获取,获取后将锁的计数器设为1。执行monitorexit指令时,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那么当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

锁优化(偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化):

  1. 偏向锁:偏向锁会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步。偏向锁失败后,会升级为轻量级锁。
  2. 轻量级锁:轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。
  3. 自旋锁和自适应自旋:轻量级锁失败后,虚拟机为了避免线程在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定。
  4. 锁消除:虚拟机在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。
  5. 锁粗化:我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

继续阅读