主页

synchronized

2020-10-13
java

轻量级锁 #

  • 使用场景:如果一个对象虽然有多线程访问,但多线程的访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 使用线程的栈中的锁记录充当锁。
  • 轻量级锁对使用者是透明的,即语法仍然是synchronized。
  • 轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。

锁膨胀 #

  • 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

自旋优化 #

  • 重量级锁竞争的时候,还可以使用自旋来优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

偏向锁 #

  • 只有在第一次使用CAS将线程ID设置到对象头的Mark Word中,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
  • 调用对象的hashCode()会禁用该对象的偏向锁;当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁;调用wait/notify会转化为重量级锁。
  • 批量重偏向:如果对象虽然被多个线程访问,但是没有竞争,这时候偏向了t1的线程的对象仍有机会重新偏向t2,重偏向会重置对象的ThreadID。当撤销偏向锁阈值超过20次后,jvm会觉得:我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
  • 批量撤销:当撤销偏向锁阈值超过40次后,jvm会觉得:自己确实错了,根本就不应该偏向。于是整个类的所有对象都会变成不可偏向的,新建的对象也是不可偏向的,针对类的优化。

Monitor

2020-10-12
java

java对象头 #

32位虚拟机下

  • 普通对象

普通对象

  • 数组对象

数组对象

  • mark word结构

mark word结构

  • 64位虚拟机下mark word结构

64位虚拟机下mark word结构

Monitor #

  • 每个java对象都可以关联一个monitor对象,monitor对象由操作系统提供。如果使用synchronized给对象上锁(重量级)后,该对象对象头的mark word中的数据就会清空(不包括标志位,但标志位会发生改变),然后指向一个monitor对象。

java线程的6种状态

2020-10-08
java

  • 操作系统层面线程有5种状态

  • 在 java中,Thread.State 将线程分为六种状态

public enum State {
        // 线程刚被创建,但是还没调用start方法
        NEW,

        /**
         * 该状态的线程在jvm中是执行状态,但是在操作系统中可能是在等待其他的资源。
         * 此状态涵盖了操作系统中的 运行态、就绪态、阻塞态
         */
        RUNNABLE,

        /**
         * 此状态的线程会等待一个monitor lock。
         * A thread in the blocked state is waiting for a monitor lock 
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling Object.wait
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * 以下方法可使线程进入此状态:
         * 	 Object.wait with no timeout
         * 	 Thread.join with no timeout 
         * 	 LockSupport.park
         * 该状态的线程会等待其他线程通过特定的动作唤醒。
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified(特定的) waiting time.
         * 以下方法可使线程进入此状态:
         * 	 Thread.sleep
         * 	 Object.wait with timeout
         * 	 Thread.join with timeout
         * 	 LockSupport.parkNanos
         * 	 LockSupport.parkUntil
         */
        TIMED_WAITING,

        // 线程已经完成了执行,终止了的状态。
        TERMINATED;
    }

并发相关信息

2020-10-08
java

Thread方法 #

  • sleep:让当前线程休眠n毫秒,休眠时让出cpu的时间片给其他线程。

  • join:等待某个线程运行结束。

  • yield:提示线程调度器让出当前线程对 CPU的使用。

  • interrupt():打断线程,可能会产生打断标记(看下面的介绍)。

    • 可以使用isInterrupted()判断线程是否被打断。
    • 如果线程正在sleep、wait、join会导致被打断的线程抛出InterruptedException,并清除打断标记。
    • 如果打断正在运行的线程,则会设置打断标记。
    • interrupted()方法能返回打断标记的状态,并将打断标记设置为假。

同步 #

  • 同步:需要等待结果返回,才能继续运行就是同步。
  • 异步:不需要等待结果返回,就能继续运行就是异步。

常见线程安全类 #

String Integer StringBuffer Random Vector Hashtable java.util.concurrent

wait和notify

