深入理解java虚拟机读书笔记_2020.02.18

第一部分:java的发展史,jvm的发展史

主要内容:

  1. java的出现:Green Project,开发在电子产品上运行的程序架构

  2. jvm迭代:Classic Vm -> Exact Vm ,HotSpot Vm (解释器跟编译器的混合执行)

  3. jdk跟jre:java开发工具(java语言,java api,jvm),java运行环境(java se api,jvm)

  4. java的优势:结构严谨,面向对象,内存管理与指针越界管理,热点代码检测,运行时编译及优化,一次编写,到处运行,庞大而完整的生态

联想:

  1. java9,10,11,12,13新特性:模块化,集合工程,各种增强,局部类型推断var关键字,加入json解析api,httpclient等

问题:

  1. 为啥解释器跟编译器混合执行就牛逼了?

第二部分:jvm的内存管理

主要内容:

  1. jvm的内存划分

    1. 程序计数器:记录指令执行

    2. 虚拟机栈:

      1. -Xss

      2. java方法执行时局部变量与栈帧的分配,请求的栈深度过大-StackOverFlowError,无法再申请内存分配栈帧-OutOfMemoryError

    3. 本地方法栈:

      1. -Xos

      2. 本地方法执行时的内存分配,再hotsopt中,不区分本地方法栈与虚拟机栈

    4. java堆:

      1. -Xms 最小大小,-Xmn最大大小

      2. 常规对象分配的地方

      3. OutOfMemoryError java heap space

    5. 方法区:

      1. -XX:MaxPermSize

      2. 类元数据+运行时常量池

      3. OutOfMemoryError perm space

    6. 直接内存区:

      1. 在虚拟机外分配使用的内存,常见于NIO中使用,避免内存复制
  2. 对象的创建

    1. 对象内存的分配

      1. 指针碰撞,计算对象大小,划分内存,要考虑同步

      2. TLAB,在线程自己的内部空间里面分配

      3. 需要结合GC方式

    2. 对象的内存布局

      1. 对象头:hash code,分代年龄,锁状态,类型指针(确定对象是哪个类的实例)

      2. 对象实例数据

      3. 对齐空间

  3. 垃圾回收

    1. 对象回收:

      1. 先判断对象是否已死:

        1. 引用计数法,无法识别两个死亡对象互相引用的情况

        2. 可达性分析,通过枚举Gc Roots,如:常量引用,静态变量引用,栈上的引用

      2. 回收时机:安全点

    2. 回收算法

      1. 复制算法:内存消耗大

      2. 分代整理:基于复制算法的思路

      3. 标记整理:

      4. 标记清除:内存碎片

    3. 回收器

      1. 新生代

        1. serial:stop the world

        2. ParNew:stop the world ,回收多线程并行

        3. Parallel Scavenge:类似于ParNew 更注重于控制吞吐量

      2. 老年代

        1. serial old:stop the world

        2. parallel old:stop the world ,回收多线程并行

        3. cms:

          1. 标记:stop the world

          2. 并发标记:并发

          3. 重标记:stop the world

          4. 并发回收:并发,回收时,用户进程也在进行,所以会产生浮动垃圾。所以内存上需要留有余地

      3. G1 能适用整堆上的垃圾回收

  4. 故障排查,性能分析工具

    1. bin目录下的自带工具

    2. jconsole,visualvm

  5. 调优案例总结

    1. 高性能硬件

      1. 采用逻辑集群+负载均衡的方式来发挥高性能硬件的优点

      2. 回收过大的内存区域,会导致GC时间过长

    2. 集群通讯导致大对象累积在内存中

    3. 使用NIO。DirectMemory的回收直到full gc才会顺便回收,当堆外内存无法分配也会导致OutOfMemory

    4. Runtime.exec()会创建系统进程,要慎用

    5. 依赖其他的远程服务太过耗时,导致线程,Socket挂起,直到jvm崩溃

    6. 数据结构导致内存翻倍,比如long 占用8B,Long 占用24B

    7. 桌面程序最小化之后,工作内存转移到磁盘,恢复的时候可能导致不正常的GC

联想:

  1. jdk8新增MetaSpace,将类元数据从方法区中移动到meta space

  2. G1的回收:[http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html]{.underline}

问题:

第三部分:

