一、synchronized工作原理
修饰普通方法,锁住的是当前对象的实例
修饰静态方法,锁住的是当前Class对象
修饰代码块,锁住的是括号里的对象
原理:
是基于监视器锁实现的,使用monitorenter和monitorexit指令完成。monitorenter编译成字节码后插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处。
一个线程开始会执行monitorenter指令尝试获得监视器锁的所有权。获得后,监视器进入数为1,并记录线程的所有者为当前线程,其他线程阻塞。重复进入,进入数+1。执行完后,通过monitorexit将进入数-1,直到进入数为0,其他线程才能获得锁。
锁:
1.自旋锁:锁的持有时间比较短,通过循环等待的方式来避免挂起恢复造成性能浪费。
2.偏向锁:当只有同一个线程获取的时候,就不需要等待,直接就能访问。
3.轻量级锁:多个线程大部分情况下也不会存在竞争,所以这个时候可以使用轻量级锁。
当当线程成进入代码同步块的时候,如果此时为无锁状态,虚拟机首先在栈帧中创建一个叫Lock record的空间,然后将对象头拷贝到该空间中,使用CAS更新对象头中指向Lock record的指针,并将lock record中owner指向对象头。如果更新成功,那么该现成持有该对象的锁,锁标记为00,表示轻量锁。
如果失败,检查对象头中是否是指向该线程的栈帧,如果是,就可以直接进入,如果不是,就需要变为重量级锁
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个都有。
五、OOM
-
java虚拟机栈溢出 stack
-
方法区溢出 directMemory
-
运行时常量溢出 constant
-
堆溢出 heap
六、垃圾回收
1.判断方法:
引用计数:
给对象添加一个引用计数器,被引用一次就+1,引用失效就-1,计数器为0,对象就是不可能再被使用的。缺点是没法解决循环引用问题。
可达性分析:
通过gcroot的对象为起点,从这些节点向下搜索,走过的路径成为引用链,当一个对象没有引用链时,则这个对象不可用。
Java虚拟机栈,本地方法栈,方法区中类静态属性、常量引用的对象。
引用:
-
强引用: new 不回收
-
软引用:通过继承SoftReference实现,系统要发生内存溢出前回收。
-
弱引用:通过继承WeakReference实现。对象只能生存到下一次垃圾回收之前。
-
虚引用:通过继承PhantomReference实现。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
2.回收算法:
标记-清除法:
分为标记、清除两个阶段,首先标记出所有需要回收的对象,然后统一回收。
1.效率问题,两个阶段效率都不高
2.空间问题,产生大量控件碎片。
复制算法:
将内存按容量划分为大小相同的2块,每次只使用一块,当一块用完了,就将存活的对象复制到另一块。然后把当前清空。
缺点:把内存缩小了一半。
商业虚拟机都采用这种,不过是将内存划分为3块,一般是8:1:1,回收时,将存活的对象从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.筛选回收:对各个区的回收价值和成本进行排序,根据用户所希望的回收时间来指定回收计划。
特点:不会产生碎片。可预测停顿。
4.gc 频率
根据运行时长和回收次数进行计算。 只有满了才会回收。
5.jvm 调优
1.监控gc状态
2.分析gc频率
3.修改新老内存大小,新生区比例,选择合适的垃圾回收器。
七、CAS
1.简介
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_WAITING,WAITING)我们可以通过调用线程的interrupt()方法来中断线程的等待,此时线程会抛InterruptedException异常。但是当线程处于BLOCKED状态或者RUNNABLE(RUNNING)状态时,调用线程的interrupt()方法也只能将线程的状态位设置为true。停止线程的逻辑需要我们自己去实现。
1)interrupt()方法,这是Thread类的实例方法。对一个线程调用interrupt()方法表示请求中断这个线程。该方法是唯一能将中断状态设置为true的方法。中断后直接退出。
2)isInterrupted()方法,这是Thread类的实例方法。测试线程是否已经中断,也就是测试线程中断状态是否设置为true。
3)Thread.interrupted(),这是Thread类的静态方法。判断线程是否被中断并清除中断状态。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(再次中断的情况除外)。
7.3 join
Thread 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执行完,即不是Alive
7.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.资源的连接,当前线程一直运行的话,资源就不会释放。