要点提炼之深入了解JVM虚拟机

Posted by Chejdj Blog on September 9, 2018

前言

其实上半年的时候就已经读过了周志华老师的《深入了解JVM虚拟机》,但是知识这东西,不用出来就会很容易被遗忘,所以近日又开始捡起这本书,做一些笔记,对知识梳理一下。

JVM的Java程序内存划分区域

程序计数器: 当前线程执行行字节码的行号指示器
Java虚拟机栈: Java方法执行的内存模型,每个方法在执行的同时创建一个栈帧,用来存储局部变量表,操作数栈,方法出口等(每个方法执行,对应着一个栈帧的入栈和出栈)
本地方法栈: 虚拟机使用native方法
Java堆: Java虚拟机管理的内存最大的一块,被所有线程共享(存放对象的实例)
方法区: 各个线程共享的内存区域,用来存储加载类的信息,常量,静态变量

一般来说分为两大阵营
线程共享: 堆,方法区
线程私有: 虚拟机栈,本地方法栈,程序计数器

运行时常量池: 方法区的一部分,用来存放编译时期的各种字面量和符号引用,当然也有动态性(String。intern()可以检查在当前常量池有没有该String,没有加进入,所以这里说编译时期不好)

直接内存: (非Java虚拟机管理部分),一般在native中申请(使用DirectByteBuffer对象对这块内存引用操作)这部分内存用于不受Java虚拟机管理,所以需要自己清理,而且一般来说系统会规定一个程序使用多少内存,如果不够就可以使用这种方法

垃圾收集器与内存分配策略

基础知识
  1. 如何判断一个对象是可以回收的? (1) 引用计数算法: 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,失效减去1,当引用为0的时候就代表应该清理,但是该方法不能解决对象之间的互相循环引用
    (2) 可达性分析算法: 通过可达性分析判定对象是否存活,基本思想从GCRoots对象为起始点,从节点向下搜索,搜索走过的路径称为引用链,当一个对象到GCRoots没有任何引用链连着,该对象就判定为可回收对象。

GRoots:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用类型:

  • 强引用 : Object obj=new Object 永不回收
  • 软引用: SoftReference 内存不足的时候,进行回收
  • 弱引用: gc处理
  • 虚引用: 因为被回收的时候,触发一个系统通知,可用于追踪类的回收
  1. 如何判定一个对象是否GC?
    可达性分析不可达的对象,会处于暂缓刑的阶段,真正宣告对象死亡经历了两次标记的过程:
    (1) 第一次标记:就是可达性分析不可达的对象,这里会对这些对象筛选,筛选的条件就是此对象是否有必要执行finalize()方法,当Object没有覆盖finalize()方法,或则fanlize()方法已经被调用了,则虚拟机不执行该方法。
    如果执行该方法,则把该对象放在F-Queue队列中执行,稍后GC对F-Queue队列进行一次小规模的标记,如果在fanlize()方法中把自己的引用和GCRoots对象关联,则移除进行“即将回收”集合,否则第二次标记清除
    (2) 对即将回收的集合,二次标记
  2. 方法区的回收
    永久代收集:
    (1)判定一个常量无用:没有该常量的引用
    (2)判断一个无用的类: 该类所有的实例被回收了,加载该类classloader回收了,该类的java.lang.class无任何地方引用
垃圾回收算法
  • 标记-清除算法
    首先标记所有需要回收的对象,在标记完成之后统一回收所有被标记的对象
  • 复制算法
    将内存分为大小相等两块,当一块用完了之后,将还存活着的对象复制到另一块上去,然后这块一起清理掉
  • 标记-整理
    先标记回收对象,让所有存放对象向同一端移动,然后清理掉边界以外内存
  • 分代收集算法
    将内存分为新生代和老年代,新生代(GC次数多)采用复制算法,老年代存活率高,使用标记-清除
    术语介绍:
    新生代包括: Eden 和Survior 老年代: OldEden
    MinorGc: 一般是指新生代GC
    FullGc: 老年代,一般也会触发一次MinorGc
    分代收集算法与内存的分配原则
  • 对象优先在Eden分配,无空间分配时,进行一次MinorGc,原先在Eden区就可以移动到Survior区,如果Survior区空间不够,就把它们通过担保机制放到OldEden去
  • 大对象的话直接进入老年区(Eden和Survior放不下)
  • 长期存活的对象进入老年区(每一次MinorGc的时候,将对象在Eden区移入到Survior区,每一次MinorGc年龄就增加1,增加到一定程度(比如15)就移动到老年区)
  • 动态年龄判断(如果Survior空间中相同年龄所有的对象大小总和占有Survior空间一半,则年龄大于或则等于该年龄的对象直接进入老年代)
  • 空间担保机制

