ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JVM必备知识点

JVM必备知识点 一、synchronized工作原理修饰普通方法锁住的是当前对象的实例修饰静态方法锁住的是当前Class对象修饰代码块锁住的是括号里的对象原理是基于监视器锁实现的使用monitorenter和monitorexit指令完成。monitorenter编译成字节码后插入到同步代码块的开始位置monitorexit插入到方法结束处和异常处。一个线程开始会执行monitorenter指令尝试获得监视器锁的所有权。获得后监视器进入数为1并记录线程的所有者为当前线程其他线程阻塞。重复进入进入数1。执行完后通过monitorexit将进入数-1直到进入数为0其他线程才能获得锁。锁1.自旋锁锁的持有时间比较短通过循环等待的方式来避免挂起恢复造成性能浪费。2.偏向锁当只有同一个线程获取的时候就不需要等待直接就能访问。无锁状态第一个线程访问的时候将markword的锁标志位设置为偏向锁将线程id记录到markword中。同意线程再次进入开销很小只需要线程id对比。偏向锁不计数所以遇到竞争只能升级。3.轻量级锁多个线程大部分情况下也不会存在竞争所以这个时候可以使用轻量级锁。第二个线程进入的时候先cas加自旋等待的方式获取锁。轻量级锁获取锁过程当线程成进入代码同步块的时候如果此时为无锁状态虚拟机首先在栈帧中创建一个叫Lock record的空间然后将对象头拷贝到该空间中使用CAS更新对象头中指向Lock record的指针并将lock record中owner指向对象头。如果更新成功那么该现成持有该对象的锁锁标记为00表示轻量锁。如果失败检查对象头中是否是指向该线程的栈帧如果是就可以直接进入如果不是就需要变为重量级锁同意线程重复进入会创建多个lock record.4.重量级锁锁标记为10指针指向monitor对象监视器锁轻量级锁发生竞争就切换到重量级锁用的是计算机内核阻塞。二、划分1.程序计数器线程私有用于代码执行2.Java虚拟机栈。线程私有存储方法相关的信息如局部变量、方法出口等。一个方法从调用到结束就对应一个栈帧从Java虚拟机栈的入栈和出栈的过程。3.本地方法栈。线程私有为使用到的native方法服务。4.方法区。内存共享存储已被虚拟机加载的类信息常量静态变量也就是编译后的代码数据。5.java堆。内存共享存储对象实例和数组。三、对象内存分布1.对象头分为2部分第一部分存储自身运行时数据哈希码GC分代年龄锁状态标志线程持有的锁、偏向线程 ID、偏向时间戳等。32位机器占32bit,64位机器占64bit。官方称为mark word.第二部分是类型指针即对象指向它的类的元数据指针虚拟机通过这个指针确定这个对象是哪个类的实例。如果是数组还得记录数组的长度。2.实例数据程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。3.对齐填充不是必然需要主要是占位保证对象大小是某个字节的整数倍。四、volatile关键字1.定义的共享变量存储在主存中而每个线程都有私有本地内存修改某个值会先拷贝副本到本地内存修改完后同步到主内存。2个线程会有不可见的问题volatile关键字修饰的变量会在写变量的时候强制刷入主存并使其他线程中的变量缓存无效。2.禁止指令重排 重排操作是为了优化性能保证的是最终执行结果不变但在多个线程中会有问题。3.无法保证原子性。syn修饰可以保证3个都有。五、OOMjava虚拟机栈溢出 stack方法区溢出 directMemory运行时常量溢出 constant堆溢出 heap六、垃圾回收1.判断方法引用计数给对象添加一个引用计数器被引用一次就1引用失效就-1计数器为0对象就是不可能再被使用的。缺点是没法解决循环引用问题。可达性分析通过gcroot的对象为起点从这些节点向下搜索走过的路径成为引用链当一个对象没有引用链时则这个对象不可用。Java虚拟机栈本地方法栈方法区中类静态属性、常量引用的对象。引用强引用 new 不回收软引用通过继承SoftReference实现系统要发生内存溢出前回收。弱引用通过继承WeakReference实现。对象只能生存到下一次垃圾回收之前。虚引用通过继承PhantomReference实现。无法通过虚引用获取一个对象的实例为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。2.回收算法标记-清除法分为标记、清除两个阶段首先标记出所有需要回收的对象然后统一回收。1.效率问题两个阶段效率都不高2.空间问题产生大量控件碎片。复制算法将内存按容量划分为大小相同的2块每次只使用一块当一块用完了就将存活的对象复制到另一块。然后把当前清空。缺点把内存缩小了一半。商业虚拟机都采用这种不过是将内存划分为3块一般是811回收时将存活的对象从Eden和其中一块存活区复制到另一块存活区。当存活区空间不够就需要老年代了。标记整理算法当对象存活率高的时候就需要进行较多的复制效率会变低。所以老年代不适合。标记出需要清理的对象然后让存活的对象向一端移动然后清理掉端边界以外的内存。分代回收算法将内存划分为新生代和老年代新生代只有少量对象存活采用复制算法老年代采用标记整理算法。3.垃圾回收器Serial收集器串行收集器新生代收集器复制算法。一个单线程收集器在进行垃圾回收时必须暂停其他工作线程。ParNew收集器新生代收集器复制算法。上一个收集器的多线程版Parallel Scavenge收集器新生代收集器采用复制算法。也是并行的多线程收集器。注重高吞吐量特点gc动态调节可以自动调节新生代内存大小比例。Serial Old 收集器是serial 收集器的老年代版本采用标记整理算法。Parallel Old 收集器老年代标记整理算法。是Parallel Scavenge收集器的老年代版本。注重高吞吐量,平衡每次回收时间和回收间隔。CMS收集器一种以获取最短回收停顿时间为目标的收集器。老年代收集器特点基于标记-清除算法实现。并发收集、低停顿工作过程1初始标记 标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题2并发标记 进行GC Roots Tracing 的过程找出存活对象且用户线程可并发执行。3重新标记 为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。4并发清理 对标记的对象进行清除回收。缺点对CPU资源非常敏感。无法处理浮动垃圾可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。(因为使用并发标记所以得预留一部分空间如果空间不足用户线程就会出现。)因为采用标记-清除算法所以会存在空间碎片的问题导致大对象无法分配空间不得不提前触发一次Full GC。G1收集器新生代和老年代收集器把这个内存划分成了大小相等的区域每个区域都会有一个对应的Rememberd set会记录当前区域中的对象被引用记录。特点管理整个堆、空间整合不会产生碎片、可预测的停顿、并行与并发工作过程1.初始标记仅标记GC Roots能直接到的对象2.并发标记从GC Roots开始对堆中对象进行可达性分析找出存活对象。3.最终标记为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。4.筛选回收对各个区的回收价值和成本进行排序根据用户所希望的回收时间来指定回收计划。特点不会产生碎片。可预测停顿。ZGC垃圾回收器优点低延迟GC停顿时间不超过10ms不会随堆内存增大而增大。超大内存支持支持8M到16T的内存返回缺点不适合cpu密集存活时间短对象创建过快的场景。原理1、三色指针Mark0,mark1,remapped。第一次回收的时候将活跃的标记为mark0,不活跃的为remapped如果当前页垃圾多就转移转移后的为remapped,没转移的为mark0。下次回收的时候用mark1。2、读屏障用户线程访问的时候进行标记处理。比如访问某个remapped对象就标记为mark0.3、基于Region的分页模型。将内存划分大中小三类不同的区域。4.gc 频率根据运行时长和回收次数进行计算。 只有满了才会回收。5.jvm 调优1.监控gc状态2.分析gc频率3.修改新老内存大小新生区比例选择合适的垃圾回收器。七、CAS1.简介compare and swap比较并交换。读取值计算好要修改的新值。如果读取的值和内存值一样则成功否则失败。2.实现AtomicInteger类就使用了。public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 var4)); ​ return var5; }修改失败会一直重试。3.缺点1.ABA问题将值从a修改为b,在修改为a。 可以用版本号解决。2.循环时间长开销大。八、线程7.1 wait notify notifyAll只能在同步代码块里面使用。wait作用1.当前线程进入等待队列执行monitor exit释放锁。2.挂起当前线程notify:1.调用后等当前线程释放锁后才会从等待队列中任意唤醒一个现成去竞争锁。notifyAll:唤起所有线程。7.2 interrupt当线程处于等待状态或者有超时的等待状态时TIMED_WAITINGWAITING我们可以通过调用线程的interrupt()方法来中断线程的等待此时线程会抛InterruptedException异常。但是当线程处于BLOCKED状态或者RUNNABLERUNNING状态时调用线程的interrupt()方法也只能将线程的状态位设置为true。停止线程的逻辑需要我们自己去实现。1interrupt()方法这是Thread类的实例方法。对一个线程调用interrupt()方法表示请求中断这个线程。该方法是唯一能将中断状态设置为true的方法。中断后直接退出。2isInterrupted()方法这是Thread类的实例方法。测试线程是否已经中断也就是测试线程中断状态是否设置为true。3Thread.interrupted()这是Thread类的静态方法。判断线程是否被中断并清除中断状态。换句话说如果连续两次调用该方法则第二次调用将返回 false再次中断的情况除外。7.3 joinThread t new Thread(); t.join();join方法有synchronized修饰锁住的是当前线程对象。等待当前线程结束。也就是等待t结束。然后调用方线程再执行。如果传时间调用方线程等待n毫秒。public final synchronized void join(long millis) throws InterruptedException { long base System.currentTimeMillis(); long now 0; ​ if (millis 0) { throw new IllegalArgumentException(timeout value is negative); } ​ if (millis 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay millis - now; if (delay 0) { break; } wait(delay); now System.currentTimeMillis() - base; } } }main线程进入锁住的是t线程。main函数执行到wait处进行等待。while的作用是当t.notifyAll被执行main线程被唤醒t线程还是Alive还会继续阻断main直到t执行完即不是Alive7.4 yeild将现成从运行状态转为ready状态八、类加载过程9.1 双亲委派一个类加载器收到类加载的请求后先让父类去加载如果父类没法完成子类才会去加载。好处1.保护核心api,不会被篡改 2.加载过的类不用再次加载9.2 类加载过程加载在内存中生成改类的Class对象作为类数据的访问入口验证Class文件字节流信息符合要求。准备为类的静态变量分配内存并初始化为默认值。解析将常量池中的符号引用替换为直接引用。(符号引用是一种描述符c里面的)初始化按顺序调用方法初始化数据。使用卸载9.3 对象初始化过程1.给对象分配内存空间2.初始化对象属性3.复制给引用变量十、TLAB多个线程同时创建对象可能会造成堆内存分配出现冲突。Thread Local Allocation Buffer为每个线程单独分配一块内存。内存泄露1.集合中放对象长生命周期的引用短生命周期。2.static变量持有对象无法释放。3.声明很大的数组。4.资源的连接,当前线程一直运行的话资源就不会释放。十一、ThreadLocalthread-ThreadLocalMap-entry[]-EntrythreadLocal, Valueentry中的key是weakreference。如果线程是核心线程或者可以重复使用那么ThreadLocalMap一直持有entry节点的强引用释放不了虽然方法执行完了threadlocal释放了但是entry[]数组中依然持有remove之后Entry节点的强引用就没了。Entry对象就能回收了。十二、线程池增加线程方法没有超过核心线程数创建核心线程。超过核心线程数加入队列。队列满了创建非核心线程。都满了 拒绝策略。拒绝策略1、抛出异常。2、当前线程处理。3、直接丢弃。4、丢弃最老的。5、自定义。关闭线程shutdown:将线程池状态置为shutdown,并不会立即停止:1.停止接收外部submit的任务2.内部正在跑的任务和队列里等待的任务会执行完3.等到第二步完成后才真正停止。shutdownNow:将线程池状态置为stop。一般会立即停止事实上不一定:1.和shutdown(一样先停止接收外部提交的任务2.忽略队列里等待的任务3.尝试将正在跑的任务interrupt中断4.返回未执行的任务列表。状态RUNNING正常接收并处理任务SHUTDOWN不接收新任务但会处理已提交的任务STOP不接收新任务不处理队列任务中断正在执行的任务TIDYING所有任务已终止准备执行terminated()钩子方法TERMINATED线程池彻底终止十三、AQS1、原理AQS的核心工作原理基于一个FIFO等待队列和一个同步状态state。其主要步骤如下同步状态AQS通过一个 int 类型的变量 state 来表示同步状态。子类通过重写 tryAcquire、tryRelease 等方法来定义获取和释放同步状态的逻辑。等待队列当线程无法获取同步状态时会被加入到AQS的FIFO等待队列中队列中的每个节点Node表示一个等待的线程。独占模式和共享模式AQS支持独占模式如独占锁和共享模式如共享锁、信号量。在独占模式下只有一个线程可以获取同步状态在共享模式下多个线程可以同时获取同步状态。模板方法AQS通过模板方法模式提供了通用的同步机制子类只需实现特定的同步逻辑。2、流程1、尝试获取同步状态获取失败加入阻塞队列最后将前一个节点status设置为SIGNAL:-1表示后面有节点需要唤醒。2、独占模式换新后面的一个共享模式循环唤醒后面所有的线程。3、锁的实现1、重入锁独占公平和非公平公平锁如果有线程等待就加入非公平锁会先尝试获取锁获取不到才去等待。2、信号量共享3、CountDownLatch共享4、状态问题SINGAL表示下一个节点要被唤醒。CANCEL: 节点被取消。CONDITION: 条件节点等待满足某个条件的时候会进入条件队列。1、当前线程执行condition.await将当前线程加入到condition队列2、完全释放掉ReentrantLock独占锁(执行await必先持有锁这里必须先释放锁)3、当前线程挂起等着进入等待(同步队列4、另一个线程执行signal唤醒signal唤醒会将线程从condition队列转移到等待(同步)队列当前线程转移到等待(同步队列后等待被它的前节点唤醒signal后会调用unlock。5、当前节点被唤醒后重新获取到独占锁后才能继续往下执行propagate状态1.6之前的bughead-1-A-1-B当有一个线程释放信号量的时候head变成(0)A去获取信号量信号量又称为0这个时候A判断信号量为0就不在唤醒下一个了。但是A判断之后又有别的线程释放信号量此时head是0不会去唤醒。A也不换唤醒此时B就阻塞了。for (;;) { Node h head; if (h ! null h ! tail) { int ws h.waitStatus; if (ws Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws 0 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h head) // loop if head changed break; }
返回列表