2020-10-06
java

  • 在调用wait方法时,线程必须要持有被调用对象的锁,当调用wait方法之后,线程就会释放掉该对象的锁。
  • 在调用Thread类的sleep方法时,线程是不会释放掉对象的锁的。

  1. 当调用wait方法时,首先要确保调用了wait方法的线程已经持有了对象的锁。
  2. 当调用了wait后,该线程就会释放掉这个对象的锁,然后进入等待状态,该线程进入对象的等待集合中(wait set)。
  3. 当线程调用了wait后进入到等待状态时,它就等待其他线程调用相同对象的notify和notifyAll方法来使得自己被唤醒。
  4. 调用wait方法的代码片段需要放在一个synchronized块或者被synchronized修饰的方法中。
  5. 当调用了对象的notify方法时,它会随机唤醒该对象等待集合中(wait set)的任意一个线程,当某个线程被唤醒后,它就会与其他线程一同竞争对象的锁。
  6. 当调用对象的notifyAll方法时,它会唤醒该对象等待集合中(wait set)中所有的线程,这些线程被唤醒后,又会开始竞争对象的锁。
  7. 某一时刻,只有唯一的一个线程拥有对象的锁。

具体案例 #

Demon对象有一个int类型的属性counter,该值初始为0; 创建四个线程,两个线程对该值增1,两个线程对该值减1; 输出counter每次变化后的结果,要求输出结果为1010101010…。