主要内容:

  1. 平台无关性:java通过虚拟机+字节码文件实现了平台无关性,jvm不仅能识别java编译器生成的字节码文件,还有很多语言在字节码文件的基础上,实现了自己的编译器,把自己的语言规范编译成字节码文件交给jvm执行。字节码文件有自己一系列的定义,比如,文件开头1-4字节为魔数,接下来的5-8字节为版本号,9-10字节代表常量池,同时定义了很多结构体,跟固定的协议,来解析class文件,其中方法体的代码是通过编译成jvm 指令存放到code属性里面

  2. 类加载的时机

    1. new,类变量的访问与操作,静态方法调用

    2. 对类反射调用

    3. 初始化一个类的时候,若父类没有初始化,要先触发父类的初始化

    4. 虚拟机启动时,main方法所在的主类

    5. 动态语言支持如MethodHandler实例解析结果的方法句柄对应的类没有初始化(类似于反射)

  3. 类的加载过程

    1. 加载:将字节码的码流加载入jvm

    2. 验证:

      1. 格式验证(魔数,版本等),通过后码流便按结构进入了方法去

      2. 元数据验证

      3. 字节码验证(程序语义验证)

      4. 符号引用验证

    3. 准备

      1. 类变量的分配内存空间

      2. 设置类变量初值

        1. static 变量 0,false,null等

        2. static finnal 常量 显式的初始值

    4. 解析

      1. 符号引用替换成直接饮用
    5. 初始化

      1. 执行cinit(编译器自动收集所有类变量的赋值动作跟static静态代码块),并发问题由jvm控制,多线程并发也保证只执行一次,与init不同,init方法上实例构造器
    6. 使用

    7. 卸载

  4. 类加载器

    1. Bootstrap ClassLoader 加载java_home下lib目录的jar

    2. Extension ClassLoader 加载java_home下lib/ext目录的jar

    3. 类+类加载器唯一确定一个类

    4. 双亲委派模型,越是基础的类,越由上层加载器加载

    5. 双亲委派模型的破坏

      1. jndi,jdbc等,需要上层类加载器主动要求下层的类加载器帮忙加载

      2. osgi:模块化

  5. 字节码执行引擎

    1. 栈帧

      1. 局部变量表

      2. 操作数栈

      3. 动态连接

      4. 方法返回地址(恢复调用者的栈帧)

      5. 附加信息

    2. 方法调用

      1. 解析(方法的调用版本在运行期是不可以改变的,这类方法的调用叫解析,如:静态方法,私有方法,这些方法在类加载的解析过程中,会把符号引用替换为直接引用)

      2. 分派

        1. 静态分派:依赖静态类型定位方法执行版本如:方法重载,在编译期就能确定

        2. 动态分派:在运行期根据实际类型确定方法执行版本如:子类重写父类方法,invokevirtual指令会在实际类型中找方法

    3. java的动态语言特性支持

      1. 动态类型语言:在运行期才会去做类型检查,所以c,c++,java本身是一门静态类型语言

      2. jdk7 提供了invokedaynamic指令,invoke包 执行方法句柄参数

    4. 基于栈的执行引擎

      1. 物理机大多基于寄存器做执行引擎,速度快,但与硬件耦合太紧

      2. 虚拟机基于栈的架构,速度稍慢,可移植性强

联想:

问题:

第四部分:程序编译与代码优化

主要内容:

  1. 编译期优化

    1. 语法糖

      1. 范型,增强for循环,自动拆装箱,
  2. 运行期优化

    1. 解释器:把字节码解释机器吗

    2. 编译器:把整个方法,或者某段循环的代码,进行优化编译成机器码

    3. 编译优化:

      1. 公共子表达式消除

      2. 数组边界检查消除(如果99%的情况不回越界,可以不判断越界情况直接使用,并处理越界异常,同样适用于NPE)

      3. 方法内联:如果在运行时不回出现多个方法版本,可以将代码内联,减少栈分配与复原

      4. 逃逸分析:如果能确定对象不回逃逸出某个范围,则可以使用栈上分配,同步消除,标量替换

联想:

问题

第五部分:高效并发

主要内容:

  1. java内存模型

    1. 内存操作

      1. lock

      2. unlock

      3. read:从主内存read到工作内存

      4. load:从工作内存load到变量副本

      5. use:工作内存到执行引擎

      6. assign:赋值给工作内存的变量

      7. store:工作内存的变量值store到主内存

      8. write:把主内存的值放入到主内存的变量中

    2. volatile的使用

      1. 语义:防止指令重排,变量的可见性

      2. 使用的约束:不依赖当前值或者只有单一的线程修改,不与其他的状态变量参与到不变约束

      3. 变量规则(1-4 实现可见,5,6实现防止指令重排)

        1. use 之前 必须 load

        2. load 之后 必须 use

        3. store 之前 必须 assign

        4. assign 之后 必须 store

        5. A->V :use + assign,P->V:read+write,B ->W: load+store,Q -> W: read+write

        6. lock 空操作的内存屏障

  2. 先行发生原则:

    1. 程序次序规则:一个线程内,书写在前面的先行发生于书写在后面的

    2. 管程锁定:unlock 先行发生于后面对同一个锁的lock

    3. volatile:写操作先行发生于后面的读操作

    4. 线程终止:线程中的所有操作先行发生于对此线程的终止检测

    5. 线程中断:对线程interrupt调用先行发生于对中断的检测

    6. 对象终结:对象初始化限行发生于对象finalize方法

    7. 传递性:A先行发生于B,B 。。。 C 则 A。。。C

  3. java线程

    1. jdk1.2之前使用用户线程实现,1.2之后使用操作系统的的原生的线程模型实现

    2. 线程调度

      1. New

      2. Ruing

      3. Waiting/Timed Waiting

      4. Blocked

      5. Terminated

  4. 线程安全与锁优化

    1. 自旋锁:在锁能很快释放掉的情况下,可以通过自旋在不放弃cpu的情况下,减少线程调度的开销

    2. 自适应自旋:通过统计上一次的自旋情况来决定是否要自旋,还是挂起等待

    3. 锁消除:局部对象的引用安全,没有逃逸等,可以使用锁消除

    4. 锁粗化:多次重复对同一个锁操作,可以扩大锁范围

    5. 轻量级锁:使用cas方式加锁

    6. 偏向锁:在持有锁之后,没有竞争

联想:

解决java中的并发问题,其实变成是捋清楚内存操作的顺序

问题: