JVM 第四节 垃圾回收和锁

垃圾回收

大部分对象朝生夕死,但是存留下来的会生存很久.
有点像是心灵鸡汤.但确实是如此.

  • 堆空间分为两大块
    • 新生代
      • Eden
      • Survivor1
      • Survivor2
    • 老年代 Tenured

不同的代,垃圾对象的回收频率不一样.因此也会采用不同的算法.

JVM 第四节 垃圾回收和锁

标志-复制法

新生代:
Eden 会占较大的空间.Survivor1 ,2 会占相对较少的空间.

这里可以有两个指针 from , to .分别指向 Survivor1,2
当GC时,Eden 与 from 中存活的对象放入 to

这里有一个小优化,
GC后,可以将 from , to 的指针对调.这样就可以保证 to 指向的那块内存一直都是空的

新生代的对象转移到老年代

  • 有一个 阈值 (默认15),GC下存活次数超过之后,可以将这个对象转移到老年代
  • 某个 survivor 的占用的空间超过 50% ,存活次数较高的对象也会转移到老年代

优点

  • 理想情况下,需要复制的对象很少,效率很高
  • 不用对整个堆空间进行扫描

缺点

  • 假如有老年代的对象引用了新生代的对象,则会进行GC Roots 进行全堆扫描

JVM 中利用卡表来解决这个问题

synchronized 及 JVM 锁

synchronized

  • 可以直接声明一个 synchronized 代码块
  • 标记静态方法 clazz
  • 标记成员方法 this

当声明 synchornzied 代码块时,编译而成的字节码会包含 monitor_enter,monitor_exit
这两个指令都会消耗操作数栈上的一个引用类型元素(也就是synchronized 引用的那个)

重量级锁

重量级锁是JVM中最基础的锁实现. JVM 会在阻塞加锁失败的线程,并且在目标被释放的时候,
唤醒这些线程.

Java线程的阻塞及唤醒,都是依靠 OS 来操作完成的.这些操作设计系统调用,从用户态到内核态,
其开销非常大.

所以为了避免昂贵的线程阻塞,唤醒操作,JVM提供了 自旋状态

其实就是一种为了不阻塞,让程序一直运行下去.而去跑一些无用的指令.
希望在跑无用指令时,能过得到锁被释放出来.

类似,等红绿灯. Java线程的阻塞相当于熄火,自旋相当于怠速.
如果红灯时间长,熄火合适,但是时间较短的话,怠速合适.

但是,我们知道谁也无法预支未来,更别说机器了.

所以,JVM 只能自适应地采用自旋.具体算法,我也不清楚…

但是,自旋还带来了一个副作用.

不公平的锁机制: 可能处于阻塞状态的线程,并没有办法立刻获得锁,
反而是 自旋的线程 优先获得了锁.

想像一下,你开车等红灯熄火了,旁边的车怠速.
突然绿灯亮了,你还需要点火,挂档啥的. 旁边的车,一脚油门走了.气不气!

轻量级锁

JVM 中也存在,多个线程在不同时间请求同一把锁. 存在较少的竞争.
针对这种情况, JVM 提供了轻量级锁.避免重量级锁的阻塞及其唤醒.

对象头中的标记字段,最后两位被用来表示对象的锁状态.

  • 00 轻量级锁
  • 01 无锁或偏向锁
  • 10 重量级锁
  • 11 GC的标记

JVM 在加锁时,会判断是否已经是重量级锁,如果不是则会在当前栈帧中划出一片区域,
作为该锁的锁记录.并且将锁对象的标记字段赋值到该锁记录中.

JVM 会尝试使用 CAS (compare and swap)
操作来替换锁对象的标记字段.

通过对比期望值

  • 是,则获得锁,继续执行.
  • 不是
    • 该线程获得了同一把锁,将锁记录清零.代表该锁被重复获取.
    • 其它线程持有该锁. JVM 会将这个锁提高到重量级锁.

偏向锁

这是一个异常乐观的锁,乐观不足已形容.所以叫它偏向.
它是认为 始终只有一个线程请求某一把锁.