包含counter的Demon类 #
//该对象提供加1和减1的操作
class Demon{
    //counter
    private int counter=0;
	//对方法加锁,当一个线程要调用该方法时,需要先获取该对象的锁
    public synchronized void inc(){//counter加1
        //此处必须使用while而不是if,防止被其他不相关的线程唤醒
        while(counter!=0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter++;
        System.out.print(counter);
        /*此处必须使用notifyAll,notify会从等待队列中
        随机选择一个线程唤醒,可能会导致程序一直阻塞*/
        notifyAll();
    }
	//counter减1
    public synchronized void dec(){
        while(counter!=1){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter--;
        System.out.print(counter);
        notifyAll();
    }
}

关键代码如上,剩余的代码也都比较简单,就省略了。如果你需要所有代码,可以通过ctrl+u查看网页源代码,并使用ctrl+f快捷键搜索"黑魔仙变身"即可找到完整代码。手机需要使用能查看网页源代码的浏览器,如via等。

垃圾回收器

2020-09-30
java

-XX:+PrintCommandLineFlags:查看命令行参数(可打印出使用的是哪个垃圾回收器)。

垃圾收集器

Serial回收器:串行回收 #

  • Client模式下默认的新生代垃圾收集器。

  • Serial收集器采用复制算法、串行回收和“Stop-The-World”机制的方式执行内存回收。

  • Serial Old-对应的老年代垃圾收集器,同样采用串行回收和“Stop-The-World”机制,只不过内存回收算法使用的是标记-压缩算法。

    • Serial Old是Client模式下默认的老年代的垃圾收集器。
    • Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用;②作为老年代CMS收集器的后备垃圾收集方案。
  • 这个收集器是一个单线程的收集器,在它进行垃圾收集时,必须停掉其他所有的工作线程,直到它收集结束。

  • -XX:+UseSerialGC:指定新生代Serial,老年代Serial Old GC。

ParNew回收器:并行回收 #

  • 多线程的Serial。

  • -XX:+UseParNewGC:指定年轻代ParNew,不影响老年代。

  • -XX:ParallelGCThreads:限制线程数量,默认开启与cpu数据相同的线程数。

Parallel Scavenge回收器:吞吐量优先 #

  • 同样采用并行回收、复制算法、STW机制。

  • Parallel Scavenge收集器的目标是达到一个可控的吞吐量,它也被称为吞吐量优先的垃圾收集器。

  • 自适应调节机制也是Parallel Scavenge与ParNew的一个重要区别。

  • 高吞吐量可以高效的利用cpu时间,尽快的完成程序的运算任务。主要用于在后台运算而不需要太多交互的任务

  • 应用场景:执行批量处理、订单处理、工资支付、科学计算的应用程序。

  • Parallel Old:老年代垃圾收集器,采用标记-压缩算法、并行回收、STW机制。

  • -XX:+UseParallelGC:手动指定年轻代使用Parallel;-XX:+UserParallelOldGC:手动指定老年代。这两个参数,当指定一个,另一个也会被开启。

  • -XX:ParallelGCThreads:设置年轻代并行的线程数。

  • -XX:+UseAdaptiveSizePolicy:设置Parallel收集器具有自适应调节策列。

CMS回收器:低延迟(Concurrent-Mark-Sweep) #

  • HotSpot虚拟机中第一款真正意义上的并发收集器,实现了让垃圾收集线程与用户线程同时执行。

  • 采用标记清除算法,也会导致stw。

  • 运行过程:

    • 初始标记:仅标记GC Roots能直接关联到的对象。速度很快,需要stw。
    • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程。
    • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的stw一般会比初始标记稍长一些。
    • 并发清除:清理标记阶段判断已经死亡的对象,释放内存空间。这个阶段可与用户线程并发。

cms

  • 由于在垃圾收集阶段用户线程仍在执行,所以在CMS回收过程中,要保证应用程序线程有足够的内存可用。因此,CMS收集器不能像别的收集器一样等到老年代几乎完全填满了才进行垃圾回收,而是在堆内存达到某一个阈值时,便开始进行回收。当CMS运行期间,内存无法满足程序要求,这时虚拟机会启用预备方案:临时使用Serial Old进行垃圾回收。

  • CMS采用标记-清除算法,只能采用空闲列表进行内存分配。

  • 优点:低延迟,并发收集。

  • 缺点:产生内存碎片;对cpu资源非常敏感,因为占用了一部分线程,会导致吞吐量降低;无法处理浮动垃圾(并发标记阶段如果产生新的垃圾,cms无法对这些垃圾进行标记,会导致这些垃圾无法及时的被回收)。

  • -XX:+UseConcMarkSweepGC:手动指定使用CMS收集器。

G1回收器:区域分代化 #

  • -XX:+UseG1GC:启用G1。

  • 目标:延迟可控的情况下获取尽可能高的吞吐量。

    • G1是一个并行回收器,它把堆内存分割为很多不相关的区域。使用不同的region来表示Eden区、幸存者0区、幸存者1区、老年代等。
    • G1有计划的避免在java堆中进行全区域的垃圾回收,G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。
    • 由于这种方式的侧重点在于回收垃圾最大量的区间,所以给G1取一个名字:垃圾优先(Garbage First)。
  • 优点:

    • 并行性:G1在回收期间,可以多个GC线程同时工作。此时用户线程stw。
    • 并发性:G1部分工作能与用户线程同时执行。
    • 分代收集:G1仍属于分代型垃圾收集器。但是不再要求整个Eden、年轻代、或者老年代都是连续的,也不再坚持固定大小和数量。它同时兼顾年轻代和老年代。
    • 空间整合:内存是以region为基本单位的。region之间是复制算法,整体上看可以看作是标记-压缩算法。可以避免产生内存碎片。
    • 可预测的时间停顿模型:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • -XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms

  • Remembered Set:一个对象可能被不同区域region所引用。当有其它region指向本region时,记录其他region的引用。

  • G1垃圾回收的过程

    • 年轻代GC:①扫描根(包含RSet);②更新RSet;③处理RSet;④复制对象;⑤处理引用。
    • 并发标记过程:①初始标记(标记根节点直接可达的对象,会触发一次年轻代GC);②根区域扫描(扫描survivor区能直达老年代的对象并标记被引用的对象);③并发标记(若发现一个region区域全是垃圾,则直接回收);④再次标记;⑤独占清理;⑥并发清理阶段。
    • 混合回收:回收整个Yong Region和一部分Old Region。
    • 如果上述方式不能正常工作,就会触发Full GC。

现状

垃圾回收相关概念

2020-09-29
java

System.gc()的理解 #

  • 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用会显示触发Full GC,但是该方法可能不是立刻就执行。

内存溢出 #

  • 产生原因:没有空闲内存,并且垃圾收集器也无法提供更多内存。

内存泄漏 #

  • 严格定义:对象不再被程序使用了,但是GC又不能回收他们的情况(仍然存在引用链)。
  • 宽泛定义:一些不太好的实践会导致对象的生命周期变得很长,甚至导致OOM,也叫宽泛意义上的内存泄漏。

安全点 #

  • 程序执行时并非在所有的地方都能停下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”(safepoint)。
  • 如何在GC发生时,检测所有的程序都跑到最近的安全点停顿下来了呢 –> 主动式中断:设置一个中断标志,各个线程运行到safe point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域 #

  • 指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
  • 可以应对处于sleep或者blocked状态的线程,这时候线程无法响应jvm的中断请求,“走”到安全点去中断挂起。
  • 当程序运行到safe region的代码时,首先标识已经进入了safe region,如果这段时间发生了gc,jvm会忽略标识为safe region状态的线程。
  • 当线程即将离开safe region时,会检测jvm是否已经完成了gc,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开safe region的信号为止。

引用 #

  • 强引用:类似“Object object=new Object()”这类的引用,只要强引用还存在,就永不回收。

  • 软引用:内存不足才回收。

  • 弱引用:发现即回收。

  • 虚引用:对象回收跟踪。

    • 如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
    • 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get方法来获取对象时,总是null。
    • 为一个对象设置虚引用关联的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。
    • 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知引用程序对象的回收情况。
    • 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
//声明强引用
Object obj=new Object();
//声明软引用
SoftReference<Object> sf=new SoftReference<Object>(new Object());
//声明弱引用
WeakReference<Object> wr=new WeakReference<Object>(new Object());
//虚引用
Object obj=new Object();
ReferenceQueue<Object> rq=new ReferenceQueue<>();
PhantomReference<Object> pr=new PhantomReference<>(obj,rq);

评估性能指标 #

  • 吞吐量:运行用户代码时间占总运行时间的比例。
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。

垃圾回收算法

2020-09-27
java

垃圾标记算法 #

  • 引用计数器法

    • 给一个对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
    • 优点:实现简单,垃圾对象便于识别;判定效率高,回收没有延迟性。
    • 缺点:需要单独的字段存储计数器,增加了存储空间的开销;每次赋值都要更新计数器,伴随着加减法的操作,增加了时间开销;引用计数器有一个严重的问题,即无法处理循环引用情况。这是一个致命缺陷,导致java在垃圾回收器中没有使用这类算法。
  • 可达性分析算法

    • 基本思路:以根对象集合(GC Roots)为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
    • GC Roots包括:①虚拟机栈中引用的对象;②类静态属性引用的对象;③方法区中常量引用的对象;④本地方法栈中(Native方法)引用的对象;⑤被synchronized持有的对象;⑥jvm内部的引用。

finalize #

  • 垃圾回收之前总会调用finalize方法,该方法可以被重写:通常是在这个方法中进行一些资源释放和清理的工作。

  • 不要主动调用该方法,该方法的执行时间是没有保障的,它完全由gc线程决定。垃圾回收机制会主动调用该方法。

  • finalize方法只能被调用一次。

  • 对象的三种状态:可触及;可复活;不可触及。

  • 判断一个对象是否可回收,至少经历两次标记过程:

    1. 如果对象到gc roots没有引用链,则进行第一次标记
    2. 进行筛选,判断该对象是否有必要执行finalize方法:①如果对象没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,该对象被判为不可及的;②如果对象重写了finalize方法且还未执行过,那么该对象将被插入到F-Queue队列中,由一个低优先级、虚拟机自动创建的Finalizer线程区执行它;③finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次标记,如果对象在finalize方法中重新与引用链上的任意一个对象建立了联系,那么在第二次标记时它将被移出“即将回收”的集合。之后,如果该对象再次出现没有引用存在的情况下,finalize方法不会再次调用,对象会直接变为不可及的状态。也就是说一个对象的finalize方法就被调用一次。

垃圾收集算法 #

  • 标记-清除算法(Mark-Sweep)

    • 执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作,第一项是标记,第二项是回收。
    • 标记:从根节点开始,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
    • 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其header中没有被标记为可达对象,则将其回收。
    • 优点:实现简单。
    • 缺点:效率不够高;导致STW;会导致内存空间不连续,产生内存碎片,需要维护一个空闲列表。

标记-清除算法

  • 复制算法

    • 执行过程:将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还活着的对象复制到另外一块上面去,然后再把已使用过的内存空间一次清理掉。
    • 优点:实现简单;运行高效,不会出现内存碎片。
    • 缺点:需要两倍的内存空间。
    • 适用场景:存活对象比较少,垃圾对象比较多的场景。

复制算法

  • 标记-压缩(整理)算法
    • 执行过程:第一阶段和标记-清除算法一样,从根节点标记所有被引用的对象。第二阶段是将所有存活对象整理到内存的一端,按顺序排放。之后清理边界外的所有对象。
    • 指针碰撞:如果内存空间以规整、有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫指针碰撞(Bump the Pointer)。
    • 优点:解决了内存碎片化的问题;消除了复制算法中内存减半的问题。
    • 缺点:效率低于复制算法和标记-清除算法;移动对象的同时,还要调整引用的地址;会导致stw。

标记-整理算法

  • 分代收集算法

    • 不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以提高回收效率。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
  • 增量收集算法

    • 如果一次将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成。
    • 总的来说,增量收集算法的基础仍然是传统的标记-清除算法和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集器以分阶段的方式完成标记、清理和复制工作。
    • 缺点:线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量下降。
  • 分区算法

    • 将堆空间划分为连续不同的小区间region,每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。