《深入理解Java虚拟机》读书笔记
中心主题第1章 走进Java虚拟机介绍Graal VM:在HotSpot的基础上增强的全栈虚拟机。HotSpot:热点检测,运行时间越长性能越好。第一次运行慢。版本升级:支持模块化,可以裁剪功能,重构垃圾收集器接口Substrate VM:提前编译,启动时间和内存占用少Java技术体系JRE:基础类库,Java虚拟机,集成库,用户界面APIJKD=JRE+Java语言+工具及工具API第2章 Ja
第1章 走进Java
虚拟机介绍
- Graal VM:在HotSpot的基础上增强的全栈虚拟机。
- HotSpot:热点检测,运行时间越长性能越好。第一次运行慢。版本升级:支持模块化,可以裁剪功能,重构垃圾收集器接口
- Substrate VM:提前编译,启动时间和内存占用少
Java技术体系
- JRE:基础类库,Java虚拟机,集成库,用户界面API
JKD=JRE+Java语言+工具及工具API
第2章 Java内存区域与内存溢出异常
运行时内存数据区域
-
线程共享
- 1.堆。几乎所有对象实例和数组都在堆分配,HotSpot有做分代划分,有些虚拟机是没有的。规范没有要求物理内存连续,但是要求逻辑内存连续,很多虚拟机为了实现简单,数组做成了物理连续。实例化或者扩容没有足够内存时抛出OOM。-Xmx200m -Xms200m 设置最大最小堆内存,-Xmn20m 设置堆的年轻代大小
- 2.方法区。也叫非堆,存放常量,被虚拟机加载的类型信息,静态变量,即时编译的代码缓存。HotSpot使用永久代实现方法区,jdk8用元空间实现。无法满足内存分配也会抛出OOM
-
线程私有
- 1.虚拟机栈。包括线程的内存模型,栈帧,局部变量表,基本数据类型和对象引用。操作数栈,动态链接,方法出口。编译期确定变量槽大小,栈帧深度超了或者栈容量太小会报StackOverflowError,动态扩容无法申请内存抛出OutOfMemoryEror。HotSpot不会动态扩容,所以不会因为扩容而OOM,但是开始申请内存就失败仍然会有OOM。-Xss128k 设置每个线程的栈大小,局部变量越多,栈帧越大,内存占用越大,栈深度越小。深度可以理解为递归深度。
- 2.本地方法栈,每种虚拟机的现实不一样,HotSpot直接跟虚拟机栈合二为一
- 3.程序计数器(不会抛出内存溢出异常)
内存分配算法
- 指针碰撞:管理的空闲内存连续,所以会有空间压缩,例如Serial,ParNew。每次申请划出一块连续空间出来
- 空闲列表法:标记哪些内存是空闲的,空闲内存不连续,例如理论上的CMS,只做内存清除没有压缩,其实CMS还是会拿到一小块内存缓冲区做指针碰撞
内存分配加锁方案
- 第一,加锁,多数使用CAS
- 第二,给线程划分一小块内存区域,叫做本地线程分配缓冲(Thread Local Allocation Buffer TLAB),内存在线程缓冲区分配(不用加锁),分配完了再加锁去获取更多内存,用-XX:+/-UseTLAB参数设定是否开启内存缓冲
对象的存储布局为三部分
-
第一,对象头
- 第一,Mark Word,保存对象hashcode,对象分代年龄,锁标志位等
- 第二,类型指针,指向类型元数据,不是所有虚拟机都有
-
第二,实例数据
-
第三,对齐填充(有些虚拟机要求是8字节的倍数)
对象头hashcode的理解
- 1.对象头存储的hashcode是调用调用了System#identityHashCode方法生成的,而不是对象重写的hashcode
- 2.为了节省对象头的存储空间(很多时候hashcode方法不会被调用到),只有调用过了hashcode方法,对象头才会存储hashcode
- 3.hashcode值的生成方式有多种,Open JDK有6种:随机数生成器,通过对象内存地址的函数生成,硬编码,通过序列,对象的内存地址强转为int,线程状态与xorshift结合
- 4.hashcode因为是保存在对象头,所以一旦生成就不会改变,即使发生GC对象地址改变了
- 5.不同对象的hashcode可以是相同的,比如基于内存地址生成的hashcode,gc之后有可能两个对象是基于同一个地址生成的hashcode
对象访问定位
- 第一种,句柄池。reference指向句柄,句柄维护指向实例对象和类型对象的指针。好处是移动实例对象不用改动reference
- 第二种,直接指针,reference直接指向实例对象,实例对象维护指向类型对象的指针。好处是速度快,因为跟句柄池比少了一跳,HotSpot使用这种
OOM异常
- 堆溢出:-XX:+HeapDumpOnOutOfMemoryError Dump出当前内存堆转储快照
jdk6用永久代实现方法区,-XX:PermSize=6M -XX:MaxPermSize=6M 设置大小 - 方法区溢出:用类型信息去填满,比如CGLIB动态代理生成很多代理类,或者大量加载JSP生成很多java类,方法区必须保存这些类型信息
- 直接内存溢出:OOM溢出时Heap Dump文件很小,系统用了NIO之类的考虑直接内存溢出
第3章 垃圾收集器与内存分配策略
判定对象回收算法
- 第一,引用计数算法:不能解决对象的循环引用问题。需要额外管理引用计数,但是判定高效。Python,FlashPlayer有使用。
- 第二,可达性分析算法:GC Roots向下搜索,走过的链路叫引用链。
GC Roots
- 虚拟机栈中本地变量表引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
- 虚拟机内部引用,比如基本数据类型对应的Class对象,常驻的异常对象,系统类加载器。
- 所有被同步锁持有的对象
四种对象引用
- 第一,强引用,就是代码中普通存在的对象引用,不会被垃圾收集器回收
- 第二,软引用,当虚拟机内存将要发生内存溢出前,会对软引用做两次回收,如果回收完内存还不够就抛出内存溢出异常
- 第三,弱引用。只被弱引用指向的对象在下次垃圾收集器回收。
- 第四,虚引用。不会对生存时间构成影响,也不能通过他获得对象,使用他的唯一目的是当被回收时收到一个系统通知
对象是否死亡
- 经过两次标记后才认为对象基本要被回收。第一次是通过可达性分析算法分析不在引用链上。第二次是看有没必要执行finalize方法,如果对象没有覆盖finalize方法,或者已经执行过一次finalize方法了,那么将不会二次标记,直接回收。如果需要执行finalize方法,对象可以在这个对象做一次死亡逃逸(只能逃逸一次)。finalize方法会被放到一个优先级很低的队列,只负责触发开始运行,但不负责执行结束。
分代收集理论三个假说
- 第一,弱分代假说:绝大多数对象都是朝生夕灭(新生代)
- 第二,强分代假说:熬过越多次垃圾收集过程的对象越难以消亡(老年代)
- 第三,跨代引用:跨代引用相对于同代引用来说仅占极少数。(记忆卡,只记录老年代指向新生代)
垃圾回收算法
- 第一,标记-清除算法:标记需要回收或者存活的,然后回收。缺点是不稳定,当有太多对象需要标记和回收时效率低,另外会产生碎片,当需要分配连续大对象时可能无法分配从而触发另一次回收动作。关注延迟,比如CMS(如果碎片多还会使用标记整理)。也会有STW
- 第二,标记-复制算法。一半空间浪费
- 第三,标记-整理算法。存活的对象往前移动,会导致STW。移动对象导致延迟,不移动就增加内存分配的压力(可能需要空闲列表来标示空闲内存)。关注吞吐量,比如Parallel
HotSpot算法细节实现
- 根节点枚举:枚举过程要STW。用一个叫做OopMap(通用对象指针,Ordinary Object Pointer)的数据结构记录GC Roots的根节点,那么就不需要去枚举方法区,栈等的GC Roots了
- 安全点:程序到达某个地方才做垃圾回收,停顿线程做回收
- 安全区域:理解为安全点的扩展拉伸,解决线程停顿走不到安全点的问题,比如Sleep。在安全区域里可以做垃圾回收,线程离开安全区域时需要检查是否完成根节点枚举
- 记忆集与卡表:在非收集区域记录收集区域的指针(一般是老年代对象指向新生代对象),解决跨代引用,卡表(一个数组记录跨代引用的内存块,卡页)是记忆集的一种实现方式,HotSpot中用到。
- 写屏障:引用类型赋值前后更新记忆集或者卡表,标记是否变脏,相当于环绕切面。
并发的可达性分析,解决对象消亡的方法有两个(都是基于写屏障来实现)
- 第一,增量更新。当黑色对象插入了指向白色对象引用关系时,就把新插入的引用记录下来,等扫描结束后再一次以黑色为根重新扫描一次(CMS)
- 第二,原始快照。当灰色对象删除指向白色对象引用时,记录要删除的引用,然后再以灰色对象为根重新扫描(G1和Shenandoah)
垃圾收集器
-
经典垃圾收集器
-
新生代收集器
- 1.Serial:单线程,工作时必须停止工作线程,jdk1.3.1之前的收集器,年轻代使用标记复制,老年代使用标记整理。在单核或者处理器少,或者低内存的机器使用,因为单线程专心做垃圾回收,所以性能也是比较高的。是客户端模式下新生代的默认收集器。
- 2.ParNew。第一款并行收集器,并行是指多个线程同时做垃圾回收,这时工作线程在等待。标记复制算法。与CMS配合使用,作为新生代收集器。CMS只能配合Serial或者ParNew使用。在单核环境比不过Serial
- 3.Parallel Scavenge。也是并行收集器,新生代里使用标记复制算法,不同的是其目标达到一个可控的吞吐量。有三个参数需要注意
第一,-XX: MaxGCPauseMillis。最大垃圾收集停顿时间。
第二,-XX: GCTimeRatio。设置吞吐量大小
第三,-XX: +UseAdaptiveSizePolicy。自适用参数,这是他与ParNew的不同之处。这个参数设置后,-Xmn设置新生代大小,Eden与Survivor比例大小参数-XX:SurvivorRatio,晋升老年代对象大小等参数-XX:PretenureSizeThreshold等参数将无效
-
老年代收集器
-
Serial Old。配合Serial使用,作为老年代的收集器,使用标记整理算法,也是单线程,是客户端模式下的收集器。老年代Concurrent model fail时作为CMS的替补收集器
-
Parallel Old。jdk6才正式使用,与Parallel Scavenge配合,作为老年代收集器,基于标记整理算法,并发收集。与新生代配合目标是吞吐量优先。
-
CMS。Concurrent Maark Sweep。基于标记清除,用空闲链表,有四个阶段,第一初始标记,第二并发标记,第三重新标记,第四并发清除。初始和重新标记需要STW,其他两个是与工作线程并发工作。目标是并发低停顿,但是会带来低吞吐量
-
缺点
- 1.内存碎片(因为使用复制清除算法)
- 2.内存管理低效(因为使用了空闲链表)
- 3.并发模式失败,退化为Serial Old串行
- 4.对处理器资源敏感,因为会与工作线程一起工作
-
-
-
跨代收集器G1(Garbage First)
- JDK9默认。把内存划分为多个region区,对每个区的回收价值收益做优先级排序,优先级越高的说明回收的空间越大,越快被回收。但是还是有分代region。另外对大对象做了专门的空间存放。
- 全功能收集器,兼顾延迟和吞吐量。由用户指定期望的延迟时间。停顿时间设置为一两百,或者三百毫秒比较合理。太小会导致每次回收很少,最后引发fullgc。
- 整体来看是标记整理算法,局部又属于标记复制,都不会产生碎片,比CMS优秀。但是垃圾回收产生的内存占用和程序运行的额外执行负载(要维护大量的记忆集,新生代和老年代相互指向)比CMS高
- 四个阶段只有并发标记是与工作线程一起执行,其他都要STW,不过占用时间都是可控的,因为他只对region回收,而不是整个堆。大概率来说,大内存时G1优于CMS
-
-
低延迟收集器
-
Shenandoah
-
也基于Region分块回收。是OpenJDK12的一个重要特性,不被OracleJDK支持。RedHat开发的,低停顿,可以到几十毫秒,但是吞吐量也低。使用读屏障和转发指针实现并发整理
-
与G1的区别
- 第一支持并发的整理算法
- 第二不使用分代收集
- 第三使用“连接矩阵”维护记忆集,降低内存和计算资源的损耗
-
-
-
ZGC
- Oracle研发,是Oracle JDK11加入具有实验性质的低延迟收集器。同样使用读屏障和转发指针实现并发整理。基于Region分块回收,没有分代。有四个阶段,基本是并发的。
-
低延迟收集器占用内存大的原因
- 分代收集能够较好的应对对象分配速率,如果没有做分代回收,没有对新生代做回收,那么就得划出一个区域给新对象分配。新分配的对象都被当成存活对象,里面可能有很多死亡对象,但是得不到回收,而回收速度可能跟不上,持续下去虚拟机只能申请更多内存给新对象。所以一般低延迟虚拟机由于没有分代回收,都比较吃内存。
-
-
GC最佳实践
- 大对象在老年代分配,因为大对象的会有高昂的开销内存复制开销,所以大对象在老年代分配,避免Eden区和两个Survivor区来回复制。会遇到新生代还有很多内存,但是对象直接到老年代。
- -XX:PretenureSizeThreshold设置直接在老年代分配对象的大小,没有k或者m单位,直接设置数值到字节,比如1024就是1k。
-
进入老年代条件
- 1 大对象超过
-XX:PretenureSizeThreshold设置的大小,比如数组
2 对象年龄超过-XX: MaxTenuringThreshold设置的值
3 动态对象年龄判定。Survivor中相同年龄的对象总和大于Survivor的一半,则大于或者等于这个年龄的对象进入老年代
- 1 大对象超过
-
空间分配担保
- 当老年代连续空间小于新生代时,会不会直接进入老年分配。JDK6 UPDATE14之前有个参数HandlePromotionFailure设置为true时,则看老年代最大连续空间大小是否大于历次进入老年代对象的平均大小,如果大于则出触发Minor GC,否则触发Full GC。JDK6 UPDATE14之后发生老年代空间大于新生代或者历次晋升的对象平均值会触发 Minor GC,否则Full GC
其他占用内存的区域
- 1.直接内存。2.线程堆栈,可以用Xss设置。 3.Socket缓存区。 4.JNI代码,本地调用。 5. 虚拟机和垃圾回收器工作也要消耗内存。
回收class满足条件(比较苛刻,不一定会回收)
- 1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 2.加载该类的ClassLoader已经被回收
- 3.该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
大对象频繁产生的调优方案
- 代码优化,少产生大对象。或者用堆外内存。或者使用G1和ZGC
第4章 虚拟机性能监控,故障处理工具
常用命令
- jps:虚拟机进程状况工具,jps -l
jstat: 统计信息监控工具,jstat -gc 进程号;jstat option vmid interval【s|ms】count。-gcutil:打印使用信息,-class 监视类加载
jmap: java内存映射工具,生成堆转储快照,jmap -dump jmid
java:运行java程序,比如class或者jar文件
javac:编译java文件
javap:反编译class文件
jar:创建和管理jar文件 - jhat: 堆转储快照分析工具,类似于Eclipse Memory Analyzer。会生成分析文件,然后通过http方式查看
jstack:堆栈跟踪工具。用来查看线程执行方法的堆栈集合,一般是用来定位线程长时间停顿的原因,比如死锁,请求外部资源导致长时间挂起.jstack option vmid . -l 附加锁信息 jstack -l 123 - jcmd和jhsdb:多功能工具箱,集成以上工具,有后发优势
Jconsole:java监控与管理控制台。基于JMX(Java Manage Bean。-Dcom.sum.management.jmxremote开启)的可视化监控、管理工具。可以监控内存、线程。JDK/bin 目录下的jconsole.exe启动
VisualVM: 多合-故障处理工具,All-in-One,是功能最强大的运行监视和故障处理程序之一。JDK 6 Update 7首次发布,可向下兼容到JDK 1.4.2 也不是全部功能兼容。
BTrace:动态日志跟踪。是VisualVM插件,在不中断目标程序运行的提前下,基于HotSpot的Instrument功能动态加入原本不存在的代码。比如用来打印调用堆栈,参数,返回值。还能作为性能监控,定位连接泄漏,内存泄漏等。阿里开源的诊断工具Arthas也是通过Instrument实现与BTrace类似的功能。
Java Mission Control:可持续在线的监控工具。需要商业授权,才能用于生产环境。跟VisualVM类型。
第5章 调优案例分析与实战
1. jdk5 文档服务器(单台)改用大内存,单实例2G改为16G,延迟更慢
-
解决方法:最后改为5个2G实例,CMS回收器
-
单个虚拟机管理大内存需要考虑的问题
- 1.回收大块堆内存导致的长时间停顿
2.大内存必须有64位虚拟机支持
3.必须保证应用程序的足够稳定。如果堆溢出,将无法产生堆转储快照,因为内存大,转储快照文件必然很大,难以生成文件,就算生成也难以分析。
4.相同程序在64位虚拟机比32位占用内存要大,因为指针膨胀以及对齐补白。
- 1.回收大块堆内存导致的长时间停顿
-
一台机器多个节点,小内存面临的问题
- 1.节点竞争全局资源,比如磁盘
2.不能高效使用某些资源池,比如连接池,有些慢了,有些空闲。用JNDI,但是比较复杂。
3.如果是32位虚拟机,受到4G内存的限制。
4.大量使用本地缓存,每个节点一份缓存,可以用集中式缓存。
- 1.节点竞争全局资源,比如磁盘
2. 外部命令导致系统缓慢
- 慎用java的Runtime.getRuntime().exec()来调用Shell脚本,因为它会创建新的进程,频繁调用会有很多fork系统调用,消耗大量处理器和内存资源。
3. 堆外内存导致的溢出错误
- 32位windows操作系统,1.6G给了堆内存,那么受到单个进程2G的限制,堆外直接内存只有0.4G的内存使用,而直接内存依赖full GC回收,如果full GC没满则不会去回收直接内存。
4. 服务器虚拟机进程崩溃
- 大量http Connect reset,异步的http请求太多,而每个http请求需要3分钟才能返回,导致大量的socket缓存占用内存,超过了虚拟机的承受能力。解决方案:异步请求使用MQ,或者使用线程池或者是http连接池控制并发的请求。
5. 不恰当数据结构导致内存占用过大
- 100万数据存入HashMap<Long, Long>中,一下子产生了大对象需要在新生代做Eden区到Survivor区的复制,很耗性能,导致Minor GC延迟。jvm调优可以让对象直接进入 老年代。将survivor空间去掉(加入参数:-XX:SurvivorRatio=65535 -XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure,第一次Minor GC之后进入老年代)。最好的办法是不用使用HashMap结构存。因为存两个 Long只要16字节,但是需要消耗88字节去维护他们,空间效率只有16/88=18%
6. 由安全点导致长时间停顿
- MapReduce或者Spark离线分析任务,使用G1,对延迟不敏感,设置为-XX:MaxGCPauseMillis=500,一段时间后垃圾回收达到了3秒以上。最后分析原因是一个int的循环里有比较耗时的请求,而int的循环不会设置安全点,所以垃圾回收器会空转等待这个线程达到安全点才回做垃圾回收。垃圾回收不是在线程的任何地方都发起回收,而是会设置安全点,线程到了安全点才会考虑做垃圾回收。安全点设置的原则是:是否让程序长时间执行。所以方法调用、循环跳转,异常跳转会设置安全点。但是int循环体不会设置安全点,因为虚拟机为了不产生太多安全点做了优化 。long循环体才有安全点。这个例子把循环索引从int改为long就可以解决安全点的问题。
第6章 类文件结构
Class文件是一组以8字节为基础单位的二进制流,紧凑排列,没有分隔符
每个Class文件的头4个字节称为魔数,值为0xCAFEBABE,用来确定文件是否为一个class文件。第5和第6是次版本号,第7和第8是主版本号,从45开始。jdk6是50。接着是常量池
高版本JDK可以向下兼容以前版本的Class文件,但是不能向上兼容。
常量是编译期确定值的,在class文件里
字节码指令:长度只有一个字节,所以操作码总数不超过256条
方法和字段名最大长度是65535
第7章,虚拟机类加载机制
类加载生命周期
- 加载-》验证-》准备-》解析-》初始化-》使用-》卸载。其中验证、准备、解析是连接阶段
其中初始化有严格的要求,有且仅有6种情况必须立即初始化
- 1.new实例化对象的时候:new 对象;读取或者设置类型的静态字段(不包括final常量);调用静态方法
2.java.lang.reflect包方法对类型进行反射调用的时候
3.初始化类时候发现父类没初始化,先初始化父类
4.当虚拟机启动,执行类型的main方法,会初始化这个类。
5.使用JDK7新加入的动态语言支持时。
6.一个接口定义了JDK8新加入的默认方法(被default修饰),如果这个实现类做了初始化,那么接口要在之前做初始化(接口有()类构造器)
不会初始化的情况
- 1.通过子类引用父类的静态字段,不会导致子类初始化
2.通过数组定义来引用类,不会触发类的初始化。
3.引用类常量不会初始化,因为常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类。
加载阶段,可以有多种文件,多种方式,只要求是二进制字节流
- 1.ZIP压缩包,比如JAR,WER,WAR格式文件
2.从网络中获取,比如Web Applet
3.运行时计算生成,比如动态代理
4.有其他文件生成,比如JSP
5.从数据库读取
6.从加密文件读取
验证阶段
- 1.文件格式验证,2.元数据验证,3.字节码验证,4,符号引用验证
- 不是必须的,如果多次验证是安全的,则可用-Xverify:none关闭大部分的类验证。
准备阶段
- 为类中定义的变量(static)分配内存并设置初始值(零值),不包括实例变量
解析阶段
- 将常量池中的符号引用替换为直接引用
初始化阶段
- 就是执行类构造方法()方法,()方法是由javac编译期自动生成的,会收集所有类的赋值动作和静态语句块,其有几个特性:
1.与构造函数不同
2.父类的()方法会先执行,所以父类的static会先执行。接口也有这个方法
3.不是必须的,如果接口中没有变量初始化赋值操作,类中没有静态语句块,也没有static变量赋值,则不会生成。接口的实现类不会执行接口的()方法。
4.同步加锁执行
类与类加载器
-
任意一个类在虚拟机的唯一性:由类加载器+类本身
判断相等可以是:equals()方法,isAssignableFrom(),isInstance(),instanceof关键字 -
双亲委派模型
- 不是类继承的关系,而是组合关系来复用父加载器
优先委派父类加载,父类加载不了再由自己加载,自己也加载不了就抛出异常。这种结构给了类一种优先级的层次关系,比如Object只有启动类加载器才能加载。
启动类加载器:<JAVA_HOME>\lib目录下jar,按照文件名加载,不符合名称的jar不会加载
扩展类加载器:<JAVA_HOME>\ext目录下jar,jdk9模块化之后弃用
应用类加载器:也叫系统类加载器,应用程序默认的加载器,如果没有自定义加载器就用这个。
- 不是类继承的关系,而是组合关系来复用父加载器
-
破坏双亲委派情况
- 1.JDK1.2之前,双亲是1.2出现的,需要兼容之前的JDK
2.自身缺陷导致。比如JNDI这种SPI服务,需要向下去加载各数据库厂商的驱动类,出现了逆向加载。直到JDK6提供java.uiti.ServiceLoader类,以META-INF/services配置信息,辅以责任链,才给SPI提供了合理的解决方案。
3.用户对程序动态追求导致的。OSGI的热部署,代码热替换,模块热部署
- 1.JDK1.2之前,双亲是1.2出现的,需要兼容之前的JDK
第8章 虚拟机字节码执行引擎
字节码信息
- 栈桢存储的信息:局部变量表,操作数栈,动态链接和方法返回地址信息
局部变量表:变量槽是变量表的最小单位,一般是32位存储一个变量,boolean 、byte,char、short、int、float、reference、returnAddress是用一个变量槽存,long和double是2个变量槽。变量槽的第0位存的是this,指向对象。
操作数栈:编译时最大深度写入了Code属性的max_stacks数据之中,32位数据类型占用栈容量是1,64位是2。java虚拟机的解析执行引擎称为“基于栈的执行引擎”。
动态链接:将符号引用转为直接引用
方法返回地址:分为正常调用完成和异常调用完成(没有返回值)
执行引擎
- 虚拟机一般会有:解析执行(解析器)和编译执行(即时编译期)
方法调用指令
- invokestatic:调用静态方法
invokespecial:调用实例构造器 ()方法,私有方法,父类方法。
invokevirtual:调用虚方法。
invokeinterface:接口方法。
invokedynamic:动态解析出调用点限定符所引用的方法,然后再执行该方法。JDK加入的,为lambda做准备
基于栈的指令集
- 优点:1.可移植,不受硬件限制。2.代码更加紧凑。缺点:执行速度相对基于寄存器的指令集慢
第9章 类加载及执行子系统的案例与实战
Backport工具
- java的时光机:Retrotranslator能够把高版本jdk编写的代码转换到低版本jdk执行。
不用场景下的类加载器
- 每一个Web应用对应一个WebApp类加载器,每一个JSP文件对应一个JasperLoader类加载器。
远程调试执行代码的途径
- 1.使用BTrace这类JVMTI工具去修改程序中某一部分的运行代码,类似的工具还有阿里的Arthas
2.使用JDK 6之后提供的Compiler API动态编译java程序。
3.“曲线救国”编写JSP文件上传服务器,然后浏览器去运行它。
4.在程序中内置动态执行的功能。
第10章 前端编译与优化
语法糖
- 泛型会在编译器做类型擦除,最后都是裸类型
第11章 后端编译与优化
解析器和编译器优点
- 解析器加快启动速度,编译器提高代码的执行效率,可以配合使用,也会相互切换
两个热点探测方式
- 基于采样的热点探测,基于计数器的热点探测
栈上的方法替换
- 被编译成本地代码的方法会被直接在栈上替换为本地代码的编号,而无须跳转到方法去执行,这种也叫做方法内联
虚方法
- invokespecial指令调用的私有方法,实例构造器,父类方法,和使用invokestatic指令调用的静态方法,还有final注释的方法是非虚方法,会在编译器解析。其他方法是虚方法,可能有多个类型继承版本。
即时编译优化技术
-
最重要的优化技术之一:方法内联
- 省去方法调用成本,比如查找方法版本,建立栈帧。为其他优化建立基础,所以一般放在优化的最靠前
-
最前沿优化技术之一:逃逸分析
- 为其他优化措施提供依据。包括:
栈上分配:可支持方法逃逸,直接在栈上分配对象内存,而不需要使用堆内存,减少了垃圾回收的压力。
标量替换:不允许逃出方法范围,可以把对象成员变量放到栈上,而不用实例化整个对象
同步消除(锁消除):如果变量是在本线程使用,不会逃逸出线程,那么对其不用同步也可以安全的使用。比如StringBuffer的appen()方法。
- 为其他优化措施提供依据。包括:
-
语言无关的经典优化技术之一:公共子表达式消除
- AB + C + AB - D --> T + C + T - D --> 2*T + C - D
-
语言相关的经典优化技术之一:数组边界检查消除
- 访问不用判断数组大小,用try-catch捕获异常)
-
空循环
- 空循环被即时编译成本地代码后会被优化掉,不会执行,所以不会延迟方法的执行时间
即时编译与提前编译
- 即时编译:会抢占系统资源。优点有:性能分析指导优化,激进预测性优化,链接时优化
提前编译:提前优化为本地代码,跟平台有关,不会与正在执行的程序抢资源。提前编译好公共类库,将大大提高性能
第12章 Java内存模型与线程
处理器内存模型
- 缓存和主内存之间有高速缓存和一致性协议连接
Java内存模型
-
定义程序中各种变量的访问规则,变量包括:实例字段,静态字段和构成数组对象的元素,不包括局部变量和方法参数
-
模型规定
- 1.所有变量存储在主内存中。
2.工作内存保存了被自己线程使用变量的主内存副本(可以是对象的引用或者字段,一般不会整个大对象)
3.线程对变量的所有操作(赋值,读取)必须在工作内存中进行,不能直接读写主内存的数据(volatile比较特殊)。
4.工作内存之间的变量相互隔离,无法访问到对方工作内存的变量,线程间的值传递通过主内存完成。
- 1.所有变量存储在主内存中。
-
勉强的对应关系
- 主内存:Java堆中的对象实例数据,物理内存
工作内存:虚拟机栈部分区域,寄存器或者高速缓存
- 主内存:Java堆中的对象实例数据,物理内存
volatile关键字
- 是最轻量级的锁同步机制
1.可以保证可见性。read-load-use指令必须连续一起出现,这里保证每次使用都从内存中去刷新最新值。assing-store-write指令必须连续一起出现,保证修改的值立即同步回主内存。
2.禁止指令重排序优化(JDK5之后支持)。使用内存屏障实现,屏障前面的指令不能排到屏障后面,后面的也一样。会用lock addl $0x0,(%esp),作用是将本处理器的缓存写入内存,同时引起其他处理器或者内核无效化其缓存。
long和double的特殊规则
- 不要求原子性访问。对于long类型有非原子性访问的风险,JDK9起有实验性的参数-XX:+AlwaysAtomicAccesses 来约束虚拟机对所有数据类型做原子性访问。double类型一般都是原子性
多线程三大特性
- 原子性:read,load,assign,use,store,write都是原子性的。long和double比较特殊。
- 可见性(有3个):
volatile:修改立即回写主内存,使用从主内存刷新值。
synchronized:线程独占的lock和unlock实现。lock作用于主内存,把变量标识为线程独占,对变量执行lock操作会清空工作内存的值,使用前重新从主内存获取新的值。unlock之前会把变量同步回主内存。
final:如果构造器没有把”this“传递出去,那么对于其他线程来说,final修饰的变量就是可见的。 - 有序性:volatile(禁止指令重排序),synchronized(变量只能被一个线程lock)。本线程内观察所有的操作都是有序的。
先行发生原则
- 我们编写代码时不用考虑太多可见性,因为有了先行发生原则
先行发生原则(Happens-Before):是Java内存模型定义的两项操作之间的偏序关系,是判断数据是否存在竞争,线程是否安全的有用手段。包括:
程序次序原则(控制流顺序),
管程锁定原则(虚拟机指令lock,unlock。unlock先行发生于后一个线程对同一个锁的lock操作),
volatile变量规则(每次从主内存刷新值),
线程启动规则:线程的所有动作在start()方法之后。
线程的终止规则:线程的所有操作发生在线程终止之前。
线程中断规则:线程的interrupt()方法先行发生于中断事件的发生。
对象终结规则:一个对象的初始化完成(构造器执行完成)先行发生于它的finalize()方法。
传递性:A先B,B先C,则A先C。
实现线程的三种方式
- 1.使用内核线程实现(1:1,HotSpot虚拟机)。缺点是切换、调度成本高昂,系统能容纳的线程数量有限。调度成本主要来源于内核态和用户态之间的切换,开销来源于响应中断、保护和恢复执行现场的成本。
2.使用用户线程(1 :N 线程的创建,同步,销毁,调度都在用户态,系统内核不能感知到)
3.混合实现(N:M)
协程
- 被设计成协同式调度的线程,早期的协程有做调用栈的保护和恢复,所以也称为“有栈协程”,主要优势是轻量。
Java线程调度
- 协同式:线程的执行时间由线程控制,执行完再通知操作系统切换到另一个线程。会导致线程执行时间不可控,可能一直阻塞。
抢占式:每个线程由系统分配执行时间,线程切换不由线程本身来决定。
Java线程6个状态
- 新建(New)
运行(Runnable):对应操作系统 的Running或者Ready
无限期等待(Waiting):没有设置等待时间的Object.wait(),Thread.join(), LockSupport.park().
限期等待(Timed Waiting):Thread.sleep(),有设置时间的Object.wait(),Thread.join(), LockSupport.parkNanos(),LockSupport.parkUntil()
阻塞(Blocked):线程等待进入同步锁的时候。
结束(Terminated):结束执行。
第13章 线程安全与锁优化
线程安全分类,安全级别由高到低,分为五类
- 不可变。final关键字修饰的。还有枚举类型,java.lang.Number的部分子类,比如Long,Double,BigInteger,BigDecimal等。AtomicInteger和AtomicLong则为可变的。
- 绝对线程安全。不管运行时环境如何,调用者都不需要任何额外的同步措施。Vector,HashTable都还不是,因为并发访问remove和迭代的时候还是有线程安全。
- 相对线程安全。Vector,HashTable,synchronizedCollection()包装的集合
- 线程兼容的。就是通常说的线程不安全。
- 线程对立。很少出现了。
线程安全的实现方法
- 1.互斥同步。synchronized(可重入),并发包的lock。
2.非同步阻塞。乐观并发策略,无锁编程,比如CAS(JKD9里面在VarHandle类里放开了面向用户的CAS操作),要硬件来保证操作和冲突检测具备原子性。CAS会有ABA问题,但是ABA问题一般不会影响到程序执行的正确性,所以很少使用到AtomicStampedReference这个类。
3.无同步方案。可重入的代码(返回的结果是可以预测的,比如递归),线程本地存储(ThreadLocal)。
锁优化
- 1.自旋锁与自适应锁。自旋锁:为了不让线程立即进入阻塞,线程会执行一个忙循环。自旋虽然可以避免线程切换的开销,但是他需要占用处理器时间,如果锁占用时间很短,自旋效果会很好。如果锁占用时间很长,只会白白消耗处理器的资源。当初默认十次自旋,可以通过-XX:PreBlockSpin来改。JDK6之后引入了自适应自旋,自旋次数基于监测的统计值来执行。
- 锁消除。如果程序执行片段确定是不会有线程安全问题,比如在方法里执行StringBuffer的appen()方法,则编译期会做锁消除,最后是无锁执行。
- 锁粗化。避免小范围频繁加锁,比如在循环里做加锁,那么可以做锁粗化,在循环外部做加锁。
- 偏向锁(synchronized):无实际竞争,且将来只有第1个申请锁的线程会使用锁,第一个线程获得锁后,后面操作就是相当于无锁操作了。因为偏向锁保存线程ID的内存位置与保存hashcode值的位置重叠,所以偏向锁一旦计算hashcode,那么偏向会被立即撤销,会膨胀为重量级锁。hashcode被计算存储过的话,就无法进入偏向状态了。有些时候如果一直是多线程竞争锁的哈,偏向是多余的,那么可以用-XX:-UseBiasedLocking来禁止偏向。
- 轻量级锁(synchronized):无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
- 重量级锁(synchronized):有实际竞争,且锁竞争时间长。
ReentrantLock与synchronized的对比
- synchronized缺点:
- 不能中断。无法强制让获得锁的线程释放锁,也无法让等待锁的线程放弃等待
2.不能设置超时
3.不能异步处理锁。即锁可用就拿锁,不可用我做其他的流程处理,比如tryLock。不用排队。(wait和notify也有这个问题)
4.不够灵活,无法根据条件加锁和解锁
- ReentrantLock相比synchronized的高级功能:
1.等待可中断,可设置超时。
2.公平锁。但是会导致性能下降,影响吞吐量。
3.锁绑定多个条件。newCondition() - 优先考虑synchronized,因为:
- 在Java语法层面同步足够清晰,也足够简单。很多程序员都会使用。
- 相比lock,不用在finally块做锁的释放。
- JDK6(包含6)之后性能跟Lock相差无几,而且Java虚拟机更容易针对synchronized做优化。
更多推荐



所有评论(0)