写在前面:这里写的基于杨晓峰的《Java核心技术36讲》,因为精简问题,我去掉了基本所有的示例代码。在此基础上,对一些我不懂的东西,我会自己去网上搜,如果内容很多的都有标记来源,如果只有短短的几句话就省略了,当然,如果我对某些东西比较熟悉,我一般都是回顾一下,并不会记录下来。他的专栏个人认为是很不错的,各位如果想详细了解的话,可以去极客等平台购买他的专栏,详细更详细。如这读后侵了作者的任何权益,请联系我删除[email protected]
第一天
2019年8月20日17:01:05
第一章:代码的一些编译过程
平常开发中,java是先通过javac把代码编译成字节码(就是class文件),然后jvm内嵌的解释器将字节码转换成为最终的机器码
不过呢,大部分jvm里面的编译器 都是热编译(就是一边运行,一边将新的代码编译成字节码)
术语 JIT(just-in-time compilation):就是代码用的时候才编译
缺点:1、吃内存 2.就是一边编译 一边要浪费时间的。可能刚刚开始编译的时候相对静态的更占时间
AOT(Ahead-of-Time Compilation):运行前编译,直接将字节码编译成机器代码
缺点:不能及时更新
参考自https://blog.csdn.net/h1130189083/article/details/78302502
第二章:异常相关
Error:跟你代码没关系的 比如一些内存溢出一类的
受检查的:写代码的时候必须要try catch的
不受检查:你根本不知道什么时候会发生 比如遍历一个list 在某个地方null搞个空指针异常
catch异常的时候 ,不要吧用户的一些信息输入到日志里面。因为那样可能导致潜在的安全问题。
try catch的性能开销很大,会引影响jvm去优化代码。用try catch远比我们通常意义上的条件语句(if/else、switch)要低效
Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作,虽然少一个快照的开销相对上比较小 但是多的话就不能忽视了
第二天
2019年8月21日16:57:14
第三章
final 不是 immutable!
比如final修饰的list能添加 数据 但是实际上是没加(只是里面的数据被限制,但是行为不受影响)
immutable修饰的直接报错
Java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的finalize 实现。Cleaner 的实现
利用了幻象引用(PhantomReference) ,这是一种常见的所谓post-mortem清理机制。
利用幻象弓用和弓用队列,我们可以保证对象被彻底销毁前做-些类似关掉资源的工作
它比finalize更加轻星、更加可靠。
吸取了finalize 里的教训,每个Cleaner的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。
实践中,我们可以为自己的模块构建一个Cleaner,然后实现相应的清理逻辑。
如果我没记错的话,effectiveJava好像说尽量少用这两个东西
第四章
软引用:只有当JVM认为内存不足时,才会去试图回收软引|用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用:这个引用如果有的话就用它,没有就重新实例化,也是类似于缓存的东西
幻想引用(虚引用):就是要凉了,你用人参都救不活。用这个东西只是告诉大家凉的时候要干什么
第五章
StringBuffer实现的一些细节,它的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的
StringBuffer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized。
这个数组构建时初始字符串长度加16 (这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。
扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。
快照:保存全部的东西
缓存:保存部分东西
String有个叫intern()的方法提示JVM把相应字符串缓存起来,下次用的时候可以去拿
缺点是这个方法吧被缓存的字符串放在PermGen(内存的永久保存区)里面(当然,之后变成了 堆里面),垃圾回收器GC 不会在主程序运行期对 PermGen space 进行清理。空间本间就不多,容易造成内存溢出
intern()是显式地排重机制,在jdk8以后,有个新特性就是G1 和GC的字符串重排,这个功能是默认关闭的
JDK9开始字符串的从char数组来存的变成一个byte数组加上一个标识符,紧凑的字符串:内存更小 速度更快
第六章
自省:知道这个类有什么东西的能力。比如用Class.forName("com.User");装载了com包下面的User类,就可以知道User类中的 字段 方法 构造器 等等
反射:知道一个类的所有属性和方法;通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
两者的区别是反射可以调用里面的方法,反射可以赋予程序在运行的时候自省
反射提供的AccessibleObject.setAccessible(boolean flag),可以修改成员变量的访问限制(就是public、private一类的)
jdk代理和cglib的区别
jdk代理生成的代理类只有一个,所以编译的很快
cglib一个目标生成一个代理类,平常的话会弄出很多个类。
第三天
2019年8月27日17:32:04
第七章
Integer值默认缓存是-128到127之间。
当然,其它的包装类也有类似的缓存
●Boolean, 缓存了true/false对应实例,确切说,只会返回两个常量实例
Boolean.TRUE/FALSE。
●Short, 同样是缓存了-128到127之间的数值。
●Byte,数值有限,所以全部都被缓存。
●Character, 缓存范围’\u0000’ 到\u007F'
1.可以通过jvm去调缓存范围
如XX:AutoBoxCacheMax =NNN参数即可将Integer的自动缓存区间设置为[-128,NNN]。注意区间的下界固定在-128不可配置
2字符串不可变是为了保证线程安全
看了下 八大基本类型都是用final修饰的
延伸 --》java的六大存储
1.寄存器 :算的最快的地方,存在于处理器内部,不过数量很少。缺点是 这个东西是编译器来决定的,我们直接控制不了
2.堆栈 速度只比寄存器慢,存在于RAM里面,有一个叫堆栈指针的东西。指针下移———压栈——创建新内存;上移——出栈——释放内存。缺点是要知道保存数据的长度和存在时间,限制了程序的灵活性。尽管有些Java 数据要保存在堆栈里——特别是对象句柄,但Java 对象并不放到其中
3.堆:存在RAM里面。不需要告诉要分多少空间、存活多长时间。创建对象直接new出来就好了。但是为了分配空间,要浪费更多的时间。
4.静态存储:在RAM里面 但是位置固定了。类似于全局变量。Java 对象本身永远都不会置入静态存储空间(为什么??)。在程序开始执行时给全局变量分配存储区,程序执行完毕就释放
5.常数存储:通常放在只读的ROM里面。直接放在代码里面,永远不会变
6.非RAM存储:程序凉了这个地方都还在。比如:图片一类的东西
参考自https://www.cnblogs.com/Jason2018/p/9312132.html
第八章
Vector和ArrayList的扩容机制:Vector是如果满了的话,创建新的数组,容量为原来的一倍,然后拷贝原有数组。ArrayList是扩容50%
Linklist就是个双向链表
用array的随机访问快 ,但是新增或者删除的话就慢(因为改了后面的下标也会跟着变)。链表的就反过来
内排序:在排序的时候所有对象都存在在一块内存当中
外排序:一块内存不够了,所以不断在内外存之间移动
十大经典排序
1.冒泡:比较相邻的,如果如果第一个比第二个大,就把他们两换个位置。这样一直重复执行,直到没有要换的为止。好处是排序简单
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
2.选择:从第一个开始,依次比较,如果小则选择,然后一直比到最后一个,如果没有更大的就放到前面去
最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
3.插入排序:从第二个开始 ,抽一个出来,如果比前一个小就插进去,一直弄到不能排
..
算法参考自https://www.zhihu.com/question/23148377/answer/718815659 还有gif图
根据treeMap最上面的注释以及源码可以看出TreeSet代码里实际默认是利用TreeMap实现的,hashSet也是利用HashMap
对比Hash、tree
tree支持自然顺序访问,但是添加、删除、包含等操作要相对低效(log(n)时间)。有序。我目前的地方也就签名一类的地方用
hash 快。无序。缺点1.如果key是自定义类,你得自己重写hashcode方法
2.如果扩容数组长度发生变化,必须把所有元素重新计算其index存放位置,所以尽可能事先确定hashmap的大小,防止扩容
参考https://blog.csdn.net/u014203449/article/details/80188929
LinkHash 有序但是比hash慢(因为要维护链表)
Collections有一个沙雕的方法弄线程安全
比如List list = Collections.synchronizedList(new ArrayList<>());
Map map = Collections.synchronizedMap(new HashMap
还有set。。collection等。。。。自己点进源码看
不过他们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下
●对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序,你可以阅读源码。
●而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插 入排序
(binarySort)结合的优化排序算法。TimSort 并不是Java的独创,简单说它的思路是查找
数据集中已经排好序的分区(这里叫run),然后合并这些分区来达到排序的目的。
JVM 在处理变长参数的时候会有明显的额外开销
第四天
2019年9月2日17:17:59
第九章
Hashtable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
一般情况下 hashmap的get、put可以达到常数时间的性能,TreeMap则是O(log(n))的时间复杂度
linkHashMap 插入的是什么顺序 查出来的就是什么顺序
treeMap 按照key来排序的
hashMap类似大的map形状(数组+链表),其中大key为数组,被分为一个一个的桶,通过哈希值决定了键值对在这个数组的寻址。如果哈希值相同,就把它们存到大value这个链表里面去。
map有个叫putVal的方法,下面有个叫resize的函数,
功能为
1.如果我们没有指定初始化大小,就给他一个初始值,初始值为表格的大小(比如map里面有6个,就初始化为6,然后再位移成8,这也就是平时为什么说尽量指定初始化大小的原因之一,还有个主要原因是扩容要吧老数组重新放到新的数组里面去)。
其中 jdk7是在new的时候就对容量初始化,8在用的时候才初始化(比如put的时候)。
2.如果大小不够,就扩容
hashmap有个叫门限值的,就是当超出这个的时候就会扩容
门限值=负载因子*容量
比如容量为100,负载因子为0.75 那么超出75的时候就会扩容
关于负载因子
1.不要乱改,java给你的就是最通用的
2.如果要改,不要超过0.75.因为会显著增加冲突
3.如果太小,就会影响到预设值,可能让更频繁的扩容,浪费开销。
有个MIN_TREEIFY_CAPACITY的常数
如果大value的链表大小超过就会变成红黑树。
小于就是单纯的普通扩容
发生哈希碰撞(就是出现了多个相同的哈希值),jdk7是将新的放到头部去,jdk8是放到数组最后面去。哈希碰撞主要是为了避免哈希碰撞拒绝服务攻击(因为在元素放置过程中,如果一个对象哈希冲突, 都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,查的慢,会严重影响存取的性能。然后导致cpu大量占用)
解决哈希碰撞有4个方法
其中开放地址又分为:
具体的看https://segmentfault.com/a/1190000012201011
第十章
Hashtable本身比较低效,因为它的实现基本就是将put、get、 size 等各种方法上"synchronized"。
ConcurrentHashMap是分段存储的,然后类似于hashmap。在构造的时候,Segment 的数量由所谓的concurrentcyLevel决定,默认是16,也可以在相应构造函数直接指定。注意, Java 需要它是2的幂数值,如果输入是类似15这种非幕值,会被自动调整到16之类2的幂数值。
初始化用的操作实现在initTable里面,这是一个典型的CAS使用场景,利用volatile 的sizeCtl作
为互斥手段:如果发现竞争性的初始化,就spin在那里,慢慢地等没冲突;
没冲突就利用CAS设置排他标志。如果成功则进行初始化;失败了就重试。
在进行并发写操作时:
●ConcurrentHashMap 会获取再入锁,以保证数据一致性, Segment 本身就是基于
ReentrantLock的扩展实现,所以,在并发修改期间,相应Segment是被锁定的。
●在最初阶段,进行重复性的扫描,以确定相应key值是否已经在数组里面,进而决定是更新还
是放置操作。重复扫描、检测冲突是ConcurrentHashMap的常见技巧。
●它进行的不是整体的扩容,而是单独对Segment进行扩容
J8中1.变成懒汉模式,用的时候才加载。有效避免初始化的开销
2.使用Unsafe、LongAdder 之类底层手段,进行极端情兄的优化。
3.结构的话变得和hashmap差不多,大key是桶,大value是链表。分段只是为了保证序列化兼容,没有任何结构上的作用了
第十一章
多路复用:通俗点就是重复利用。举个例最简单的例子:从A地到B地,坐公交2块。打车要20块,为什么坐公交便宜呢,因为公交走很多不同的人走的同样的路
这里所讲的就是“多路复用”的原理。
IO:同步阻塞
NIO:同步非阻塞
NIO2(AIO):异步非阻塞
首先,熟悉一下NIO的主要组成部分:
●Buffer, 高效的数据容器,除了布尔类型,所有原始数据类型都有相应的Buffer实现。
●Channel, 类似在Linux之类操作系统上看到的文件描述符,是NIO中被用来支持批量式I0
操作的-种抽象。
File或者Socket,通常被认为是比较高层次的抽象,而Channel则是更加操作系统底层的一
种抽象,这也使得NIO得以充分利用现代操作系统底层机制,获得特定场景的性能优化,例
如,DMA (Direct Memory Access)等。不同层次的抽象是相互关联的,我们可以通过
Socket获取Channel,反之亦然。
●Selector, 可以检测自己上门注册的Channel现在能不能用,进而实现了单线程对多
Channel的高效管理。
线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
IO都是同步阻塞模式,所以需要多线程以实现多任务处理。而NIO则是利用了单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么
第五天
2019年9月5日17:00:31
第十一章
当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先
在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。类似打普雷BOSS时候的切换光与暗
写入的话就相反
缺点是会带来一定的额外开销,可能会降低l0效率。
而基于NIO transferTo的实现方式,在Linux和Unix上,则会使用到零拷贝技术,数据传输一套带走,
不需要用户态参与。注意,transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。
如何提高类似拷贝等IO操作的性能,有一些宽泛的原则:
1.用缓存(减少IO次数)。
2.使用transferTo等机制,减少上下文切换和额外l0操作。
3.减少一些没必要的流程,例如转换(编解码一类的)、对象序列化和反序列化(不要用文本信息,就直接传二进制信息,不要再转成字符串)
八大基本类型 除了boolean 其它的都要buffer实现,看图
Buffer有几个基本属性:
1.capacity:就是这个buffer的size()
2.position:操作数据的起始位置。start
3.limit:相当于操作的限额。大小默认就是这个buffer的全部长度。在读取或者写入时,limit的意义很明显是不样的。比如,读取操作时,很可能将limit设置到所容纳数据的上限;而在写入时,则会设置容量或容星以下的可写限度
4.mark:记录上一次postion的位置,默认是0,算是一个便利性的考虑,往往不是必须的。
讲下buffer操作的流程
●我们创建了一个ByteBuffer,准备放入数据,capacity 当然就是缓冲区大小,而position就
是0, limit默认就是capacity的大小。
●当我们写入几个字节的数据时,position 就会跟着变大,但是它不可能超过limit的大
小。
●如果我们想把前面写入的数据读出来,需要调用flip方法,将position设置为0, limit 设置
为以前的position那里。就是从0开始读
●如果还想从头再读一遍,可以调用rewind,让limit不变,position 再次设置为0。
有两个特殊的buff:Direct Buffer和MappedByteBuffer
Direct Buffer:进行临时数据的存放,由于使用的是堆外内存,省去了数据到内核的拷贝,因此效率比用ByteBuffer要高不少。
DirectBuffer的构造函数主要做以下三个事情:
1、根据页对齐和pageSize来确定本次的要分配内存实际大小
2、实际分配内存,并且记录分配的内存大小
3、声明一个Cleaner对象用于清理该DirectBuffer内存
它就是个幻引用。其本身不是public类型,内部实现了一个Deallocator负责销毁的逻辑销毁往往要拖到full gc,使用不当很容易内存溢出但是请注意,
Direct Buffer创建和销毁过程中,都会比一般的堆内Buffer增加部分开销,所以通常都建议用于长期使用、数据较大的场景。
使用Direct Buffer,我们需要清楚它对内存和JVM参数的影响。首先,因为它不在堆上,所以
Xmx之类参数,其实并不能影响Direct Buffer等堆外成员所使用的内存额度,我们可以
用jvm的 -Xx:MaxDirectMemorySize=512M
关于宰掉Direct Buffer的两个办法:一种是通过System.gc来回收,另一种是通过构造函数里创建的Cleaner对象来回收。一些大量用direct buffer的框架会自己去调用清除的方法,比如netty
跟踪direct Buffer在jdk8后有个叫Native Memory Tracking (NMT)的工具
-XX:NativeMemoryTracking={summary|detail}
注意,激活NMT通常都会导致JVM出现5%~10%的性能下降
第十二章
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。接
口,不能实例化;不能包含任何非常星成员,任何field都是隐含着public static final的意义;
同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定
义了非常多的接口,比如java.util.List。
抽象类是不能实例化的类,用abstract关键字修饰class,其目的主要是代码重用。除了不能实
例化,形式上和一般的Java类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽
象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承
的方式达到代码复用的目的。Java 标准库中,比如collection框架,很多通用部分就被抽取成
为抽象类,例如java.util. AbstractList。
Java类实现interface使用implements关键词,继承abstract class则是使用extends关键
为什么Java中不支持多重继承?
为什么Java不支持多重继承, 可以考虑以下两点:
1)第一个原因是围绕钻石形继承问题产生的歧义,
超父类、父类、子类如果有同一个方法,系统都不知道用哪个。
考虑一个类 A 有 foo() 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现,现在 D 类使用多个继承派生自 B 和C,如果我们只引用 foo(), 编译器将无法决定它应该调用哪个 foo()。这也称为 Diamond 问题,因为这个继承方案的结构类似于菱形,见下图:
即使我们删除钻石的顶部 A 类并允许多重继承,我们也将看到这个问题含糊性的一面。如果你把这个理由告诉面试官,他会问为什么 C++ 可以支持多重继承而 Java不行。嗯,在这种情况下,我会试着向他解释我下面给出的第二个原因,它不是因为技术难度, 而是更多的可维护和更清晰的设计是驱动因素, 虽然这只能由 Java 言语设计师确认,我们只是推测。维基百科链接有一些很好的解释,说明在使用多重继承时,由于钻石问题,不同的语言地址问题是如何产生的。
2)对我来说第二个也是更有说服力的理由是,多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题。假设你需要多重继承的情况并不多,简单起见,明智的决定是省略它。此外,Java 可以通过使用接口支持单继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义。
Java不支持多继承其实主要就是在规范了代码实现的同时,也产生了一些局限性, 影响着程序设计结构。
基本的设计原则:S.O.L.I.D
●单一职责(Single Responsibility):一个类不要去干另一个类干的事
●开关原则(Open-Close, Open for extension, close for modification),:尽量新增,少修改
●里氏替换(Liskov Substitution) , 这是面向对象的基本要素之一,进行继承关系抽象时, 凡
是可以用父类或者基类的地方,都可以用子类替换。
●接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了
太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内
聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果
某个接口设计有变,不会对使用其他接口的子类构成影响。
●依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模
块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦
合度的法宝。
第六天
2019年9月6日17:48:04
第十四讲
设计模式可以分为创建型模式、结构型模式和行为型模式。
●创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式
(Factory、 Abstract Factory)、单例模式(Singleton)、构建器模式(Builder) 、原型模
式(ProtoType)。
●结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见
的结构型模式,包括桥接模式(Bridge) 、适配器模式(Adapter) 、装饰者模式
(Decorator)、代理模式(Proxy)、组合模式(Composite) 、外观模式(Facade) 、享
元模式(Flyweight) 等。
●行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有
策略模式(Strategy) 、解释器模式(Interpreter) 、命令模式(Command) 、观察者模式
(Observer)、迭代器模式 (Iterator)、 模板方法模式 (Template Method)、访问者模
式(Visitor) 。
代理模式和装饰者模式区别:
代理,偏重因自己无法完成或自己无需关心,需要他人干涉事件流程, 更多的是对对象的控制。 就是关心别人
装饰,偏重对原对象功能的扩展,扩展后的对象仍是是对象本身。 就是关心自己
参考自https://www.jianshu.com/p/c06a686dae39 前部分的。。。。后面的一大堆看的没意义
双检查锁:比如new 一个对象之前先判断它有没有被创建。。。
比如
public class Singleton {
// 注意这个地方 private volatile static Singleton uniqueSingleton; private Singleton() { } public Singleton getInstance() { if (null == uniqueSingleton) { synchronized (Singleton.class) { if (null == uniqueSingleton) { uniqueSingleton = new Singleton(); } } } return uniqueSingleton; } }
第十五讲
synchronized和ReentrantLock有什么区别呢?
类似于事务锁。。。。@trans和直接手动事务
reentrantlock就是手动 需要.unlock
syn就是类似注解一类的
如果使用synchronized,我们根本无法进行公平性的选择,其永远是不公平的,这也是主流操
作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很
少会导致“饥饿” 发生。与此同时,若要保证公平性则会引入额外开销,自然会导致一定的吞吐
量下降。所以,我建议只有当你的程序确实有公平性需要的时候,才有必要指定它。
第十六讲
有的观点认为Java不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当
JVM进入安全点(SafePoint) 的时候,会检查是否有闲置的Monitor,然后试图进行降级。
synchronized是JVM内部的Intrinsic Lock,所以偏斜锁、轻星级锁、重星级锁的代码实现,并不在核心类库部分,而是在JVM的代码中。
hotspot jvm官方文档http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/interpreter/interpreterRuntime.cpp
后缀为sharedRuntime.cpp/hpp,它是解释器和编译器运行时的基类。
后缀为synchronizer.cpp/hpp, JVM同步相关的各种基础逻辑。
在运行过程中,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对
方操作结束,这样就可以自动保证不会读取到有争议的数据。
读写锁看起来比synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,
主要还是因为相对比较大的开销。
所以,JDK在后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式(编译器和处理器猜测)。优化
读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着读,然后通过validate
方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
第十七讲
线程的状态有个枚举类 ,就是java.lang.Thread.State
状态有:
●新建(NEW) ,表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态。
●就绪(RUNNABLE) ,表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能
是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队。
●在其他-些分析中,会额外区分-种状态RUNNING, 但是从Java API的角度,并不能表示
出来。
●阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待
Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占
了,那么当前线程就会处于阻塞状态。
●等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消
费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait) ,另外的生产者线程去
准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。Thread.join0
也会令线程进入等待状态。
●计时等待(TIMED WAIT) ,其进入条件和等待状态类似,但是调用的是存在超时条件的方
法,比如wait或join等方法的指定超时版本
●终止(TERMIINATED) ,不管是意外退出还是正常执行结束,线程已经完成使命,终止运
行,也有人把这个状态叫作死亡。
从线程生命周期的状态开始展开,那么在Java编程中,有哪些因素可能影响线程的状态呢?主
要有:
●线程自身的方法,除了start, 还有多个join方法,等待线程结束; yield是告诉调度器,主动
让出CPU;另外,就是一-些已经被标记为过时的resume、stop、 suspend 之类,据我所
知,在JDK最新版本中,destory/stop 方法将被直接移除。
●基类Object提供了一些基础的 wait/notify/notifyAll方法。如果我们持有某个对象的
Monitor锁,调用wait会让当前线程处于等待状态,直到其他线程notify或者notifyAll。所
以,本质上是提供了Monitor 的获取和释放的能力,是基本的线程间通信方式。
●并发类库中的工具,比如CountDownLatch.await(会让当前线程进入等待状态,直到latch
被基数为0,这可以看作是线程间通信的Signal。
守护进程是在后台运行不受终端控制的进程(如输入、输出等)
一般的网络服务都是以守护进程的方式运行。守护进程脱离终端的主要原因有两点:(1)用来启动守护进程的终端在启动守护进程之后,需要执行其他任务。(2)(如其他用户登录该终端后,以前的守护进程的错误信息不应出现)由终端上的一些键所产生的信号(如中断信号),不应对以前从该终端上启动的任何守护进程造成影响。要注意守护进程与后台运行程序(即加&启动的程序)的区别
有的时候应用中需要一个长期驻留的服务程序, 但是
不希望其影响应用退出,就可以将其设置为守护线程,如果JVM发现只有守护线程存在时,将
结束进程,具体可以参考下面代码段。注意,必须在线程启动之前设置。
再有就是慎用ThreadLocal,这是Java提供的一种保存线程私有信息的机制,因为其在整个线程
生命周期内有效,所以可以方便地在-个线程关联的不同业务模块之间传递信息,比如事务ID、
Cookie等上下文相关信息。同时,它的数据存储于线程相关的ThreadLocalMap,其内部条目是弱引用。针对这个map 废弃项目的回收依赖于显式地触发(就是把key设置成null),否则就要等待线程结束,进而回收相应的ThreadLocalMap
第七天
2019年9月9日17:23:00
第十八讲
定位死锁最常见的方式就是利用jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而
找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,类似JConsole甚至可以在图
形界面进行有限的死锁检测。
查死锁的方法:1.确定进程id
2.调用jstack获取线程栈:
3.找到线程状态为waiting或者block的 ,对比
避免锁的方法:1.尽量少用多个锁,需要的时候才去持有锁
2.设置锁的获取顺序。意淫各个锁可能执行的顺序
3.设置锁的超时时间
if (lock.tryLock() || lock.tryLock(timeout, unit)) {
// ...
}
这样处理就好,没拿到就尝试超时去拿
4.假设可能发生锁的情况,进而去避免
第十九讲
我们通常所说的并发包也就是java.util.concurrent 及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面:
●提供了比synchronized更加高级的各种同步结构,包括CountDownLatch、CyclicBarrier、Semaphore等,可以实现更加丰富的多线程操作,比如利用Semaphore作为资源控制器,限制同时进行工作的线程数量。
●各种线程安全的容器,比如最常见的ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组CopyOnWiterrayList等。
●各种并发队列实现,如各种BlockedQueue实现,比较典型的ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue 等。
●强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。多线程框架Executor详解https://www.cnblogs.com/fengsehng/p/6048610.html
这章主要讲的是同步结构
随便拿的几个显式锁
●CountDownLatch, 允许一个或多个线程等待某些操作完成。
●CyclicBarrier, -种辅助性的同步结构,允许多个线程等待到达某个屏障。
●Semaphore, Java 版本的信号量实现。它通过控制一定数星的允许(permit) 的方
式,来达到限制通用资源访问的目的。比如坐电梯的时候,一趟只允许12个人,超过就等下一班。Semaphore就是个计数器,基本逻辑基于acquire/release(其逻辑是,线程试图获得工作允许,得到许可则进行任务,然后释放许可,这时等待许可的其他线程,就可获得许可进入工作状态,直到全部处理完)
CountDownLatch和CyclicBarrier的区别:
●CountDownLatch 是不可以重置的,所以无法重用;而CyclicBarrier则没有这种限制,可以
重用。
●CountDownLatch 的基本操作组合是countDown/await。只要次数够了就行,次数为countDown的大小。调用await的线程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程里countDown,只要次数足够即可。所以就像Brain Goetz说过的,CountDownLatch 操作的是事件。
CyclicBarrier的基本操作组合,则就是await,当所有的伙伴(parties) 都调用了await,才会继续进行任务,并自动进行重置。注意,正常情况下,CyclicBarrier 的重置都是自动发生的,如果我们调用reset方法,但还有线程在等待,就会导致等待线程被打扰,抛出BrokenBarrierException异常。CyclicBarrier 侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。
为什么并发容器里面没有ConcurrentTreeMap呢?
因为TreeMap要实现高效的线程安全是非常困难的,它的实现基于复杂的红黑树。为保证
访问效率,当我们插入或删除节点时,会移动节点进行平衡操作,这导致在并发场景中难以进行
合理粒度的同步。而SkipList结构则要相对简单很多,通过层次结构提高访问速度,虽然不够紧
凑,空间使用有一定提高(O(nlogn)),但是在增删元素时线程安全的开销要好很多。
CopyOnWrite 到底是什么意思呢?它的原理是,任何修改操作,如add、set、
remove,都会拷贝原数组,修改后替换原来的数组,通过这种防御性的方式,实现另类的线程安全
比如hashmap扩容就是这样。所以这种数据结构,相对比较适合读多写少的操作,不然修改的开销还是非常明显的。
第二十讲
有时候我们把并发包下面的所有容器都习惯叫作并发容器,但是严格来讲,类似
ConcurrentLinkedQueue这种"Concurrent*" 容器,才是真正代表并发。
关于问题中它们的区别:
●Concurrent 类型基于lock-free,在常见的多线程访问场景,-般可以提供较高吞吐星。
●而LinkedBlockingQueue内部则是基于锁,并提供了BlockingQueue的等待性方法。
●Concurrent 类型没有类似CopyOnWrite之类容器相对较重的修改开销。
●但是,凡事都是有代价的,Concurrent往往提供了较低的遍历一致性。你可以这样理解所谓
的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
历。
●与弱一致性对应的,就是我介绍过的同步容器常见的行为"fail-fast" ,也就是检测到容器在
遍历过程中发生了修改,则抛出ConcurrentModificationException,不再继续遍历。
●弱一致性的另外一个体现是,size等操作准确性是有限的,未必是100%准确。
●与此同时,读取的性能具有一 定的不确定性。
有界队列:就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列:指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue。
●ArrayBlockingQueue 是最典型的的有界队列,其内部以final的数组保存数据,数组的大小
就决定了队列的边界,所以我们在创建ArrayBlockingQueue时,都要指定容量
●LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑
实现的,只不过如果我们没有在创建队列时就指定容量,那么其容星限制就自动被设置为
Integer.MAX_ VALUE,成为了无界队列。
●SynchronousQueue, 这是一个非常奇葩的队列实现, 每个删除操作都要等待插入操作,反之
每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是1吗?其实不是的,其
内部容星是0。
●PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统
资源影响。
●DelayedQueue 和LinkedTransferQueue同样是无边界的队列。对于无边界的队列,有一个
自然的结果,就是put操作永远也不会发生其他BlockingQueue的那种等待情况。
以LinkedBlockingQueue、ArrayBlockingQueue 和SynchronousQueue为例,我们一-起来
分析一下,根据需求可以从很多方面考量:
●考虑应用场景中对队列边界的要求。ArrayBlockingQueue是有明确的容星限制的,而
LinkedBlockingQueue则取决于我们是否在创建时指定,SynchronousQueue 则干脆不能缓
存任何元素。
●从空间利角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为
其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间, 所以初始内存需求更
大。
●通用场景中, LinkedBlockingQueue 的吞吐星般优于ArrayBlockingQueue,因为它实现
了更加细粒度的锁操作。
●ArrayBlockingQueue 实现比较简单,性能更好预测,属于表现稳定的“选手”。
●如果我们需要实现的是两个线程之间接力性(handoff) 的场景, 按照专栏上一讲的例子,你
可能会选择CountDownLatch,但是SynchronousQueue也是 完美符合这种场景的,而且线
程间协调和数据传输统一起来,代码更加规范。
●可能令人意外的是,很多时候SynchronousQueue的性能表现,往往大大超过其他实现,尤
其是在队列元素较小的场景。
第二十一讲
Executors目前提供了5种不同的线程池创建配置:
●newCachedThreadPool0, 它是一 种用来处理大量短时间工作任务的线程池,具有几个鲜明
特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲
置的时间超过60秒,则被终止并移出缓存长时间闲置时,这种线程池,不会消耗什么资
源。其内部使用SynchronousQueue作为工作队列。
●newFixedThreadPool(int nThreads), 重用指定数目(nThreads) 的线程,其背后使用的是
无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量
超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的
工作线程被创建,以补足指定的数目nThreads。
●newSingleThreadExecutor(, 它的特点在于工作线程数目被限制为1,操作-个无界的工作
队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允
许使用者改动线程池实例,因此可以避免其改变线程数目。
●newSingleThreadScheduledExecutor(和newScheduledThreadPool(int corePoolSize),
创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一
工作线程还是多个工作线程。
●newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8才加入这
个创建方法,其内部会构建ForkJoinPool, 利用Work-Stealing算法, 并行地处理任务,不保
证处理顺序。
线程池这个定义就是个容易让人误解的术语,因为ExecutorService除了通常意义上
“池”的功能,还提供了更全面的线程管理、任务提交等方法。
Execut相关的设计目的
●Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦
●ExecutorService 则更加完善,不仅提供service的管理功能,比如shutdown等方法,也提
供了更加全面的提交任务机制,如返回Future而不是void的submit方法。
●Java标准类库提供了几种基础实现,比如ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool。 这些线程池的设计特点在于其高度的可调节性和灵活性,以尽星满足复杂多变的实际应用场景,我会进一步分析其构建部分的源码,剖析这种灵活性的源头。
●Executors 则从简化使用的角度,为我们提供了各种方便的静态工厂方法。
应用与线程池的交互和线程池的内部工作过程:
●内部的”线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销
毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压
力退去,线程池会在闲置-段时间 (可以认为是心跳时间,默认60秒)后结束线程。
●工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为0的SynchronousQueue (使用newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用LinkedBlockingQueue。
●ThreadFactory 提供上面所需要的创建线程逻辑。
AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架
线程池的构造:
●corePoolSize,所谓的核心线程数,可以大致理解为长期驻留的线程数目(除非设置了
allowCoreThreadTimeOut)。对于不同的线程池,这个值可能会有很大区别,比如
newFixedThreadPool会将其设置为nThreads,而对于newCachedThreadPool则是为0。
●maximumPoolSize, 顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对
于newFixedThreadPool,当然就是nThreads,因为其要求是固定大小,而
newCachedThreadPool则是Integer.MAX _VALUE。
●keepAliveTime 和TimeUnit,这两个参数指定了额外的线程能够闲置多久,显然有些线程池
不需要它。
●workQueue,工作队列,必须是BlockingQueue。
线程池生命周期
idle是闲置的意思,实际上线程池一创建就会被用的,是不存在这个状态的。前几天有说过 J8之后线程池用的时候才创建
使用线程池应该避免的问题
1.避免任务堆积。比如工作的只有两三个,但有成千上万个等着被吃。可以使用jmap之类的工具,查看是否有大量的任务对象入队。
2.避免过度扩展线程。我们通常在处理大星短时任务时,使用缓存的线程池,比如在最新的
HTTP/2 client API中,目前的默认实现就是如此。我们在创建线程池的时候,并不能准确预
计任务压力有多大、数据特征是什么样子(大部分请求是1K、100K还是1M以上?),所
以很难明确设定一个线程数目。
3另外,如果线程数目不断增长(可以使用jstack等工具检查) ,也需要警惕另外-种可能性,
就是线程泄漏,这种情况往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放。建议你
排查下线程栈,很有可能多个线程都是卡在近似的代码处。
4避免死锁等同步问题,对于死锁的场景和排查,你可以复习专栏第18讲。
5尽量避免在使用线程池时操作ThreadLocal(很难死掉,全程活着)
线程池大小选择:
1.如果我们的任务主要是进行计算, 那么就意味着CPU的处理能力是稀缺的资源
,能通过大量增加线程数提高计算能力吗?往往是不能的,如果线程太多,反倒可能导致大量的上下
文切换开销。所以,这种情况下,通常建议按照CPU核的数目N或者N+1。
2.如果是需要等的。比如IO多 可以 线程数 = CPU 核数 × 目标 CPU 利用率 ×(1 + 平均等待时间 / 平均工作时间)
3.除了一和二的CPU问题,还可能受各种系统资源限制影响,有些问题我们可以通过扩大可用端口范围解决的,如果我们不能调整资源的容量,那么就只能限制工作线程的数了。这里的资源可以是文
件句柄、内存等。
背压(Backpressure)机制:在数据流从上游生产者向下游消费者传输的过程中,上游生产速度大于下游消费速度,导致下游的 Buffer 溢出,这种现象就叫做 Backpressure 出现
第二十二讲
CAS更加底层是如何实现的,这依赖于CPU提供的特定指令,具体根据体系结构的不同还存在着明显区别。比如,x86 CPU提供cmpxchg指令;而在精简指令集的
体系架构中,则通常是靠一对儿指令(如"load and reserve"和"store conditional" )实现
的,在大多数处理器上CAS都是个非常轻星级的操作,这也是其优势所在。
AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架
AQS内部数据和方法,可以简单拆分为:
●一个volatile的整数成员表征状态,同时提供了setState和getState方法
●一个先入先出(FIFO) 的等待线程队列,以实现多线程间竞争和等待,这是AQS机制的核心
之--。,
●各种基于CAS的基础操作方法,以及各种期望具体同步结构去实现的acquire/release 方
法。
利用AQS实现一个同步结构,至少要实现两个基本类型的方法,分别是acquire操作,获取资
源的独占权;还有就是release操作,释放对某个资源的独占。
首先,来看看tryAcquire。在ReentrantLock中,tryAcquire 逻辑实现在NonfairSync和
FairSync中,分别提供了进一步的非公平或公平性方法,而AQS内部tryAcquire仅仅是个接近
未实现的方法(直接抛异常) ,这是留个实现者自己定义的操作。
以非公平的tryAcquire为例,其内部实现了如何配合状态与CAS获取锁,注意,对比公平版本
的tryAcquire,它在锁无人占有时,并不检查是否有其他等待者,这里体现了非公平的语义。
接下来我再来分析acquireQueued,如果前面的tryAcquire失败,代表着锁争抢失败,进入排
队竞争阶段。这里就是我们所说的,利用FIFO队列,实现线程间对锁的竞争的部分,算是是
AQS的核心逻辑。
当前线程会被包装成为一个排他模式的节点(EXCLUSIVE) ,通过addWaiter方法添加到队列
中。acquireQueued的逻辑,简要来说,就是如果当前节点的前面是头节点,则试图获取锁,
一切顺利则成为新的头节点;否则,有必要则等待
总体上,tryAcquire 是按照特定场景需要开发者去实现的
部分,而线程间竞争则是AQS通过Waiter队列与acquireQueued提供的,在release方法
中,同样会对队列进行对应操作。
第八天
2019年9月20日17:32:03
第二十三讲
类加载过程:1.加载阶段:jvm读取字节码并转成class文件、jar文件之类的class对象,如果不是class对象就会抛ClassFormatError异常
2.链接:就是放到jvm里面去运行。①验证:检测字节是否符合java虚拟机规范
②准备:为静态变量等分配内存空间
③解析:将常星池中的符号引用(symbolic reference) 替换为直接引用 比如String temp="沙雕"; 在常量池中找到"沙雕这个字符串",然后赋值给temp
3.初始化:去执行真正的代码初始化的逻辑 ,注意。。。按照java基础知识,,静态变量,静态方法等静态相关的才在jvm编译的时候执行
双亲委派模型:简单说就是当类加载器(Class-Loader) 试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载Java类型。
三种Oracle JDK内建的类加载器
①●启动类加载器(Bootstrap Class-Loader) ,加载jre/lib 下面的jar文件,如rtjar。 它是个
超级公民,即使是在开启了Security Manager的时候,JDK仍赋予了它加载的程序
AllPermission。
有时候要改jdk自带代码的时候,可以用
②●扩展类加载器(Extension or Ext Class-Loader) ,负责加载我们放到jre/lib/ext/目录下面
的jar包,这就是所谓的extension机制。该目录也可以通过设置"java.ext.dirs" 来覆盖。
java -Djava. ext .dirs=your ext_ dir HelloWorld
③●应用类加载器(Application or App Class-Loader),就是 加载我们最熟悉的classpath的
内容。这里有一个容易混淆的概念,系统(System) 类加载器,通常来说,其默认就是JDK
内建的应用类加载器,但是它同样是可能修改的,比如:
java -Djava.system.class.loader=com.yourcorp.YourClassLoader HelloWorld
借用一张图
这就说到了刚说的那个双亲委派,如果1要加载一个类型,2也加载这个类型,3也加载这个类型,这样就重复加载了三次,完全没必要
通常类加载机制有三个基本特征:
●双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,
是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以
在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如, Java 中
JNDI、JDBC、 文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲
委派模型去加载,而是利用所谓的上下文加载器。
●可见性,跟继承差不多 。子类加载器可以访问父加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
●单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会
在子加载器中重复加载。但是注意,类加载器”邻居”间,同一类型仍然可以被加载多次,因
为互相并不可见。
第二十四讲
ASM:一个框架的名称,作用是生成二进制class文件
java从字节码转成class主要是通过一个叫defineClass的方法或者这个方法的重载
对于一一个普通的Java动态代理,其实现过程可以简化成为:
1.提供一个基础的接口,然别人去调
2实现InvocationHandler, 对代理对象方法的调用,会被分派到其invoke方法来真正实现动
作。
3通过Proxy类,调用其newProxyInstance方法,生成一个实现了相应基础接口的代理类实
例
最为流行的字节码操纵框架包括:
字节码技术应用场景:AOP技术、Lombok去除重复代码插件、动态修改class文件等
Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。比如lombok就不用写那么多get/set方法
其实字节码增强的主要流程就是
1. 在内存中获取到原来的字节码,然后通过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,得到一个新的byte数组。
2、使修改后的字节码生效
有两种方法:
1) 自定义ClassLoader来加载修改后的字节码;
2)替换掉原来的字节码:在JVM加载用户的Class时,拦截,返回修改后的字节码;或者在运行时,使用Instrumentation.redefineClasses方法来替换掉原来的字节码
比较下我知道的几个字节码框架
1.BECL:可以让您深入jvm汇编语言进行类库操作的细节。
2.ASM:是一个轻量级Java字节码操作框架,直接涉及到JVM底层的操作和指令
高性能,高质量
3.CGLB:生成类库,基于ASM实现
4.javassist:是一个开源的分析,编辑和创建Java字节码的类库。性能较ASM差,跟cglib差不多,但是使用简单。
BCEL与javassist有不同的处理字节码方法,BCEL在实际的jvm指令层次上进行操作(BCEL拥有丰富的jvm指令集支持) 而javassist所强调的是源代码级别(class)的工作。
参考自https://blog.csdn.net/art_code/article/details/90509485
第二十五讲
之前提到的是java的六大存储(CPU寄存器那些东西),
jvm存储也有六个
1.程序计数器。在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行, 也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行本地方法,则是未指定值(undefined) 。
2.虚拟机栈。就是java栈,每个线程在创建时都会创建一个虚拟机栈, 其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java方法调用。其实jvm的操作就是对栈针的压栈和出栈
栈帧中存储着局部变量表、操作数(operand) 栈、动态链接、方法正常退出或者异常退出的定
义等。
3.堆。放实例的地方。所有线程都能共享
4.方法区。也是所有线程共享的,存那些全局变量一类的东西。就是之前那个容易造成内存溢出的沙雕永久代。
运行时常量池:
Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。
类加载后,常量池中的数据会在运行时常量池中存放!
这里所说的常量包括:基本类型包装类(包装类不管理浮点型,整形只会管理-128到127)和String(也可以通过String.intern()方法可以强制将String放入常量池)
字符串常量池:
HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet
jdk 1.7后,移除了方法区间,运行时常量池和字符串常量池都在堆中。
参考自https://www.cnblogs.com/natian-ws/p/10749164.html
6.本地方法栈。功能和虚拟机栈差不多,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务
引起内存溢出的可能:
●堆内存不足是最常见的OOM原因之一, 抛出的错误信息是"java.lang.OutOfMemoryError;Java heap space",原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小;或者出现JVM处理弓|用不及时,导致堆积起来,内存无法释放等。
●而对于Java虚拟机栈和本地方法栈,这里要稍微复杂一点。如果死循环,我们写一段程序 不断的进行递归调用,且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM实际会抛出
StackOverFlowError;当然,如果JVM试图去扩展栈空间的的时候失败,则会抛OutOfMemoryError。
●对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,
常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代
出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似
Intern字符串缓存占用太多空间,也会导致0OM问题。对应的异常信息,会标记出来和永久
代相关: "java.lang.OutOfMemoryError: PermGen space"。
●随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的0OM有所改观,出现
OOM,异常信息则变成了: "java.lang.OutOfMemoryError: Metaspace"
●直接内存不足,也会导致OOM
当然,也不是在任何情况下垃圾收集器都会被触发的,比如你用128M的内存去玩DNF,我们去分配一个超大对象,类似一
个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出
OutOfMemoryError。
第二十六讲
堆结构示意图
1.新生代:新生代是大部分对象创建和销毁的区域。一般情况下生命周期很短,
看图里面,Eden是负责对象初始化分配的。S0和S1是两个Survivor,就是from和to
GC的时候,将Eden里面活下来的和from里面的对象转到to里面。这样做的目的是防止内存碎片化,清理没有用的对象
在Eden里面有个叫Thread Local Allocation Buffer (TLAB)的,JVM为每个线程分配的一个私有缓存区域,过程是 比如下面的图 ,分虚拟内存Tread1出来,指针top从start开始 ,如果到了end(就是缓存满了)就再开一个TLAB出来,一直重复这样
2.老年代:放置长生命周期的对象,通常都是从Survivor区域拷贝过来的对象。
但是如果对象太大,就在Eden上找不到一段足够长的空间,然后jvm就会吧这个对象直接放到老年区去
3.永久代:和前面几课一样,存那些这部分就是Java类元数据、常星池、Intern 字符串缓存。不过在JDK 8之后就不存在永久代这块儿了。
jvm配置这些东西的参数千奇百怪,可以配置最大堆体积(-Xmx value)、初始最小堆体积(-Xms value
)、老年代和新生代的比例(-XX:NewRatio=value)
相信对eclipse或者idea太卡,去网上找调优的都看过这个-xmx一类的东西
一般情况下,老年代 新生代的比例为 2:1
除了设置比例 还可以直接设置新生代内存的大小(-XX:NewSize=value)
在JVM里面,如果最小堆体积小于配置的最大的堆体积,那么空出来的会变成不可用的状态,等下次用的时候才逐渐扩容
JDK9的默认GC是G1,虽然它在较大堆场景表现良好,但本身就会比传统的Parallel GC或
者Serial GC之类复杂太多,所以要么降低其并行线程数目,要么直接切换GC类型
JIT编译默认是开启了TieredCompilation的,将其关闭,那么JIT也会变得简单,相应本地
线程也会减少。
如果把复杂的GC换成简单的可见,不仅总线程数大大降低(25→13),且GC设施本身的内存开销就少了非常多。AWS Lambda中Java运行时就是使用的Serial GC,可以大大降低单个function的启
动和运行开销。
第九天
2019年10月8日17:00:17
第二十七讲
常见的几个GC:
1.Serial GC:最老的GC,工作是单线程的,而且还会进入到一个叫STW(Stop-The-World)的里面去。但是因为是单线程,所以实现简单,初始化也简单。
JVM的Client模式与Server模式:JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。在cmd用java -version就可以看到是server还是client模式。64位因为只支持server模式
STW:类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。
危害
2.ParNew GC,很明显是个新生代GC实现,它实际是Serial GC的多线程版本,最常见的应用
场景是配合老年代的CMS GC工作
3.CMS (Concurrent Mark Sweep) GC,基于标记-清除(Mark-Sweep) 算法,设计目标
是尽量减少停顿时间,这一点对于Web等反应时间敏感的应用非常重要,一直到今天, 仍然
有很多系统使用CMS GC。但是,CMS采用的标记-清除算法,存在着内存碎片化问题,所
以难以避免在长时间运行等情况下发生full GC导致恶劣的停顿。另外,既然强调了并发
(Concurrent) , CMS会占用更多CPU资源,并和用户线程争抢。在JDK9中被标志成了废弃
4.Parallel GC:顾名思义,就是新生代和老年代的GC是并行的(高效),被称为吞吐量优先的GC
5.G1:兼顾吞吐星和停顿时间的GC实现。JDK9之后变成默认的GC了。 可以直观的设值停顿时间,相对于CMS GC ,G1未必能做到CMS最好情况下的延时停顿,但比最差情况要好得多
G1 仍存在年代的概念,使用了Region棋盘算法,实际上是标记-整理(Mark-Compact)算法,可以避免内存碎片,尤其是堆非常大的时候,G1优势更明显。
G1 吞吐量和停顿表现都非常不错。
对象实例收集, 主要是两种基本算法,引用计数和可达性分析
1.引用计数算法,顾名思义,就是为对象添加一个引用计数,用于记录对象被弓|用的情况,如果
计数为0,即表示对象可回收。
2.可达性分析。通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么即可认为是可回收对象。JVM会把虚拟机栈和本地方法栈中
在Java语言中,可作为GC Roots的对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
常见的辣鸡回收算法
1.复制 算法。将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。就是把活着的对象复制到to区域里面去。为了避免碎片化,还是按照顺序来存放的。因为是复制不是移动,所以需要内存占用和时间开销
2.标记-清除(Mark-Sweep) 算法。先标记要回收的,在标记完成后统一回收所有被标记的对象
主要不足有两个:
一是效率问题,标记和清除效率都不高,二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
3.标记-整理算法:标记过程仍与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存
4.分代收集算法:就是按照区域来执行不同的算法。。。新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收
辣鸡收集过程
创建对象是分配在Eden区域,但是如果分配到了阀值的时候,就会触发minor gc把没被用的就清除掉,然后无限次这么重复,但某个对象执行minor gc的次数到了一个阀值的时候就会变成老年代里面去,这个阀值可以通过jvm来指定-XX:MaxTenuringThreshold=
一般情况下我们可以把新生代GC (Young GC)叫作Minor GC
,老年代GC叫作Major GC,将对整个堆进行的清理叫作Full GC,
一些潜力的新GC:
Epsilon GC,该GC只做内存分配而不做内存回收(reclaim),当堆空间耗尽关闭JVM即可,因为它不做任何垃圾回收工作,所以又叫No-op GC
ZGC:只能在64的linux上用,mas都不能用。Java 11 新加入的ZGC号称可以达到10ms 以下的 GC 停顿,承诺在数TB的堆上具有非常低的暂停时间。ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。
着色指针
着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位: finalizable, remap, mark0和mark1。 着色指针的一个问题是,当您需要取消着色时,它需要额外的工作(因为需要屏蔽信息位)
多重映射
要了解多重映射的工作原理,我们需要简要解释虚拟内存和物理内存之间的区别。 物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。 虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。 操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。
多重映射涉及将不同范围的虚拟内存映射到同一物理内存。 由于设计中只有一个remap,mark0和mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作
读屏障
读屏障是每当应用程序线程从堆加载引用时运行的代码片段(即访问对象上的非原生字段non-primitive field):
这与其他GC使用的写屏障形成对比,例如G1。读屏障的工作是检查引用的状态,并在将引用(或者甚至是不同的引用)返回给应用程序之前执行一些工作。 在ZGC中,它通过测试加载的引用来执行此任务,以查看是否设置了某些位。 如果通过了测试,则不执行任何其他工作,如果失败,则在将引用返回给应用程序之前执行某些特定于阶段的任务。
标记
现在我们了解了这两种新技术是什么,让我们来看看ZG的GC循环。
GC循环的第一部分是标记。标记包括查找和标记运行中的应用程序可以访问的所有堆对象,换句话说,查找不是垃圾的对象。
ZGC的标记分为三个阶段。 第一阶段是STW,其中GC roots被标记为活对象。 GC roots类似于局部变量,通过它可以访问堆上其他对象。 如果一个对象不能通过遍历从roots开始的对象图来访问,那么应用程序也就无法访问它,则该对象被认为是垃圾。从roots访问的对象集合称为Live集。GC roots标记步骤非常短,因为roots的总数通常比较小。
该阶段完成后,应用程序恢复执行,ZGC开始下一阶段,该阶段同时遍历对象图并标记所有可访问的对象。 在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。
在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。
重定位
GC循环的下一个主要部分是重定位。重定位涉及移动活动对象以释放部分堆内存。 为什么要移动对象而不是填补空隙? 有些GC实际是这样做的,但是它导致了一个不幸的后果,即分配内存变得更加昂贵,因为当需要分配内存时,内存分配器需要找到可以放置对象的空闲空间。 相比之下,如果可以释放大块内存,那么分配内存就很简单,只需要将指针递增新对象所需的内存大小即可。
ZGC将堆分成许多页面,在此阶段开始时,它同时选择一组需要重定位活动对象的页面。选择重定位集后,会出现一个Stop The World暂停,其中ZGC重定位该集合中root对象,并将他们的引用映射到新位置。与之前的Stop The World步骤一样,此处涉及的暂停时间仅取决于root的数量以及重定位集的大小与对象的总活动集的比率,这通常相当小。所以不像很多收集器那样,暂停时间随堆增加而增加。
移动root后,下一阶段是并发重定位。 在此阶段,GC线程遍历重定位集并重新定位其包含的页中所有对象。 如果应用程序线程试图在GC重新定位对象之前加载它们,那么应用程序线程也可以重定位该对象,这可以通过读屏障(在从堆加载引用时触发)实现,如流程图如下所示:
这可确保应用程序看到的所有引用都已更新,并且应用程序不可能同时对重定位的对象进行操作。
GC线程最终将对重定位集中的所有对象重定位,然而可能仍有引用指向这些对象的旧位置。 GC可以遍历对象图并重新映射这些引用到新位置,但是这一步代价很高昂。 因此这一步与下一个标记阶段合并在一起。在下一个GC周期的标记阶段遍历对象对象图的时候,如果发现未重映射的引用,则将其重新映射,然后标记为活动状态。
但是,ZGC还是处在测试阶段。。。
参考自https://blog.csdn.net/weixin_39639119/article/details/81627658
第二十八讲
基本的调优思路可以总结为:
●理解应用需求和问题,确定调优目标。假设,我们开发了一个应用服务,但发现偶尔会出现性
能抖动,出现较长的服务停顿。评估用户可接受的响应时间和业务星,将目标简化为,希望
GC暂停尽星控制在200ms以内,并且保证一定标准的吞吐量。
●掌握JVM和GC的状态,定位具体的问题,确定真的有GC调优的必要。具体有很多方法,
比如,通过jstat等工具查看GC等相关状态,可以开启GC日志,或者是利用操作系统提供
的诊断工具等。例如,通过追踪GC日志,就可以查找是不是GC在特定时间发生了长时间的
暂停,进而导致了应用响应不及时。
●这里需要思考,选择的GC类型是否符合我们的应用特征,如果是,具体问题表现在哪里,是
Minor GC过长,还是Mixed GC等出现异常停顿情况;如果不是,考虑切换到什么类型,如
CMS和G1都是更侧重于低延迟的GC选项。
●通过分析确定具体调整的参数或者软硬件配置。
●验证是否达到调优目标,如果达到目标,即可以考虑结束调优;否则,重复完成分析、调整、
验证这个过程。
之前有说过G1是用一个叫棋盘的方法,这里面的格子就叫region,大小相等,而且数值是在1M到32M字节之间的一个2的幂值数,这个格子的数量可以手动分配,G1也会根据堆大小来自己调整
设计这个格子的缺陷是如果有一个特别大的对象,region装不下你要去为这个格子扩容。
一些通用实践:
●如果发现Young GC非常耗时,这很可能就是因为新生代太大了,我们可以考虑减小新生代的
最小比例。
-XX:G1NewSizePercent
降低其最大值同样对降低Young GC延迟有帮助。
-XX:G1MaxNewSizePercent
●如果是Mixed GC延迟较长,我们应该怎么做呢?
减少格子的数量
-XX:G1MixedGCCountTarget
第二十九讲
关于这讲,因为我在《java并发编程艺术》一书中有阅读过,所以这张的时候还是相对来说忽略掉很多我已经知道的东西了。还是那些指令重拍、并发锁一类的。。。。
第三十讲
其实这一张介绍的是docker的一些坏处,与老版本的jdk一些不兼容的地方
jvm有个叫ergonomics的东西,通俗一点就是为垃圾收集器,堆大小和运行时编译器提供与平台相关的默认选择,
比如
●JVM会大概根据检测到的内存大小,设置最初启动时的堆大小为系统内存的1/64; 并将堆最
大值,设置为系统内存的1/4。
●而JVM检测到系统的CPU核数,则直接影响到了Parallel GC的并行线程数目和JIT
complier线程数目,甚至是我们应用中ForkJoinPool等机制的并行等级。
但是由于docker环境一类的差异,java很有可能会判断错,就像我以为有一辆卡宴,其实只有一辆自行车。
更加严重的是,JVM 的一些原有诊断或备用机制也会受到影响。为保证服务的可用性,-种常见
的选择是依赖"-XX:OnOutOfMemoryError" 功能,通过调用处理脚本的形式来做一些补救措
施,比如自动重启服务等。但是,这种机制是基于fork实现的,当Java进程已经过度提交内存
时,fork 新的进程往往已经不可能正常运行了。
docker的缺点还有是只能在linux上面跑。
但是如果把jdk升级一下,就基本能解决这些问题,因为时代在变,java也在跟着变。
JDK9有一些测试的参数,比如针对内存做限制
JDK10新增了参数用以明确指定CPU核心的数目。
-XX:ActiveProcessorCount=N
当然,如果实在要用docker和老版本的jdk,有几个方法
●明确设置堆、元数据区等内存区域大小,保证Java进程的总大小可控。(就是设置docker给分配java的大小,然后用jvm去去指定堆大小之类的)
●明确配置GC和JIT并行线程数目,以避免二者占用过多计算资源。
-XX:ParallelGCThreads
-XX:CICompilerCount
第三十一讲
java的几个安全攻击
1.sql注入
2.操作系统命令注入 rm -rf /*之类的
3.xml注入:攻击者可以修改XML数据格式,增加新的XML节点,对数据处理流程产生影响。具体例子参考自https://blog.csdn.net/u011402896/article/details/80914134
4.xss攻击。原理
它允许恶意web用户将代码植入到提供给其它用户使用的页面中
1.攻击者对某含有漏洞的服务器发起XSS攻击(注入JS代码)
2.诱使受害者打开受到攻击的服务器URL(邮件、留言等,此步骤可选项)
3.受害者在Web浏览器中打开URL,恶意脚本执行。
关于防止攻击,其实java也做了应付
第一,运行时安全机制。可以简单认为,就是限制Java运行时的行为,不要做越权或者不靠谱的事情,具体来看:
●在类加载过程中,进行字节码验证,以防止不合规的代码影响JVM运行或者载入其他恶意代码。
●类加载器本身也可以对代码之间进行隔离,例如,应用无法获取启动类加载器(Bootstrap
Class-Loader)对象实例,不同的类加载器也可以起到容器的作用,隔离模块之间不必要的可见性等。目前,Java Applet、RMI等特性已经或逐渐退出历史舞台,类加载等机制总体上反倒在不断简化。
●利用SecurityManger机制和相关的组件,限制代码的运行时行为能力,其中,你可以定制policy文件和各种粒度的权限定义,限制代码的作用域和权限,例如对文件系统的操作权限,或者监听某个网络端口的权限等。
可以看到,Java的安全模型是以代码为中心的,贯穿了从类加载,如URLClassLoader加载网
络上的Java类等,到应用程序运行时权限检查等全过程。
●另外,从原则上来说,Java的GC等资源回收管理机制,都可以看作是运行时安全的一部分,
如果相应机制失效,就会导致JVM出现OOM等错误,可看作是另类的拒绝服务。
第二,Java提供的安全框架API,这是构建安全通信等应用的基础。比如加密解密,授权鉴权
第三,就是JDK集成的各种安全工具,例如:
●keytool, 这是个强大的工具,可以管理安全场景中不可或缺的秘钥、证书等,并且可以管理
Java程序使用的keystore文件。
●jarsigner, 用于对jar文件进行签名或者验证。
第三十三讲
系统变慢解决先看看代码,然后再看看如CPU之类的底层
系统性能分析中,CPU、内存和I0是主要关注项。
流程是 用top看cpu的负载情况。平均负载(load average)的三个值(分别是1分钟、5分钟、15分钟),找到负载重的,
●利用top命令获取相应pid, "-H” 代表thread模式,你可以配合grep命令更精准定位。
top –H
●然后转换成为16进制。
printf "%x" 线程的pid
●最后利用jstack获取的线程栈,对比相应的ID即可。
当然也可以去查看线程上下文切换频率,而且比系统的线程中断率高很多的话,就证明这个有问题 需要通过pidstat之类的去进一步定位
pidstat是sysstat工具的一个命令,用于监控全部或指定进程的cpu、内存、线程、设备IO等系统资源的占用情况。pidstat首次运行时显示自系统启动开始的各项统计信息,之后运行pidstat将显示自上次运行该命令以后的统计信息。用户可以通过指定统计的次数和时间来获得所需的统计信息。
具体命令可以看https://www.jianshu.com/p/3991c0dba094
对于JVM层面的性能分析,我们已经介绍过非常多了:
●利用JMC、JConsole 等I具进行运行时监控。
●利用各种工具,在运行时进行堆转储分析,或者获取各种角度的统计数据(如jstat -gcutil分
析GC、内存分带等)。
●GC日志等手段,诊断Full GC、Minor GC,或者引用堆积等。
第三十四讲
本章主要强调基准测试
什么时候需要开发微基准测试呢?
我认为,当需要对一个大型软件的某小部分的性能进行评估时,就可以考虑微基准测试。换句话
说,微基准测试大多是API级别的验证,或者与其他简单用例场景的对比,例如:
●你在开发共享类库,为其他模块提供某种服务的API等。
●你的API对于性能,如延迟、吞吐星有着严格的要求,例如,实现了定制的HTTP客户端
API,需要明确它对HTTP服务器进行大量GET请求时的吞吐能力,或者需要对此其他API,
保证至少对等甚至更高的性能标准。
所以微基准测试更是偏基础、底层平台开发者的需求,追求极致性能
用什么框架比较好?
如果你是做Java API级别的性能对比,JMH往往是你的首选。
JMH不止能对Java语言做基准测试,还能对运行在JVM上的其他语言做基准测试。而且可以分析到纳秒级别。
如何保证微基准测试的正确性,有哪些坑需要规避?
●保证代码经过了足够并且合适的预热。默认情况,在server模式下,JIT会在一段代码执行10000次后,将其编译为本地代码,client 模式则是1500次以后。我们需要排除代码执行初期的噪音,保证真正采样到的统计数据符合其稳定运行状态。通常建议使用下面的参数来判断预热工作到底是经过了多久。
-XX:+PrintCompilation
我这里建议考虑另外加上-个参数,否则JVM将默认开启后台编译,也就是在其他线程进行,
可能导致输出的信息有些混淆。
-Xbatch
基准测试本来就是为了防止jvm自作聪明导致的问题
关于-X命令相关的 可以参考https://www.cnblogs.com/holos/p/6616028.html
●防止JVM进行无效代码消除(Dead Code Elimination) ,就是那些没有用到的代码,那么JVM就可能直接判断无效代码,根本就不执行它。
类似于这种
●防止发生常星折叠(Constant Folding) 。JVM如果发现计算过程是依赖于常最或者事实上
的常量,就可能会直接计算其结果,所以基准测试并不能真实反映代码执行的性能。JMH 提
供了State机制来解决这个问题,将本地变星修改为State对象信息。
类似于一个缓存,发现别的地方有值就直接拿过来用。
●另外JMH还会对对象进行额外的处理,以尽星消除伪共享(False Sharing) 的影响,
标记@State, JMH 会自动进行补齐。
●如果你希望确定方法内联(Inlining) 对性能的影响,可以考虑打开下面的选项。
-XX:+PrintInlining
意思是当方法被内联后打印出来
函数的调用过程。
调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。
这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。也就是通常说的压栈和出栈。
因此,函数调用要有一定的时间和空间方面的开销。那么对于那些函数体代码不是很大,又频繁调用的函数来说,这个时间和空间的消耗会很大。
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换.这样就不会产生转去转回的问题,但是由于在编译时将函数体中的代码被替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。
参考自https://blog.csdn.net/ke_weiquan/article/details/51946174
第三十五讲
JVM在对代码执行的优化可分为运行时(runtime) 优化和即时编译器(JIT) 优化。运行时优化
主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如
TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存
(inline cache,用于优化虚方法调用的动态绑定)。
JVM的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。
它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运
行profile的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我
有一条instanceof指令,在编译之前的执行过程中,测试对象的类一直是同- 个,那么即时编
译器可以假设编译之后的执行过程中还会是这一个类, 并且根据这个类直接返回instanceof的
结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。
当然,JVM的优化方式仅仅作用在运行应用代码的时候。如果应用代码本身阻塞了,比如说并发
时等待另一线程的结果,这就不在JVM的优化范畴啦。
而即时编译器(JIT) ,则是更多优化工作的承担者。JIT 对Java编译的基本单元是整个方法,
通过对方法调用的计数统计,甄别出热点方法,编译为本地代码。另外一个优化场景,则是最针
对所谓热点循环代码,利用通常说的栈上替换技术(OSR, On-Stack Replacement)
从理论上来看,JIT 可以看作就是基于两个计数器实现,方法计数器和回边计数器提供给JVM统
计数据,以定位到热点代码。实际中的JIT机制要复杂得多
调优的角度或者手段
●调整热点代码门限值(就是编译多少次)
之前有说JIT的默认门限,server 模式默认10000次,client 是1500次。门限大小也存在
着调优的可能,可以使用下面的参数调整;与此同时,该参数还可以变相起到降低预热时间的作
用。
-XX:CompileThreshold=N
JVM会周期性的对计数的数值进行衰减操作,导致调用计数器永远不能达到门限值,除了可以利用
CompileThreshold适当调整大小,还有一个办法就是关闭计数器衰减。
-XX:-UseCounterDecay
●调整Code Cache大小
JIT编译的代码是存储在Code Cache中的,需要注意的是Code Cache是存在大小限
制的,而且不会动态调整。这意味着,如果Code Cache太小,可能只有- -小部分代码可以被
JIT编译,其他的代码则没有选择,只能解释执行。所以,-个潜在的调优点就是调整其大小限
制。
-XX:ReservedCodeCacheSize=
当然,也可以调整其初始大小。
-XX:InitialCodeCacheSize=
分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
第0层:程序解释执行,解释器不开启监控功能,已出发第一层编译
第1层:也称c1编译,将字节码编译成本地代码,进行简单可靠的代码(我理解的就是把class字节码变成c/c++编译出来的那种相对高效的native代码,可是为什么native就高效呢?)
第2层:c2编译,将字节码编译成本地代码,与此同时会启用一些优化手段(比如:公共子表达式消除、数组边界检查消除、方法内联等等)这些手段会导致编译时耗时较长,但是会使得之后的代码执行效率较高,所以热代码会使用c2
因为这个分层的编译,所以Code Cache的默认值也变大了
●调整编译器线程数,或者选择适当的编译器模式
JVM的编译器线程数目与我们选择的模式有关,选择client模式默认只有一个编译线程, 而
server模式则默认是两个,如果是当前最普遍的分层编译模式,则会根据CPU内核数目计算C1
和C2的数值,你可以通过下面的参数指定的编译线程数。
-XX:CICompilerCount=N
●调整编译器线程数,或者选择适当的编译器模式
JVM的编译器线程数目与我们选择的模式有关,选择client 模式默认只有一个编译线程, 而
server模式则默以是两个,如果是当前最普遍的分层编译模式,则会根据CPU内核数目计算C1
i无识别结果和C2的数值,你通过面的参数指定的编译线程数。
-XX:CICompilerCount=N
在强劲的多处理器环境中,增大编译线程数,可能更加充分的利用CPU资源,让预热等过程更
加快速;但是,反之也可能导致编译线程争抢过多资源,尤其是当系统非常繁忙时。例如,系统
部署了多个Java应用实例的时候,那么减小编译线程数目,则是可以考虑的。
生产实践中,也有人推荐在服务器上关闭分层编译,直接使用server编译器,虽然会导致稍慢的
预热速度,但是可能在特定工作负载上会有微小的吞吐星提高。
●其他一些相对边界比较混淆的所谓”优化”
比如,减少进入安全点。
第三十六讲
事务隔离级别4个:
●读未提交
●读已提交
●可重复读(Repeatable reads),保证同-个事务中多次读取的数据是一致的,这是MySQL
InnoDB引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为MySQL
在可重复读级别不会出现幻象读。
●串行化(Serializable) ,并发事务之间是串行化的,就是要先去拿锁之类的操作。如果SQL使用WHERE语句,还会获取区间锁(MySQL 以GAP锁形式实现,可重复读级别中默认也会使用),这 是最高的隔离级别。
反映到MySQL数据库应用开发中,悲观锁-般就是利用类似SELECT .. FOR UPDATE这样的
语句,对数据加锁,避免其他事务意外修改数据。乐观锁则与Java并发包中的
AtomicFieldUpdater类似,也是利用CAS机制,并不会对数据加锁,而是通过对比数据的时间
戳或者版本号,来实现乐观锁需要的版本判断。
分工:
第三十七讲
创建bean组件的过程
Spring Bean的销毁过程会依次调用DisposableBean的destroy方法和Bean自
身定制的destroy方法。
Spring Bean有五个作用域,其中最基础的有下面两种:
●Singleton, 这是Spring的默认作用域,也就是为每个I0C容器创建唯一的一 个Bean
实例。
●Prototype, 针对每个getBean请求,容器都会单独创建一个Bean实例。
从Bean的特点来看,Prototype 适合有状态的Bean,而Singleton则更适合无状态的情
况。另外,使用Prototype作用域需要经过仔细思考,毕竟频繁创建和销毁Bean是有明
显开销的。
如果是Web容器,则支持另外三种作用域:
●Request, 为每个HTTP请求创建单独的Bean实例。
●Session, 很显然Bean实例的作用域是Session范围。
●GlobalSession,用于Portlet容器,因为每个Portlet有单独的Session,
GlobalSession提供一个全局性的HTTP Session。
Spring AOP引入了其他几个关键概念:
●Aspect, 通常叫作方面,它是跨不同Java类层面的横切性逻辑。在实现形式上,既可
以是XML文件中配置的普通类,也可以在类代码中用” @Aspect"注解去声明。在运
行时,Spring 框架会创建类似? Advisor来指代它,其内部会包括切入的时机
(Pointcut)和切入的动作(Advice) 。
其实就是after和before这些东西的中间,正常执行的那个
●Join Point, 它是Aspect可以切入的特定点,在Spring里面只有方法可以作为Join
Point。
●@Advice,它定义了切面中能够采取的动作。
BeforeAdvice 和 AfterAdvice 包括它们的子接口是最简单的实现。而 Interceptor 则是所谓的拦截器,用于拦截住方法(也包括构造器)调用事件,进而采取相应动作,所以 Interceptor 是覆盖住整个方法调用过程的 Advice。通常将拦截器类型的 Advice 叫作 Around,在代码中可以使用“@Around”来标记,或者在配置中使用“
●Join Point仅仅是可利用的机会。可以插的地方
●Pointcut 是解决了切面编程中的Where问题,让程序可以知道哪些机会点可以应用某
个切面动作。
●而Advice则是明确了切面编程中的What,也就是做什么;同时通过指定Before、
After或者Around,定义了When,也就是什么时候做。
AOP如果不去刨根问底的话,还是算比较简单的,就是你从某个点插进去,告诉jvm你在这个点的前后要干嘛。
举个很简单的例子,你考研的时候分数要出来了,你把出成绩做个切点,before为修改你的分数为及格线。
第十天
2019年10月23日15:16:06
第三十八讲
单独从性能角度,Netty 在基础的NIO等类库之上进行了很多改进,例如:
●更加优雅的Reactor模式实现、灵活的线程模型、利用EventLoop等创新性的机制,
可以非常高效地管理成百上千的Channel.
●充分利用了Java的Zero-Copy机制,并且从多种角度,“斤斤计较” 般的降低内存分
配和回收的开销。例如,使用池化的Direct Buffer等技术,在提高I0性能的同时,减
少了对象的创建和销毁;利用反射等技术直接操纵SelectionKey,使用数组而不是Java
容器等。
●使用更多本地代码。例如,直接利用JNI调用Open SSL等方式,获得比Java内建
SSL引擎更好的性能。
●在通信协议、序列化等其他角度的优化。
总的来说,Netty 并没有Java核心类库那些强烈的通用性、跨平台等各种负担,针对性能
等特定目标以及Linux等特定环境,采取了一些极致的优化手段。
本来最近在看netty的。。。刚好这一章发现有个这个。。。。
Netty > java.nio + java. net!
从 API 能力范围来看,Netty 完全是 Java NIO 框架的一个大大的超集
除了核心的事件机制等, Netty 还额外提供了很多功能,例如:
●从网络协议的角度,Netty 除了支持传输层的UDP、TCP、OSCTP协议, 也支持
HTTP(s)、WebSocket 等多种应用层协议,它并不是单一协议的API。
●在应用中,需要将数据从Java对象转换成为各种应用协议的数据格式,或者进行反向的
转换,Netty为此提供了-系列扩展的编解码框架,与应用开发场景无缝衔接,并且性
能良好。
●它扩展了Java NIO Buffer, 提供了自己的ByteBuf实现,并且深度支持Direct Buffer
等技术,甚至hack了Java内部对Direct Buffer的分配和销毁等。同时,Netty 也提
供了更加完善的Scatter/Gather机制实现。
Netty 的几个核心概念
●@ServerBootstrap, 服务器端程序的入口,这是Netty为简化网络程序配置和关闭等
生命周期管理,所引入的Bootstrapping机制。我们通常要做的创建Channel、绑定端
口、注册Handler等,都可以通过这个统-的入口,以Fluent API等形式完成,相对简
化了API使用。与之相对应,0 Bootstrap则是Client端的通常入口。
●@Channel,作为一个基于NIO的扩展框架,Channel和Selector等概念仍然是
Netty的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。
●@EventLoop, 这是Netty处理事件的核心机制。例子中使用了EventLoopGroup。我
们在NIO中通常要做的几件事情,如注册感兴趣的事件、调度相应的Handler等,都
是EventLoop负责。
●@ChannelFuture, 这是Netty实现异步I0的基础之- -, 保证了同一个Channel操作
的调用顺序。Netty 扩展了Java标准的Future,提供了针对自己场景的特有C Future
定义。
●ChannelHandler,这是应用开发者放置业务逻辑的主要地方,也是我上面提到的
"Separation Of Concerns"原则的体现。
●OChannelPipeline, 它是ChannelHandler链条的容器,每个Channel在创建后,自
动被分配一个ChannelPipeline。在上面的示例中,我们通过ServerBootstrap 注册了
Channellnitializer,并且实现了initChannel 方法,而在该方法中则承担了向
ChannelPipleline安装其他Handler的任务。
第三十九讲
本章主要讲的是分布式的id唯一办法
因为不同的机器,时间可能不是一样的 所以获取当前时间可能会有不同的,可能会造成重复id,
着重介绍了推特的雪花算法
头部是 1 位的正负标识位。紧跟着的高位部分包含 41 位时间戳,通常使用 System.currentTimeMillis()。后面是 10 位的 WorkerID,标准定义是 5 位数据中心 + 5 位机器 ID,组成了机器编号,以区分不同的集群节点。最后的 12 位就是单位毫秒内可生成的序列号数目的理论极限
即使是这样,还是可能会造成有重复的问题,只是说稍微减少可能
2038年问题:一个4字节也就是32位的存储空间的最大值是2147483647,请注意!2038年问题的关键也就在这里———当时间一秒一秒地跳完2147483647那惊心动魄的最后一秒后,它就会转为负数,也就是说时间无效
其实就是由于32位整型溢出,时间将会被“绕回”(wrap around)成一个负数。记得以前用八门神器改游戏数据的时候应该也出现过这种问题
总结下,以前我都是以为看了自己就知道,结果正是那种 如果你自己说不出来,就是你没有搞懂。
因为大部分都是涉及到基础之类的,后面当然也有高档的gc,平常吹逼的时候能把这些完整的吹出来。当然,有时候是自己主动去装逼,有时候是同事突然问。这种问什么什么都懂的,可比你一句天若有情天亦老,猪大腰子火来炒好多了。。。。
说完这些说下自己的缺点吧
不愿去搞:关于前面的@State,虽然有去测试,但是流程没有完全跑通,考虑到因为是测试java api实际没什么用就去搞其它的了。这其实是很致命的
其次,说实话,三十多讲看了10天,但是这十天都是间隔比较大的。一般是忙的时候就忙,空的时候才看。其实,事在人为,贵在坚持。每天就算没空都要抽一点点时间。说实话,有时候我经常要自己回头来看笔记的
2019年10月23日17:16:20