发生MinorGC 检查老年代最大可用空间>新生代所有对象的总空间,大于则认为MinorGC安全,不成立
看虚拟机是否设置了允许担保失败,若允许,检查老年代可用空间>历次晋升老年代对象的平均大小
大于就进行MinorGC,如果小于就进行FullGC

虚拟机类加载机制

类的加载过程: 加载,链接,初始化,验证,准备,解析(对于一些链接怎么个过程,验证怎么个过程就留到后面有需要的时候学习,这里就是浅尝辄止)

  • 类加载的时机: new这个类,该类没有初始化(类的初始化),或则读取该类的静态字段
    对类进行反射调用,没有初始化,子类初始化的时候,父类还没有,父类加载
    类的初始化:执行类的构造器(所有的类变量的赋值动作和静态语句块合并产生)
  • 类加载器 classloader 每一个类的加载器不一致, instanceof(我们经常判断那个对象是否是该类,equals也一样),比较的就是类加载器是否相同
    双亲委派模型
    概念:当一个类加载器收到类加载请求,自己不先尝试加载,把请求给父类加载完成,当父类不完成的时候才自己完成。
    classloader双亲委派模型
    classloader: 一般分为启动类加载器,扩展类加载器,应用程序加载器,他们每一个加载器都去加载java包中某部分的类
    官方推荐我们自己定义类加载器的时候,推荐我们使用双亲委托模型,避免遇到两个类相同但是不是同一个类加载器,判定为不同

高效并发

高效并发,我觉的是非常重要的,具体的线程之间的操作的话可以读一读高洪岩老师的《Java 多线程编程核心技术》,结合实例理解深刻一点,但是我读感觉有点在凑字数的嫌疑,例子还是挺好的(有些重复例子)

  1. Java内存模型
    所有的变量都存储在主内存中,每一个线程还是自己的工作内存,保存该线程使用到的变量,从主内存中拷贝一份(这里变量: 实例字段,静态字段和构成数组对象的元素)
    Java内存模型
    save和load是原子操作,但是合起来就不是,所以我们需要这种模型,我们就需要懂得多线程的时候如何保证线程安全
  2. volatile的理解
    (1) 保证此变量对所有线程的可见性,不保证原子性
    a. 运算结果不依赖变量当前值
    b. 变量不需要与其他状态变量工作参与不变约束
    (2) 禁止指令重排序优化
    指令排序原则: 普通变量仅仅保证该方法执行过程中所有依赖赋值结果地方都能得到正确结果 先行先发生原则:若A先于B,A操作影响B,B能观察到

线程安全和锁优化

线程安全概念: 多个线程对同一个对象进行访问的时候,不管线程如何调度,都能得到正确的结果。

线程安全实现的几种方法

(1)同步互斥
synchronized: 同步块对于同一条线程可重入
ReentrantLock: 等待可中断,实现公平锁以及锁绑定
a. 等待可中断: 持有锁线程长期不释放锁,正在等待锁的线程放弃等待
b. 公平锁: 等待同一个锁,按照申请锁的时间顺序依次获得锁
c. 锁绑定多个条件: 可以同时绑定多个condition,而synchronized只能通过wait,notify,notifyall
(2)非阻塞同步
基于冲突检测乐观并发策略,先进行操作,没有线程争用共享数据,操作成功,有争用在采取其他措施,比较有名的是“CAS指令”
CAS: 内存中的值V,旧的预期值A,新值B,仅当V符合A时,处理器采用B值赋值给A
(3)无同步代码
不依赖堆上的数据,和公共系统资源,状态量由参数传入线程本地(这部分说了和没有说一样。。)

锁优化

下面我们只要知道Jvm对锁进行一定的优化

  1. 锁消除
    对一些代码要求同步,但是被检测不可能存在共享数据竞争的锁进行锁消除(判断该变量是否逃逸出去)
  2. 自旋锁和自适应自旋
    挂起和恢复线程消耗大,让后面请求锁线程请求锁线程不放弃处理器等一下,执行循环

题外话

不知不觉,已经来到公司一个月+1个礼拜了,自己还是需要多看看公司前人写的代码,导师也经常吐槽前人写的代码太过随意,想要重构其中比较关键的代码,我呢就努力的看懂这些部分,看看导师如何重构,导师也经常开小灶给我讲他打算如何重构,积累到一定量我也写一遍博客,自己参与的项目也上线了,经理发了某个用户的吐槽,说实话,心里有点不舒服,毕竟自己做的功能,没有达到满意的效果,自己还是要加油,最近加快速度的看代码,然后制定以下学习的方向的,下一个礼拜打算学习一下retrofit的内部的总体思路,gson库和Support-Annotation注解库内部思路,主要涉及网络传输这一块