笔记
2023-11-28 10:40:19 0 举报
AI智能生成
登录查看完整内容
个人笔记
作者其他创作
大纲/内容
强引用
弱引用
软引用
幻像引用
四种对象引用
Collections工具类
ArrayList和Vector区别
CopyOnWriteArrayList
ConcurrentHashMap
CopyOnWriteArraySet
并发集合有哪些
常见考点
7:使用数组+链表
8:使用数组+链表+红黑树
8前后区别
初始容量16
扩容2倍
当桶位大于装填因子*总量时扩容
如果链表长度大于8且桶为超过64,则链表转为红黑树。否则扩容2倍,重新hash
桶位置index = (n-1)&hash = hash%n(总桶必须是2的倍数)
节点小于6的红黑树退化为 链表
JDK8原理
线程不安全
可以存NULL
先通过哈希函数计算出实际存储地址
开放定址法
再散列函数法
HashMap 几采用了链地址法,也就是数组+链表
链地址法
如果两个不同的元素,通过哈希函数的胡的实际存储地址相同,产生hash冲突
HashMap 的主干是一个Entry数组。Entry是HashMap的基本组成单元,每个Entry包含一个key-value键值对
HashMap实现原理以及源码分析
两个对象equals为true,但hashCode不一定相同
由于hashMap是哈希表,通过计算得到HashCode,根据hashCode计算出实际存储地址
hashMap要求equals为true,hashCode也要相同,因此重写了equals方法也同时需要修改hashCode方法
hsahCode不同的对象不会在同个key桶里
hashCode方法就是对象存储位置的映像,因此HashCode能够快速定位到对象的地址。
Java hashCode() 和 equals()
子主题
主要参数
先计算hash
hash与table.length取模 得到index值
table[index]位置有值,则会在此位置形成链表
如果链表长度与table长度符合扩容则扩容,或者转化为红黑树
添加元素
put
hash与table.length 取模得到 index值
遍历链表或者红黑树
得到元素
get
计算hash值,得到index位置,便利查找,删除元素
删除元素
remove
生成一个new_table[old_table.length*2]
遍历旧HashMqp将值通过put方法插入新HashMap
扩容方法
resize
遍历将table制空
clear
与get类似,计算hash值,得到index值
containsKey
所以不要依赖这个方法做事情
遍历整个HashMap
contaionsValue
h ^= (h >>> 20 )^(h >>>12);return h ^(h>>>7)^(h>>>4);
hash
主要方法
HashMap实现原理浅析
在多线程情况下,出现同时put同个index的情况下,会导致某个值被取代
hash碰撞于扩容导致
HashMap 线程不安全问题
如果在扩容时,在数据从旧数据复制到新数组的过程中,这时候某个线程插入一条数据,这时候插入的时新数组。但是复制过程不做数据判断,就会导致新插入的值被覆盖了。
HashMap线程不安全的体现
HashMap 线程不安全的表现
HashMap并发导致死循环 CurrentHashMap
hashMap 数组扩容之后,最消耗性能的点就出现了。需要遍历旧数组中的数据,添加到新数组中去。resize
当hashMap桶位占用个数达到0.75*table.length
扩容将桶位数扩充为2倍
为了减少过多的扩容,使用HashMap时,需要尽可能的创建符合业务逻辑的长度
并且长度为2的n次方
深入理解HashMap (原理,查找,扩容)
因为 hash&(table.length-1) = hash%length
使用位运算符,可以提高计算效率
为什么要保证长度为2的n次幂
源码解读
HashMap
实现方法都加了synchronize
默认初始容量11,装填因子也是0.75
每次扩容2倍+1
直接使用hashCode作为hash值
线程安全
HashTable
红黑树实现
key自动排序
TreeMap
链表实现
需要有顺序的去存储key-value时
线程同样不安全
LinkerdHashMap
取消了分段锁,使用的Node锁+cas与synchronized来保证并发安全synchroized只锁住当前链表或者红黑二叉树的首节点,只要hash不冲突,就锁住,提高效率
JDK1.8
Map
动态数组实现
在添加数据时进行初始化,扩容时1.5倍
初始容量为10
实现RandomAccess接口,支持快速随机访问(只是标识接口)
数在扩容时,同时add数据,导致线程中的数据会被覆盖
多线程问题
线程安全的list
ArrarList
双向链表
插入和删除时间复杂度为O(1)
不支持快速随机访问
空间消耗更大一些,多两个引用
LinkedList
每次扩容2倍
底层加synchroized和ArrayList
Vector
写入时复制
适合大量读操作,不适合大量写操作
可以读写分离,并不是读写锁实现
特点
初始容量为0,写入进行扩容,新容量=插入容量+就容量
原理
List
TreeMap换皮
Comparator放入对象实现此接口,会使用比较器进行排序,否则使用升序排序
底层使用二叉树(红黑树)
TreeSet
HashMap换皮
主要使用HashMap实现,key不可重复,Value保持null的Object对象
HashSet
LinkedHashMap换皮
LinkedHashSet
和CopyOnWriteArrayList原理一样
CopyOnWriteSet
Set
ArrayDeque
Deque
LinkedBlockingQueue
ArrayBlockingQueue
常用子类
add
element
抛出异常
offer
poll
peek
有返回值,不会抛出异常
take
阻塞等待
offect
超时等待
四组常用API
BlockingQueue
AbstractQueue
Queue
Colletion
集合
boolean
byte 1个字节
char 2个字节
short 2个字节
int 4个字节
long 8个字节
fload 4个字节
double 8个字节
8种基本数据类型
Byte
Short
Interger
Long
Double
Boolean
Character
名称
Byte,Short,Integer,Long,Characher
面试点
8种包装类型
JAVA8与JAVA9的区别,8使用char数组,而9使用byte数组,通过coder来确定使用哪种编码
内部有final修饰,String类不可修改。字符串不可修改。
String
线程不可变,可以被该改变
StringBuilder
线程安全,内部有synchronized关键字修饰
StringBuffer
String 类型
数据类型
在JAVA8 中接口可以有default方法,也可以有静态方法
静态方法不能被重写,default方法可以被重写
接口中方法都是默认public的,并且不允许定义为其他的
接口中的字段都是static和final的
接口
抽象类和方法都是使用abstract修饰,如果类中包含抽象方法,那么类必须被声明为抽象类
抽象类不能被实例化,只能被继承
抽象类
抽象类与接口
RuntimeException
非运行时异常
运行时异常是不可处理的,交给JVM处理。例如空指针异常。对于非运行时异常则需要使用try-catch处理。否则编译不能同通过,例如SQLException。
区别
Exception
OutOfMemoryError 内存溢出异常
StackOverflowError 站溢出
ThreadDeath 线程问题
系统级别的异常,不可处理异常
Error
分类
Error 一般由JVM 抛出,Error 是Java 程序不能够处理的错误。Exception是java程序能够处理的异常。
Exception 和 Error 的区别?
异常
该类不能被继承
类
该方法不能被重写
方法
使得数值不能被修改
基本类型
引用不能变,但是被引用变量的对象本身被修改
引用类型
数据
可以声明的位置
使用java提供的反射技术,并且关闭安全校验也可以修改其值
final修饰的变量可以被修改
final
成员变量
1.父类(静态变量,静态代码块)
2.子类(静态变量,静态代码块)
3.父类(实例变量,普通代码块)
4.父类构造方法
5.子类(实力变量,普通代码块)
6.子类构造方法
初始化顺序
静态代码块
static
关键字
Java泛型设计原则:只是在编译时期没有出现警告,那么运行时期就不会出现ClassCasException异常
泛型:把类型明确的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型
简介
泛型类:在编译后泛型类就确定了类型
泛型方法,某个方法上使用了泛型,外界只看该方法。
子类泛型类型确定
子类泛型类型不确定:继续使用父类定义的泛型
泛型类派生出的子类
使用泛型
在泛型类中并没有像我们面向对象的继承结构,想要使用任意的泛型类型,我们都可以使用通配符。
?通配符可以表示匹配任意类型,任意JAVA类型都可以
通配符设置上限,?extends Type
?super Type
下限
?extends Type
子类可以从泛型读取
超类可以从泛型写入
PECS原则
使用通配符
如果参数之间的类型有依赖关系,或者返回值与参数之间有依赖关系,就可以使用泛型
如果没有依赖关系,就使用通配符,通配符会灵活一些
通配符和泛型方法
代码简洁,更可靠,编译期间通过,意味着运行时不会出现ClassCasException异常
泛型好处
泛型类型在编译期起作用,编译后在运行时将类型擦除,编译期保证数据安全,运行时提高效率。
类型擦拭
参考BeanDao实现
应用
泛型
1.反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法
2.反射可以在一个类运行的时候获取类的信息的机制,可以获取在编译期不可能获得的类的信息
3.对于任意一个对象,都能调用它的任意一个方法和属性
4.因为类的信息是保存在Class对象中的;而这个Class对象是在程序运行时被类加载器(ClassLoader)动态加载
5.当类加载器装载运行了类后,动态获取Class对象的信息,操作Class对象的属性,方法的功能称为反射
概念
反编译:.class->.java
通过反射机制访问Java对象中的属性,方法,构造方法
作用
Class c3 = new Refect().getClass(); ClassLoader 装载入内存。
Class c2 = Reflect.class; 返回类对象运行时真正所指的对象,所属类型
Class c1 = Class.forName(\"com.mxm.Reflect\") 会让ClassLoader装载类,并运行
获取Class对象的方法
Class c4 = Class.forName(\"com.mxm.Reflect\") Object o = c4.newInstance(); newInstance 使用类的加载机制
无参数创建对象
有参数创建对象
Class-类的创建;
反射类中的构造方法
Constructor
Field field = class.getDeclaredField(\"name\
获取属性
Field类描述的时属性对象,其中可以获取到很多属性信息,包括名字,属性类型,属性注解
Field类描述
在安全管理器中会使用checkPermission方法来检查权限,而setAccessible(true)并不是将方法权限改为public,二十取消Java的权限控制检查,所以public方法的accessible属性也是false
安全管理
Field field = class.getDeclaredField(\"name\")String prive = Modeifier.toString(field.getModofoers());getModofoers() 放回的是一个 代表类,成员变量,方法修饰符
修改属性的修饰符
反射方法
Field
Method m = class.getDeclaredMethod(\"setName\
Method
访问修饰符的信息
Modifier
涉及到的类
当Class.forName() 中路径获取不到对应的Class时,会抛出异常。
获取不到Class
修饰符导致的权限问题,抛出相同异常
获取不到Field
getField只能获取对象和父类的public 修饰的属性
getDeclaredField 获取对象中的各种修饰符属性,但是不能获取父类的任何属性
获取父类修饰符
获取不到父类的非public的方法
获取不到父类的构造方法
使用收到限制,对应的Class中必须存在一个无参数构造器,且必须有访问权限
Class.newInstance()
适应任何类型的构造器方法,无论是否有参数都可以调用,只需要setAccessible()方法控制访问权限。
Contructor.newInstance()
newInstance方法创建类对象的两种方法
例子:Public class TestMethod{ static void test(){}}Class class = Class.forName(\"TestMethod\");Method m = class.getDeclaredMethod(\"test\");m.invoke(null);关键Method.invoke的第一个 static 反复噶因为属于类本身,所以不需要填写对象,填写null就可以
放射静态方法
Java的泛型擦除概念,泛型T在编译后会被擦除,自动向上转型为Object
public class Test<T>{ public void test(T t){}}Class class = Test.class;Method m = class.getDeclaredMethod(\"test\
反射的泛型参数方法
反射框架:JOOR
反射进阶
反射
注解其实就是代码中的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相对应的处理
介绍
反射出该类的方法
通过方法得到注解上的具体信息
将注解上的信息注入到方法上
注入普通属性其实就三步:
得到想要类中注入的属性,得到该属性的对象
得到属性对应的写方法,通过写方法得到注解
获取注解详细的信息,将注解的信息注入到对象上
调用属性写方法,将已填充数据的对象注入到方法中
注入对象
自定义Annotation
RetentionPolicy.SOURCE:Java文件时期
RetentionPolicy.CLASS:Class文件时期
RetentionPolicy.RUNTIME:运行时期
通常使用RUNTIME
表示注解周期
@Retention
TYPE
FIELD
METHOD
PARAMETER
CONSTRUCTOR
LOCAL_VARIABLE
ANNOTATION_TYPE
RACKAGE
用于指定注解修饰程序单元
@Target
将被javadoc工具提取成文档
@Documented
使得注解具有继承性
@Inherited
元注解
避免低级错误,检查是继承父类的方法有效性
@Overried
标记为过时的
@Deprecated
去除编译器警告
@SuppressWarnings
当把一个不是泛型的集合赋值给一个带泛型的集合时候,这种情况就容易出现堆污染
@SafeVarargs
指定接口为函数式接口
@FunctionalInterface
常见注解
注解
Consumer void accept(T t) 将T 类型的参数引用与该方法
Supplier T get(); 返回类型为T 的对象
Predicate boolean test (T t): 确定类型为T 的参数是否满足test逻辑
函数接口
被该注解注解的是函数式接口,表示该接口只能定义一个方法。
@FunctionalInterface(接口注解)
concat
distinct
parallel
sequential
unoradered
map
filter
sorted
limit
skip
有状态
Intermediate
min
max
count
sum
某个功能在Stream接口中没有,多半就在collect方法中实现
常用场景
collect
生成唯一值
reduce
t->t
Function.identity()
通用
Terminal
操作类型
Steam(parallelStream):懒加载方式,运行时才会生成数据
Lamada
JAVA基础知识
如何实现一个流程控制
并发工具
继承Thread类创建线程
实现Runnable接口创建线程
Runnable定义的方法时run
Callable定义的方法是call
Runnable是没有返回值的
Callable的call是有返回值
Runnable的run是不能抛出异常的
Callable的call是可以抛出异常的
使用Callable和FutureTask创建线程
实现方式
使用stop方法强行停止,但是Java已经标记为过期方法,不推荐使用
使用退出标识,正常退出,run执行完成
使用interrupt方法中断线程
停止线程
只唤醒一个等待线程
无法指定唤醒哪个线程
notify();
notifyAll();
唤醒所有处于等待的线程
唤醒线程进入就绪队列,得到锁才能运行
wait();方法,那么线程便会处于该对象等待池中,等待池不会去竞争该对象的锁
优先级高的线程竞争得到锁的概率大,但是不能保证一定是优先级高的先得到锁
notify()和notifyAll()有什么区别
sleep() 是线程类的方法,不涉及通信,调用时会暂停此线程指定的时间,但是监控依旧保持,不会释放锁资源。
wait() 时Object的对象,用于线程通信,调用时会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才进入对象锁线程池准备获得对象锁的运行状态
sleep()方法必须捕获异常InterruptedException,而wait()\otify()\otifyAll()不需要捕获异常
sleep方法只让出CPU,而并不会释放同步资源锁
线程执行sleep()方法后转入阻塞状态
sleep()方法指定的时间为线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后开始执行。
notify的作用相当于唤醒睡着的人,但是不包括帮他分配任务,只是叫醒。
sleep()与wait()有什么区别
守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;
如果还有一个或者以上的非守护线程则不会退出。(以上针对正常退出,调用System.exit则必定会退出)
JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程
Thread的SetDaemon方法设置称为守护线程,必须在线程启动之前调用
当你在一个守护线程中产生了其他线程,那么这些新产生的线程不用设置Daemon属性,都将是守护线程,用户线程同样。
意义 JVM 不需要等待它退出,让JVM喜欢什么就退出,不用管它。
Daemom线程,它有什么意义?
等待,通知,唤醒
锁机制
同步机制
ThreadLocal类
使用全局变量
共享内存
通讯
多线程如何通讯协作
新生
NEW
运行
RUNNABLE
阻塞
BLOCKED
等待
WAITING
TIMED_WAITING
终止
TERMINATED
java线程中的6个状态
基础知识
可重入锁
synchronized与RentrantLock都是可重入锁
最大作用避免死锁
ReentrantLock
ReentrantReadWritedLock
ReadWriteLock
Lock能完成synchronized 所有实现的所有功能
Lock有比synchronized更精确的线程语义和更好的性能,而且不强制要求一定要获得锁
synchronized自动释放锁,而Lock一定要求程序员手动释放,最好在finally释放
synchronized与java.util.concurrent.lock.Lock
乐观锁和悲观锁是两种思想,用于解决并发场景下的线程竞争问题
乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据,因此乐观锁不会上锁,只是在执行更新的时候,如果别人修改了数据,则放弃操作。
悲观锁,则在修改数据之前,加锁,防止别人共同操作
理解
加锁
悲观锁
CAS操作包括3个操作数:需要读写的内存位置(V)进行比较的预期值(A)拟写入的新值(B)
如果内存位置V的值等于预期A的值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋锁到成功为止。
CAS是由CPU支持的原子性操作,其原子性是在硬件层面进行保证的。
CAS循环等待需要不断调用CPU,资源消耗大
功能局限性,只能针对当个变量进行原子性操作。一般需要结合volatile配合线程安全
A状态,先被修改成B状态,然后又改回来A状态
导致已经被修改过,但是线程不知道
可以加入版本号,时间戳的方式实现。
ABA问题
高竞争下的开销问题
缺点
CAS机制
乐观锁
遵循先来先用
公平锁
FIFO原则,线程进入等待线程
队列开头的回合
非公平锁
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值非公平锁在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的(在ReentrantLock中很明显可以看到其中同步包括两种,分别是公平的FairSync和非公平的NonfairSync。公平锁的作用就是严格按照线程启动的顺序来执行的,不允许其他线程插队执行的;而非公平锁是允许插队的。默认情况下ReentrantLock是通过非公平锁来进行同步的,包括synchronized关键字都是如此,因为这样性能会更好。因为从线程进入了RUNNABLE状态,可以执行开始,到实际线程执行是要比较久的时间的。而且,在一个锁释放之后,其他的线程会需要重新来获取锁。其中经历了持有锁的线程释放锁,其他线程从挂起恢复到RUNNABLE状态,其他线程请求锁,获得锁,线程执行,这一系列步骤。如果这个时候,存在一个线程直接请求锁,可能就避开挂起到恢复RUNNABLE状态的这段消耗,所以性能更优化)
公平锁与非公平锁
互斥锁时同步状态的一种特殊情况,相当于只存在一种临界资源,因此·1同时只能给一个线程提供服务。在复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。
互斥锁
实现
乐观锁与悲观锁
锁
servlet线程不安全
线程安全就是只,多个线程访问同一段代码,产生结果一致
JAVA基础
多线程
线程同步
同步有几种方式实现
jvm的优化操作
代码重排序
禁止代码重排序
保证线程可见性
无法保证原子性
保证线程读取到的值是内存真实值,而不是缓存
volatile有什么用
核心线程数
corePoolSize
最大线程数
maximumPoolSize
当线程池中线程数量超过最corePoolSize时,空闲线程的存活时间
keepAliveTime
keepAliveTime单位
unit
线程工厂
threadFactory
new ThreadPoolExecutor.CallerRunsPolicy();只要线程池没有关闭,该策略直接在调用者的线程中,运行当前被丢弃的任务
new ThreadPoolExecute.AbortPolicy();直接抛出异常,阻止系统正常功能
new ThreadPoolExecutor.DiscardPolicy();该策略默默丢弃无法处理的任务,不处理
new ThreadPoolExecutor.DiscardOldestPolicy();该策略丢弃最老的一个请求
拒绝策略
handler
一般自己创建有界队列实现
任务队列
workQueue
线程池
TreadPoolExecutor
java.util.concurrent工具包,并发大师Doug Lea的杰作
时JUC的核心
围绕一个同步队列和park还有自旋锁实现
公平锁时在唤醒的时候不能插队,非公平锁可以插队
获取公平锁和非公平锁的区别
lock方法真正调用的是acquire
lock加锁
调用sync.release方法
unlock解锁
详解
AQS队列同步器
可重入可中断锁,他是Lock最重要的实现类之一
FairSync()这个方法时公平锁,调用NonfairSync()方法时非公平锁
减到0执行await方法
减法计数器
CountDownLatch
触发线程运行
加法计数器
CyclicBarrier
acquire会减少一个资源,release释放锁
信号量
Semaphore
读写互斥
写写互斥
读读并存
ReentrantReadWriteLock
读写锁
参考BlockingQueue
SynchronousQueue
阻塞队列
ForkJoin
Future
JUC
网络编程的几本C/S模型,即连个进程的通信
服务端提供IP监控端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双向通信
传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口,Socket负责起连接操作,连接成功后,双方通过输入和输出流进行同步阻塞
采用BIO通信模型服务端,通常由一个独立的Acceptor线程负责监听客户端的连接
它接收到的客户端连接请求之后为每个客户端创建一个新的线程进行链路处理完成后,通过输出流返回答应客户端,线程销毁
典型一个请求一个答应通信模型
并发高,CPU、IO等待的冲突问题导致性能急剧下降
最大问题
BIO服务通信模型
传统BIO
如果使用CachedThreadPool线程池,其实除了能自动帮我们管理线程,本质还是BIO
使用FixedThreadPool我们就有效的控制了线程最大数量,保证了系统有限资源的控制,实现了N:M模型
读数据时比较慢,大量并发的情况下,其他接入消息只能一直等待,这是最大的弊端
伪异步I/O
磁盘操作:file
字节操作:inputStream和OutputStream
字符操作:Reader和Writer
对象操作:Serializable
网格操作:Socket
常见使用
BIO
NIO面向块,BIO面向流
NIO非阻塞,BIO阻塞的
NIO与BIO
以前的BIO模型是阻塞的,也就是说有多少个流就必须要有多少个线程,因为在等待接收或者发送的时候该线程是阻塞的。如果IO是非阻塞的,那么我们就可以让一个线程管理多个IO或者,多个线程管理多个IO
非阻塞作用
直接缓冲区需要经过一个Copy的阶段从内核空间Copy到用户空间
直接缓冲区不需要经过copy阶段
使用ByteBuffer的AllocateDirect方法
使用FileChannel的map方法
使用直接缓冲区的两种方式
直接缓冲区和非直接缓冲区
数组
本质
capacity最大容量
position当前读写的字节数
还可以读写的字节数
状态变量
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
BoubleBuffer
常用实现
buffer缓冲区
通过Channel是对原I/O中的流模拟的
可以理解为管道就是火车道,而缓冲区就是火车
管道理解
FileChannel从文件中读写数据
DatagramChannel能通过UDP读写网络中的数据
SocketChannel能通过TCP读写网络中的数据
ServerSocketChannel可以监听新进来的TCP连接,像web服务器那样,对每个新进来的连接都创建一个SocketChannel
常用管道
管道可以同时进行读写,流只能读或者只能写
管道可以实现异步读写数据
管道可以从缓冲中读取数据,也可以写数据到缓冲中
管道和流的区别
Channel管道
一个组件,可以检测盒多个Nio channel,看卡读或者写时间是否就绪
多个Channel事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
Selector选择器
Java IO 全解
NIO的三个核心部分
创建NIO服务端的步骤
非阻塞面向缓冲区的高性能IO
NIO
IO
JAVA
就是客户端和服务器的一种交互的一种通信格式
GET
PUT
HEAD
DELETE
POST
OPTIONS
HTTP提供的方式
RESTful风格就是基于HTTP生成的
告知服务器意图
HTTP1.0 每次进行HTTP通信都会断开
HTTP1.1版本后,就是持久连接了,一次HTTP请求可以持续处理多个请求
持久化连接为官线化方式发送创造了条件,再一次HTTP连接里面,不需要等待服务器响应请求,就可以继续发送第二次请求
持久连接
200正常响应
204成功处理,但是服务器没有新数据返回,页面不更新
206 对服务器进行范围请求,只返回一部分数据
2xxx,一般都是成功处理
301请求的资源已经重新分配到新的URI中,URL地址已经改变了【永久重定向】
302请求资源临时被分配到新的URI中,URL地址没有变
303与302相同功能,但明确客户端应该采用GET方式获取资源
304发送了附带请求,但是不符合条件【返回未过期的缓存数据】
307与302相同,但不会把POST请求变成GET
3xx
400请求报文语法错误
401需要认证身份
403没有权限访问
404服务器没有这个资源
4xx
500 内部资源错误
503服务正在忙
5xx
常用状态码
HTTPS就是披着SSL的HTTP
HTTP在建立通信线路的时候使用公开的私有秘钥,当建立完成连接后,随后就使用共享秘钥进行加密和解密
HTTPS 是基于第三方认证机构来获取受认可得证书
1.用户向web服务器发起一个安全连接的请求
2.服务器返回经过CA认证的数字证书,证书里包含了服务器的public key(公钥)
3.用户拿到数字证书,用自己浏览器内置的CA证书解密得到服务器的public key
4.用户服务器的public key 加密一个用于接下来的对称加密算法的秘钥,传给web服务器
5.因为只有服务器有private key 可以解密,所以不用担心中间人拦截这个加密的秘钥,服务器拿到这个加密的秘钥,解密获取秘钥,再使用对称加密算法,和客户端完成接下来的通信
过程
HTTPS简述
DNS:负责解析域名
HTTP:产生请求报文数据
TCP协议:分割HTTP数据,保证数据运输
IP协议:传输数据包,找到通信的目的地址
网站通信初略的过程
HTTP是的无状态的,也就是说,它是不对通信状态进行保存的。它并不知道之前通信的对方是谁
由于我们很多时候都需要知道对方是谁,于是我们欧了Cookie来解决问题
HTTP是不保证状态的协议
使用压缩技术把实体主体压小,在客户端再把数据解析
这种技术可以实现断点续传
使用分块传输码,将实体主体分块传输,当浏览器解析到实体主体就能够显示了
提升传输效率
代理
能够提非HTTP请求的操作,访问数据库什么的
网关
建立一条安全的通信路径,可以使用SSL等加密手段进行通信
隧道
服务器与客户端之间的应用程序
请求行:包含请求方法,URI,HTTP版本信息
请求首部字段
请求内容实体
空行
请求报文
一个住状态行【用于描述服务器对请求的处理结果】
首部字段【用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它会送回来的数据】
一个空行
实体内容【服务器向客户端回送数据】
响应报文
HTTP请求和报文组成
默认使用长链接,只要客户端与服务端没有断开TCP请求,则可以一直保持链接,可以发送多次HTTP请求
管线化,客户端可以同时发出多个HTTP请求,而不是一个个的等待响应
断点续传,实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输
HTTP1.1新版特性
多路复用
二进制分帧层
HPACK对HTTP2头部压缩
服务器推送
流量控制
流优先
其他改动
HTTP2
HTTP
Open System Interconnection
操控奥模型是国际标准化组织定制的一个用于计算机或通信系统间的标准体系,一般OSI参考模型或者七层模型
只要遵循这个七层协议就可以实现就可以实现计算机互联
所有网络有关
定义物理设备标准
物理层
将物理层接收的数据进行MAC地址的封装和解分装,也可以简单的理解为物理寻址
网卡,交换机
STP
数据链路层
控制子网的运行,如逻辑编址
分组传输,路由
IP
网络层
定义一些传输数据的协议端口
TCP
UDP
传输层
负责在玩那个罗中两个节点建立,维持和终止通信
SMTP,DNS
会话层
确保一个系统的应用层发送的消息可以被另外一个系统的应用层读取
TeInet
表示层
文件传输,文件管理,电子邮件信息处理
应用层
OSI 七层模型各层作用
OSI七层模型
TCP是面向连接的,可靠的流协议,通过三次握手建立连接,通信完成时要拆除连接。
UDP是面向无连接通信协议,UDP通信时不需要接收方确定,属于不可靠传输,可能出现丢包现象
TCP和UDP
通俗易懂的讲解TCP/IP三次握手与四次挥手姿势
连接需要三次握手,断开需要四次挥手
ACK:TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1
SYN:在连接建立时用来同步序号,当SYN=1而ACK=0时,表明这是连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请求或者连接接受报文。
FIN即完,终结的意思,用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经完毕,并要求释放连接。
第一次握手
服务单收到数据包后由标志SYN=1知道客户端请求建立连接,服务端将标志SYN和ACK都设置为1
ack=J+1,随机参数一个值seq=k,并将数据包发送给客户端确定连接请求,服务器进入SYN_RCVD状态
第二次握手
则开始将数据包发送给服务端,服务端检查ack是否为K+1,ACk是否为1,如果正确连接成功,客户端和服务端进入ESTABLISHED状态,完成三次握手,随后客户端与服务端之间可以开始传输数据。
第三次握手
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN WAIT 1 状态
第一次挥手:
Server收到FIN后,发送以恶ACK给Client,确定序号为收到序号+1(与 SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第二次挥手:
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第三次挥手:
Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手
第四次挥手:
挥手
三次握手和四次挥手
TCP/IP中的数据包
TCP中通过序列号与确认应答提高可靠性
网络编程基础(网络基础知识)
Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。
主机A的应用程序要能和主机B的应用程序通信,必须通过Socket建立连接,而建立Socket连接必须需要底层TCP/IP协议来建立TCP连接。
建立TCP连接需要底层IP协议来寻址网络中的主机。
Socket套接字
HTTP是无状态,浏览器和服务器进行一次HTTP操作,就建立一次连接,但是任务结束就中断
短连接是指SOCKET连接后发送后接收完数据后马上断开连接
连接=传输=关闭连接
WEB网站的http服务一般都是短连接,因为长连接对于服务端来说会消耗一定的资源。
使用场景
短连接
长连接指建立SOCKET连接后不管是否使用都保持连接,但是安全性较差
连接=传输数据=保持连接=传输数据=关闭连接
数据库的连接用长连接,如果短连接频繁的通信会造成Socket错误,而且频繁的Socket创建也是对资源的浪费。
长连接
网络编程常见术语
TCP/IP协议
应用层的任务就是通过应用进程间的交互来完成特定的网络应用。
应用层协议定义是应用进程间通信和交互的规则。
对于不同的网络应用需要不同的应用协议。
运输层的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。
面向连接
可靠传输
面向报文
不可靠传输
主要是一下两种协议
运输层
在计算机网络中进行通信的两个计算机之间可能会经过多个数据链路,也可能还要经过很多通信子网。
网络层的任务就是选择合适的网间路由和交换节点,确保数据及时传送。
数据链路层,通常简称链路层。
两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议。
两个节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧。
物理层上只以透明的比特流进行传送,尽可能的屏蔽掉具体的介质和物理设备的差异。
五层协议体系结构
简略版
应用数据被切割成TCP认为最适合发送的数据块
TCP给发送的每个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层
校验和:TCP将保持它的首部和数据的校验和。
TCP的接收端会丢弃重复的数据
流量控制:TCP连接的每方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲能接纳的数据。
拥塞控制:当网络拥塞时,就减少数据的发送。
也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认,在收到确认后再发下一个分组
ARQ协议:
当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文。
超时重传:
TCP可靠传输
网络
不在线程之间共享该变量
将状态变量修改为不可变的变量
在访问状态变量时使用同步
避免错误措施
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。
当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。
结论
从一开始就考虑线程安全问题,并做好设计工作。会比线程不安全的类使用后,当出现问题再去处理要容易的多。
代码首先要能正常运行,然后才是性能,其次是,在满足需求的条件下,尽可能的简单,在没有性能问题前,尽量不去优化性能。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
个人积累
并发编程实践
并发
理解用例图
图例
用例的一个最主要的特征是它是相对独立的
业务用例的定义是业务执行者希望通过和所研究组织交互获得的价值。
业务用例
系统能够为执行者提供的、涉众可以接受的价值。
系统用例
理解1
UML
try { // 业务逻辑。}catch (Exception e){ // 输出错误日志。}finally { // 消息签收。}
无论如何倒要在finally里去签收消息,避免异常消息再放入队列中,导致持续的异常死循环(产生原因:由于Spring默认requeue-rejected配置为true,默认情况下,消息异常后会重新入队,入队后又会被消费,从而导致了死循环)。
消费者消息处理模板代码
MQ
单体日志
中文翻译:https://wu-sheng.gitbooks.io/opentracing-io/content/
OpenTracing
分布式日志
日志
指令集少,需要指令多,基于栈实现,可以跨平台
执行性能差
架构
通过类加载器加载类
JAVA进程
执行JAVA程序
使用Runtime 或者 System 的exit方法或Runtime的halt方法
退出
生命周期
1996
第一台商用虚拟机
1.0~1.4
只有解释器
Sun Classic VM
1.2
只在Solaris短暂使用
JIT 与 解释型混合使用
热点代码的探测
Exact VM
1997
1.3默认虚拟机
GC
在最佳响应时间与最佳执行性能中取得平衡
HotSpot VM
专注服务端应用
只使用即时编译器
性能高,程序启动时间慢
BEA 的 Jrockit
多用途
三大商用虚拟机之一
在IBM产品上很快
IBM的 J9
软硬绑定
商用
Azul VM
Liquid VM
Microsoft JVM
Taobao JVM
历史版本
简图
ClassFiles 字节码文件
通过类全限定名获取定义此类的二进制流
这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
启动类加载器
使用C/C++实现,嵌套在JVM内部
JAVA_HOME/jre/lib/rt.jar
加载核心类库
不继承java.lang.ClassLoader,无父类加载器
加载应用类和应用类加载器,并指定为他妈呢的父类加载器
只加载Java,javax,sum开头的类
BootStrap ClassLoader引导类加载器
JAVA编写
派生于ClassLoader类
父类加载器为启动类加载器
加载java.ext.dirs或者jre/lib/ext
如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
Extension ClassLoader扩展类加载器
父类加载器为扩展类加载器
负责加载环境变量classpath或者系统熟悉java.class.path指定类库
该类加载是默认的类加载器,一般来说,Java应用的类都可以由它来完成加载
通过ClassLoader#getSystemClassLoader()方法可以获取该类加载器。
应用类加载器
Application ClassLoader系统类加载器
BootStrap ClassLoader引导类加载器
非JAVA实现
引用加载器
继承与ClassLoader的加载器
Extension ClassLoader扩展类加载器
Application ClassLoader系统类加载器
JAVA实现
自定义加载器
由上至下,包含关系。(非继承)
不包括启动类加载器
抽象类,其后所有的类加载器都继承自ClassLoader
返回该类的超类加载器
getParent()
加载器名称为name的类,返回java.lang.Class类的实例
loadClass(String name)
查找
findClass(String name)
查找被加载过
findLoadedClass(String name)
从内存中获取
define
连接指定一个Java类
resolveClass
常见方法
ClassLoader
当前类
Class.forName(\"className\").getClassLoader();
获取线程上下文
Thread.currentThread().getContextClassLoader();
系统
ClassLoader.getSystemClassLoader();
调用者
DriverManger.getClaserClassLoader();
获取方式
按需加载方式
类加载时,先去查看父类加载器是否能加载此类
父类在递归
直到引导类
父类都不能加载,子类才加载
双亲委派机制
Loading 加载
CA FE BA BE
字节头标识
确保Class文件的字节流的合法性,保证被加载的类的正确性,不会破坏虚拟机本身安全
意义
文件格式校验
元数据校验
字节码校验
符号引用验证
四种校验方式
Verify 验证
为类变量分配内存并设置默认值,0值
这里不包括使用final修饰的static 因为final在编译时就分配了,准备阶段会显式初始化
不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量时会随着对象一起分配到堆中的
Prepare 准备
将常量池里的符号引用装换为直接引用的过程
事实上,解析操作往往会伴随着JVM的执行完成初始化之后再执行
解析动作主要是对类,接口,字段,类方法,接口方法,方法类型等。
Resolve 解析
Linking 连接
初始化阶段就是执行类构造器方法<clinit>()的过程
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
构造器方法中指令按语句在源文件中出现的顺序执行
<clinit>()不同于类构造器(类的构造器在虚拟机视角为<init>())
该类具有父类,JVM会保证子类的<clinit>()执行前,父类<clinit>()已经执行完成
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
Initialization 初始化
负责将Class文件加载到内存当中。(只负责加载,不负责运行)(媒婆)
Class Loader 类加载器
类型信息
域信息
方法信息
JIT代码缓冲
运行时常量池
静态变量
存储类型
尽管所有的方法区在逻辑上属于堆的一部分,但是一些简单的实现可能不会选择进行垃圾收集或者进行压缩。HotSpotJVM将方法区又称作非堆(Non-Heap)
可以看做独立于Java堆的内存空间
于堆一样,时线程共享区域
物理不连续,逻辑连续
可固定大小,也可扩展
同样会抛出OOM异常
概述
设置成较高值,避免过多的FullGC
建议将MetaspaceSize
枚举
完整有效名称 包.类名
直接父类完整有效名称
修饰符
有序列表
包括
类型
域名称
域类型
域修饰符
域
方法名称
方法返回值类型
参数表
字节码,操作数栈,局部变量表
异常表
用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
具备动态性
类似菜谱->真的食材
将符号表的符号装载成真实的引用
字符串常量池
JIT编译内容
减少class的文件大小
为什么要用常量池
内存结构
永久代很难确定
替换永久代原因
但是又很有必要
条件很苛刻
无引用就回收
常量池
实例被回收
无派生子类实例
类加载器被回收
无引用
java.lang.Class
Method Area 方法区/Metaspace 元空间
JVM实例
堆内存
一对一关系
堆是物理上不连续,但是逻辑上连续的一块内存空间
几乎所有的对象实例都存储在堆内存中
数组和对象永远不会出现在栈上,因为栈帧中保持引用。引用指向对象或者数组中堆中位置
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集时才会被移除
堆,是GC的重点区域
现代GC大多数都是针对分代收集理念设计的
Eden
Survivor
新生代
Youg Generation Space
养老代
Tenure Generation Spce
永久代
Permanent Space
元空间
Mate Space
1.7永久区/1.8 元空间
内存细分
用于设置堆的起始内存,等价于 -XX:InitialHeapSize
-Xms
bi表示最大内存,等价于-XX:MaxHeapSize
-Xmx
一般将两个值设置成一样的,这样GC后不需要重新计算分配堆的大小,从而提高性能
设置新生代与老年代的比例
新生代1,老年代2
例如-XX:NewRatio=2
-XX:NewRatio
默认时8:1:1
事实上是6:1:1
设置Eden与Survivor的比例
-XX:SurvivorRatio
自适应的分配策略
-XX:-UserAdaptiveSizePolicy
设置新生代的空间大小
一般不使用
-Xmn
设置GC晋升年龄
-XX:MaxTenuringThreshdld
查看默认值
-XX:PrintFlagsInitint
最终值
jps 进程号
jinfo -flag 参数名 进程号
-XX:PrintFlagsFinal
查看逃逸分析的筛选结果
-XX:+PrintEscapeAnalysis
显示开启逃逸分析
-XX:DoEscapeAnalysis
指令参数
一旦堆超过了-Xmx的最大内存值,就会抛出异常OutOfMemoryError异常
对象创建过程
对象分配策略
TLAB
图
对象创建在Eden区
YGC
Minor GC
出现STW
Eden区满了
Survivor区
计算年龄
Eden 存活的放到
s0,s1 有个一定是空的
另外一个是from
谁空是to
Minor GC 时也需要判断是否存活
到了就晋升到老年代
判断GC年龄是否到晋升年龄15
只有Eden满了才会触发MinorGC/YGC
触发特殊规则
晋升
Survivor 满了
直接晋升到老年代
内存大于新生代
GC频繁在新生区,很少在老年区,几乎不在永久代或者元空间
总结
对象创建于回收过程
对象的生命周期不同
大部分的对象是临时对象
不用每次都把堆里的所有对象进行GC
优化GC的性能,对于不同生命周期的对象,进行分代回收
分代原因
可以避免多线程内存争夺问题
快速分配策略
空间不大,只占Eden的1%,大对象无法分配
设置是否开启
默认开启
-XX:UseTLAB
设置占比
-XX:TLABWasteTargetPercent
如果在TLAB分配失败,JVM会采用加锁机制确保数据原子性
栈上分配,标量替换优化技术
如果逃逸分析后,对象没有出方法,就在栈上分配
锁消除
对象就是聚合量
Class person{ id,name}
聚合量
标量替换
逃逸分析
堆
Heap 堆
PC寄存器对物理PC寄存器的一种抽象模拟
含义
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
运行最快的存储区域
线程私有的
上下文切换时,记录当前运行的指令位置
唯一不会发生OutOtMemoryError区域
PC寄存器存下一步指令地址
Program Counter Register 程序计数器(PC寄存器)
外部环境
本地方法大多数都是C写的
操作系统交互
解释器使用C实现
Sun java
一般在某些领域使用其他语言能够有更加好的性能,则使用其他语言实现
不能与abstract连用
类似Thread类的中方法
Java调用非Java代码编写的接口
本地方法
线程私有
本地方法使用的栈
本地方法可以通过本地接口方法接口来访问虚拟机内部的运行时数据区
甚至直接使用本地处理器中的寄存器
直接使用内存等
当某个线程调用本地方法时,它就进入了一个全新的并且不再受到虚拟机限制的世界,它与虚拟机拥有同样的权限
并非所有JVM都支持本地方法以及本地方法栈
Native Method Stack 本地方法栈
栈管运行
堆管存储
栈与堆类似按菜谱做菜,菜谱时栈,菜时堆
栈与堆
与方法一对一的关系
一个活动运行中,只有一个活动栈帧,称之当前栈帧
执行引擎运行只针对当前栈帧
如果调用了新方法,新方法就会称为当前栈帧
栈运行的原理
本地变量表
局部变量数组
基本数据类型
对象引用
retrunAddress
定义一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
不存在线程安全问题
局部变量表所需的容量大小是在编译期确定下来的
变量槽
局部变量表的存储单元
可重复利用,节省资源
Slot
存在局部变量表index=0 上
this
引用数据类型
数据类型分类
initaial阶段显示赋值
都经过默认初始化赋值LInking的Prepare阶段
类变量
随着对象的创建在堆空间中分配,并进行默认赋值
实例变量
使用前,必须进行显示赋值,否则编译不通过
局部变量
类中声明的位置分
静态变量与局部变量的对比
Local Variables 局部变量表(LocalVariableTable)
可以使用数组或者链表实现
在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈或者出栈
用于存储计算过程中中间结果,同时作为计算过程中变量的临时存储空间
方法刚开始执行时,操作数栈时空的
并非采用访问索引的方式来访问数据,只能使用入栈和出栈
如果被调用的方法有返回值,返回值将会被压入到当前栈帧的操作数栈中
解释引擎是基于栈的执行引擎
类型在执行编译时就确定了
将栈顶元素全部缓存在CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
栈顶缓存技术
指令多,内存与CPU的交互次数多,导致性能比较低
Operand Stack 操作数栈
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了实现动态链接Dynamic Linking
在JAVA源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
虚方法
动态链接的作用就是为了将这些符号引用转化为调用方法来直接引用
别名:指向运行时常量池的方法引用
Dynamic Linking 动态链接
帧数据区内容
重点
存储了该方法PC寄存器中的值
正常退出
异常退出则由异常表控制
方法退出后,PC计数器的值作为返回地址,即调用该方法的指定的下一条指令的地址
byte
char
short
int
ireturn
long
lreturn
float
freturn
dobble
dreturn
Date
areturn
void
return
一个方法的返回指令,根据方法的返回值实际数据来确定
Return Address 方法返回地址
图解
图解2
栈帧中调优最密切的部分就是局部变量表
局部变量表中的变量也是重要的垃圾回收的根节点
只要被局部变量表中直接或者间接引用的对象不会被回收
调优
编译期间既可以知道其实际值,可以将调用方法的符号引用转化为直接引用的过程称之为静态引用
早期绑定
静态链接
被调用方法在编译期间无法确定值,也就是说需要在运行期间才能将符号引用转化为直接引用的,这种引用转换过程几倍动态性,因此称之动态链接
晚期绑定
动态链接
空参构造器大多都是早期绑定
final 方法就没有晚期绑定
静态方法
私有方法
final方法
实例构造器
父类方法
编译器就确定了调用版本,这个版本在运行时不可变的,称为非虚方法
其他都称虚方法
(虚方法)与非虚方法
解析阶段确定唯一方法版本
调用静态方法
非虚
invokestatic
调用<init>方法,私有及父类方法,解析阶段确定唯一方法版本
invokespecial
调用所有虚方法
除了final之外都是虚方法
invokevirtual
调用接口方法
invokeinterface
动态解析出需要调用的方法,然后执行
JAVA动态语言的体现
具体体现:Lamda
而不是对语言本身的修改
对操作指令集的修改
invokedynamic
编译期间就确定类型
变量类型
静态语言
运行期间才能确定类型
变量值类型
动态语言
动态语言与静态语言
小记忆
操作数栈顶第一个元素纪录了实际凑在哦的类型
直接引用
有访问权限
Java.lang.IllegalAccessError 异常
经常发生在Maven导包中,重复jar包等
无访问权限
如果在该类型中找到对应操作的名称相符的方法,则进行访问权限的校验
如果没有合适的方法,则继续向上递归,(双亲委派) 重复2操作
如果到最后都没找到合适方法,则抛出java.lang.AbstractMethodError异常
非虚方法编译期就可以确定方法类型了,所以不用
类加载的链接阶段创建并初始化,类的变量初始化准备之后,JVM会把类的方发表也初始化。
提高效率
虚方法表
方法重写
虚拟机中几条方法调用指令
方法的调用
内部结构
附加信息
存储的是栈帧
线程一致
程序的运行
对象的引用地址
保持局部变量
部分结果
返回值
仅此于PC寄存器
入栈
出栈
只有两种操作方式
无GC
优点
OOM,SOFE
优缺点
设置栈内存大小
默认1024k
-Xss
参数
Java Virtual Machine Stack 虚拟机栈
栈,堆,元空间
元空间与堆
关系图解
普通线程
GC回收线程
守护线程
后台线程
栈溢出
StackOverflowError
栈溢出情况
不能,但是能竟可能不。
调整栈大小,可以保证不出现溢出吗?
非,浪费资源
分配的栈内存越大越好吗?
不会
垃圾回收是否会涉及到虚拟机栈?
不一定,基本类型则安全,引用类型得看引用类型是不是在方法区内。
方法中定义的局部变量是否线程安全?
面试题
new出来的对象都在堆里
扎乱记忆
Runtime Data Area 运行时数据区
JVM核心之一
虚拟机的执行引擎是由软件实现的
能被执行那些不可被硬件直接支持的指令格式
CAFE BEBA
主要任务负责将字节码装到其内部
将字节码翻译成机器语言
逐行将字节码翻译成机器指令
解释器
将源代码直接编译成机器语言
JIT编译器
完全采用解释器模式执行程序
-Xint:
完全采用即时编译器的模式执行程序,如果即时编译出现问题,解释器会介入执行
-Xcomp:
采用解释器+即时编译器的混合模式共同执行程序
-Xmixed:
Execution Engine 执行引擎
Native Method Interface 本地方法接口
最常见
单例模式,生成器
工厂模式
new
反射方式,只能调用空参数,public
newInstance()
Class
反射的方式,可以调用空参,带参构造器
newInstance(Xxx)
不调用任何构造器,当前类需要实现Cloneable接口
clone()
从文件中,网络中,获取对象的二进制流
使用反序列化
第三方库Objenesis
对象创建的方式
判断对象类是否加载,连接,初始化
指针碰撞
空闲内存列表
为对象分配内存
CAS
处理并发安全问题
每个属性都设置默认值,保证对象实例字段在不赋值时可以直接使用
初始化分配到的空间
哈希值
GC年代
锁状态标识
线程持有的锁
偏向线程id
偏向时间戳
运行时元数据
执行元数据InstanceKlass,确定对象所属的类型
类型指针
先放父类再放子类
相同宽度在一起
实例数据
对齐填充
设置对象的对象头
执行init方法进行初始化
创建步骤
对象实例化
栈帧中的引用如何找到堆中的具体实现的?
问题
栈引用->句柄池地址->具体实例
句柄访问
栈引用->具体实例->类信息
直接指针(Hotspot实现)
对象访问定位
对象实例化内存布局和访问定位
在涉及到大内存使用时,使用直接内存可以避免内核态和用户态的切换提高性能
直接内存
大汇总
不可变序列
char[]
1.8之前
byte[]
1.9之前
底层变化
String Pool
最小值1009
默认长度为60013
所以当hash冲突多了链表越来越长就会卡
底层是一个固定大小的Hashtable
字符串常量池不会存储相同的内容的字符串
设置StringTable长度
-XX:StringTableSize
和普通对象一样
在堆中
内存分配
String str = \"1\";String str2= str.intern();
先去池里找对象是否存在,用equals判断
存在则返回池中地址,不存在则创建新的字符到池里
返回新对象
保证相同的字符对象只会存储一份,节省内存
如果池里有,则不会放入,返回已有串池中的对象地址
如果没有,则会复制此对象,放入串池汇总,并返回串池中的对象地址
1.6
如果有,则不会放入,返回池中地址
如果过没有,会把对象地址引用复制一份,放入串池,返回串池中的引用地址
1.7
注意对象和对象引用地址
各版本差异
所以使用的内存会比没使用的少
因为有GC
intern()
直接冲常量池里取值
常量和常量拼接
结果就在堆中,非字符串常量池
原理StringBuilder
等价于new String();
StringBuilder s = new StringBuilder()
s.append(\"a\")
s.append(\"b\")
s3=s.toString();
例子 String s3= s1+s2;
只要有一个是变量
常量池中不会存在相同内容的常量
从常量池中取值,没则创建
intern()方法
字符串拼接操作
StringTable
垃圾回收概述
每个对象都保持一个整型的引用计数器属性,用于记录对象被引用的情况
简单,判断效率高
额外开销
循环引用问题
引用计数算法
追踪性垃圾收集
从GC Roots 从上至下的搜索可到达的对象
可到达的对象间形成引用链
不可到达的对象则称作垃圾对象
能够被根对象直接或间接引用的对象才是存活对象
线程中的参数,局部变量表
虚拟机栈中引用对象
类引用的静态变量
方法区里静态属性引用的对象
字符串常量池引用
方法区常量引用对象
被同步锁synchronized持有的对象
Class对象,异常对象,类加载器
虚拟机内部引用
采用栈方式存储变量或者指针,对象在堆里,但是本身不在堆里的就是Root
如果局部回收的话,未回收区域的对象也可以作为Root
GC Roots
可达性分析
定义在Object
对象销毁前的自定义处理逻辑
只能被调用一次
未重写不调用
finalize()
存活
可触及
未调用finalize()
可复活
调用过finalize()
不可触及
对象三种状态
清除:未被标记的对象被回收
标记:存活对象
STW
效率不算高
需要STW
清理出来的内存不连续,碎片化
清除只是把需要被清除的内存存入空闲内存表里,后续使用直接覆盖
标记清除算法
运行高效
无碎片化
G1实现内存和时间开销
region
存活对象多,效率就差,因为要全部复制
特别的
复制算法
清除:未被标记的对象被回收,并进行压缩操作
标记-清楚-压缩
移动式
解决标记算法碎片化问题
避免复制算法内存消耗
效率上低于复制算法
移动对象同时还需要调整引用地址
移动过程需要STW
标记压缩算法
只不同区间采用不同的回收算法
分代思想被现有虚拟机广泛应用
分代
交替执行
不断的上下文切换
造成系统吞吐量的下降
增量收集算法
控制STW时间
G 1
与分区不同的是,他是将堆分成一个个分区Region
分区
垃圾回收算法
等同于Runtime.getRuntime().gc()
会触发Full GC
无法保证具体运行时机
System.gc()
OOM是没有空闲内存了,且垃圾收集器也无法提供更多的内存了
官方解释
设置的堆内存过小
存在大量大对象,并且GC无法即时回收
产生原因
内存溢出
对象不会再被程序使用到了,但是GC又无法回收它们的情况,叫内存泄漏
广义上不合理的生命周期的对象也可以称为内存泄漏
存储渗漏
用生命周期关联的一些生命周期比较低的对象
连接未手动断开
例子
内存泄漏
内存的溢出与内存泄漏
Stop The World
停止应用程序的运行
确保可达性分析的一致性,准确性
可达性分析算法执行时
场景
只能减少STW时间
GC都无法避免STW
ParNew
Parallel
Scavenge
Parallel Old
并行垃圾回收器
Serial
串行垃圾回收器
CMS
G1
并发垃圾收集器
垃圾回收的并行与并发
在特定位置才可以GC
太少,GC时间长
太多,GC太频繁,影响性能
选择执行时间较长的指令作为安全点
主动中断
安全点
是只在一段代码片段里,对象的引用关系不会发生变化,在此区域中任何位置GC都是安全的。
就是在这段代码里,不会影响GC
所以在运行这段代码时,可以同时GC
GC没结束,先不要脱离这段代码
安全区域
安全点与安全区域
正常new出来,最传统的引用
只要存在强引用垃圾收集器永远不回收
强
只要系统在发生内存溢出前,就会把这些对象列入回收范围中进行二次回收。
会在第二次GC被回收
还有用,但是非必须
一般用来做缓存
Object obj = new Object();SoftReference<Object> sf = new SoftReference<Object>(obj);obj = null;
java.lang.ref.SoftReference
软
只能存活到下一次垃圾回收,换句话来说,只要GC它就得死
例子WeakReference
弱
无法通过虚引用来得到对象实例
在垃圾收集器回收时收到系统通知
PhantomRefernce
虚
各种引用
终结器引用
垃圾回收相关概念
应用运行时间/总时间(包括垃圾收集时间)
吞吐量
STW,应用停止服务时间
暂停时间
堆区内存大小
内存占用
垃圾收集时间/总时间
垃圾收集开销
收集频率
对象出生到死亡再到回收时间
快速
GC分类与性能指标
Parallel Scavenge
新生代收集器
Serial Old
老年代收集器
整体
根据业务场景选择合适垃圾收集器
不同的垃圾回收器概述
最基本最悠久的垃圾回收器
老年代GC
CMS 兜底方案
标记整理
单线程运行
单线程下运行高效
简单
Client 模式下不错的选择
优势
会开启Serial GC 与 Serial Old GC
-XX:+UseSerialGC
开启
Serial 回收器,串行回收
并行
Serial 多线程版本
适合多核版本
新生代使用
ParNew回收器:并行回收
提高CPU利用率,适合后台运算,不需要过多交互任务
可控制的吞吐量
自适应的调节策略
多线程版本
Parallel old
开启新生代
-XX:+UseParallelGC
开启老年代
-XX:+UseParallelOldGC
设置并行数
-XX:ParallelGCThreads
设置最大停顿时间
-XX:MaxGCPauaseMillis
垃圾收集时间占总时间比例
-XX:GCTimeRatio
自适应调节策略
-XX:+UseAdaptiveSizePolicy
参数设置
Parallel回收器:吞吐量优先
低延迟
实现回收时收集线程与用户线程同时工作
对于响应速度有要求,希望系统STW时间尽可能短的。
采用标记清除算法
速度快
只是标记出GC Roots能直接关联出来的对象
初始标记
直接关联对象开始遍历整个对象图过程,耗时,但是不需要STW
并发标记
修正因为并发标记期间,因用户线程继续工作导致标记产生变动的那一部分对象标记来纪录。
重新标记
清理删除掉标记阶段判断为死亡的对象。释放内存空间。
并发清理
重置线程
过程中STW需要的时间比较短,在耗时的并发标记和并发清除阶段。可以并发执行,不会STW
因为并发的过程,所以需要保证在CMS执行时,还有多余的空间来让CMS有时间清除垃圾。
一般设置一个阈值来处理。
如果因为CMS没有能即时清理垃圾,会出现Concurrent Mode Failure
此时会使用Serial Old来处理,那就STW很久了
解释
采用标记清除算法,会产生碎片化的内存空间
不可避免,因为并发执行过程中,用户线程在运行
无法分配大内存对象时, 不得不提前FullGC
产生内存碎片
并发阶段会占用部分CPU资源
CMS对CPU资源非常敏感
因为只会清除标记阶段标记的垃圾。所以不会清理并发阶段产生的新垃圾
CMS无法收集浮动垃圾
总结弊端
指定FUllGC 后进内存压缩
-XX:+UseCMSCompactAtFullCollection
设置多少次Full GC后进行内存压缩
-XX:CMSFullGCsBeforeCompaction
设置CMS线程数
-XX:ParallelCMSThreads
手动指定使用CMS收集器执行
-XX:+UseConcMarkSweepGC
设置内存使用的阈值
1.6及以上为92%,之前为68%。
如果内存增加的慢,阈值可以大一些。如果快则小一些。
尽可能避免Full GC
-XX:CMSInitiatingOccupanyFraction
CMS回收器:低延迟
对于服务器端的多核心CPU以及大内存模式下效果比较好
在延迟可控的情况下,尽可能的提高吞吐量
效果
Region来表示Eden,幸存者0区,1区,老年代等
避免全区的回收,可以精确到某个区来进行回收工作
会计算每个Region的回收价值
按照回收价值进行回收
分区算法
此时用户线程STW
G1回收时支持多线程回收
并行性:
G1与应用线程交替执行,部分工作可以和应用程序同时执行,一般不会在整合回收阶段完全STW
并发性:
并行与并发
G1任然是分代收集的垃圾回收器。他会区分为老年代和年轻代。同其他GC一样
堆空间分为若干个区域,这些区域中逻辑上是年轻代或者老年代
回收时,它可兼顾老年代和年轻代
分代收集
Region之间,使用复制算法
整体上时标记压缩算法。
空间整合
软实时
每次根据允许的收集时间,优先回收价值最大的Region,保证有限时间内尽可能高的提高回收效率。
可预测的暂停模型
不具备完全碾压CMS
会产生额外的内存占用和CPU消耗
指定使用G1
-XX:+UseG1GC
1~32MB
2的次方
设置Region的大小
-XX:G1HeapRegionSize
默认是200ms
最大停顿时间
-XX:MaxGCPauseMillis
STW工作线程数,最大是8
-XX:ParallelGCThread
默认是STW工作数的四分之一
并发标记线程数
-XX:ConcGCThreads
默认45
触发并发GC周期的JAVA堆率阈值
-XX:InitiatingHeapOccupancyPercnet
大内存,多处理器
需要低GC延迟
堆6G以上,GC低于0.5
适用场景
用于存储大对象
一个装不下,就找连续的H区
还装不下就Full GC
整理后的H区连续的装不下就报OOM
humongous
并行独占式
可能冲Eden区到Survivor 也可能是 老年代区
也可能都涉及
可能会包含YGC同时执行
老年区分配区空闲区域,空闲区域称为新的老年区
不一定会对整合老年代进行回收
OGC
一个对象可能被不同区域的对象引用
分区间存在相互引用
每个分区都有Rememberd Set
Reference 写操作时,都会产生一个Write Barrier 中断操作
判断写入的引用指向的对象是否和该Reference类型数据在不同Region
如果不同,通过CardTable把相关引用信息记录到引用指向对象所在的Region对相应的Remembered Set中
垃圾收集时,在GC根节点的枚举范围加入Rememberd Set,可以保证不进行全局扫描,也不会遗漏
Rememberd Set
只会回收Eden和Survivor区
处理引用
复制对象
处理RSet
更新RSet
扫描根
独占清理
再次标记
根区域扫描
初始化标记
百分百是垃圾的被回收了
Region的回收价值也 被计算出来了
分8次回收,垃圾回收价值高于65%,切优先回收价值高的
混合回收
年轻代回收
具体过程
GC过程
G1回收器:区域划分代式
适用单线程
响应速度优先
串行
多CPU环境雨CMS配合适用
并行运行
后代运行不需要多交互的场景
吞吐量优先
单CPU客户端模式
老年代
串行运行
后台运行不需要多交互场景
使用与B/S
并发运行
面向服务端应用
响应速度
标记压缩,复制
新老
并发并行
垃圾回收器总结
GC日志分析
垃圾收齐器的新发展
-XX:PrintCommandLineFlags
jinfo -flag 相关参数 进程ID
查看GC指令集
杂
垃圾回收器
Java语言:跨平台的语言
只要语言遵守JVM字节码格式就可以使用JVM运行
只和Class里的字节码关联
JVM:跨语言的平台
词法解析
语法解析
语义解析
生成字节码
javac
前端编译器的主要任务是将Java代码转化为字节码
字节码文件的跨平台性
ECJ
ajc
Java的前端编译器
类文件有几个部分?
透过字节码指令看代码细节
字节码是一种二进制的类文件,内部是JVM指令,可以直接编译成机器码
一种由一个字节长度,代表某种特定含义的操作码,以及随其后的数字代表操作数
字节码指令
虚拟机中许多指令不包括操作数,只有操作码
操作码
astore_1~3
操作码和操作数
astore 4
bipush 10
字节码
.class 文件在磁盘中
网络中的流
内存等
存在形式
本质上就是二进制流
咖啡宝贝
ca fe ba be
使用魔数而不是基于文件扩展名,因为文件扩展名是可以随意改动的
前4位
魔数
第五第六位 副版本
第七第八位 主版本
1.8
十进制52
00 00 00 34
高版本兼容低版本,低版本无法运行高版本的class
向上累加
java版本号1.1=45
Class文件版本
基石
内容最为丰富的区域之一
十进制22
只有21项常量池
用来无指定向引用时来引用
0下标是空出来的
从下标1开始有常量项
00 16
举例
常量池表计数器
第九位第十位
进入方法区的运行时常量池存储
类加载后
字面量和符号引用
下标从1开始
文本字符串
声明final的常量值
字面量
java/lang/String;
java.lang.String全限定名
类和接口的全限定名
add()
字段的名称和描述符
描述符的作用是用来描述字段的数据类型,方法参数列表(数量,类型,顺序)和返回值
B
C
D
double
F
I
J
S
Z
V
Ljava/lang/Object;
L
对象类型
[
数组类型
类型解析
虚拟机运行时,需要从常量池中获取对应的符号引用
在此类加载过程中解析阶段将其替换为直接引用,并翻译到具体的内存中
符号引用转为直接引用
就是符号
符号引用
与虚拟机内存结构相关
是真实的地址值
只有加载Class文件时才会动态链接
方法的名称和描述符
常量池表
在常量池之后
两位
访问标志
在访问标识之后
当前类索引
当前类索引之后
父类索引
父类索引之后
说明个数
后续每两位标识一个索引
接口索引集合
类索引,父类索引,接口索引集合
fields
类级别变量
不包括局部变量
不包括父类变量
字段无法重载
说明
两位,表示字段个数
字段计数器
访问标识
字段名索引
字段描述符的索引
属性计数器
属性表集合
字段表集合
对应着一个类或者接口的方法信息
method_info
方法计数器
方法名索引
描述索引
属性集合
方法体
方发表集合
2
属性名索引
4
属性长度
1
属性表
code
辅助信息
附加属性表计数器
附加属性名的索引
必须是2
全限名称
源文件索引
SourceFile
内容
虚拟机的基石:Class文件
可能跟着0个或者多个的操作数Operands
由一个字节长度的,代表某种特定操作含义的数字(称作操作码,Opcode)
Java虚拟机的指令
Class文件结构
解析字节码作用
生成的字节码包括局部变量表信息
javac -g 操作
<classes>
<options>
java版本
-version
仅仅显示公共的类和成员
-public
受到保护的/公共类和成员
-protected
所有
-p -private
显示程序包/受到保护的/公共类和成员
-package
显示正在处理的类系统信息
-sysinfo
显示静态最终常量
-constants
输出内部类型签名
-s
输出行号和本地变量表
-l
对代码进行反编译
-c
-v -verbose
javap
字节码所属的路径
Classfile
最后修改时间,大小
Last modified
MD5散列值
MD5
源文件名称
Compiled from
副版本
主版本
flags
Constant pool
字段表集合信息
字段名
字段描述符:字段类型
常量属性值
ConstantValue
构造器也在这
描述符
descriptor
方法的code属性
操作数栈的最大深度
stack
局部变量表所需的存续空间
locals
方法接收参数个数
args_size
操作数
偏移量
Code
行号表
指名字节码指令的偏移量和java源代码中的行号对应关系
LineNumberTable
描述内部局部变量的信息
局部变量表
开始
Start
长度
Length
槽位
Name
Signature
localVariableTable
<clinit>
Static()
源文件名
方法表信息
文件解读
使用javap指令解析Class文件
do{ 自动计算PC寄存器的值加1; 根据PC寄存器的指示位置,从字节码流中取出操作码; if(字节码存在操作数) 从字节码流中取出操作数; 执行操作码所定义的操作;}while(字节码长度>0);
执行模型
它们的操作码助记符中都有特殊的字符来表明专门为那种数据类型服务
i
l
s
b
c
d
f
显示相关
只能数组类型
arraylength
没有明确地指明操作类型的字母
隐式相关
goto
数据类型无关
无相关
字节码与数据类型
xload
xload_<n>
将一个局部变量加载到操作数栈
-1
iconst_m1
inconst_<i>
fconst_<f>
lconst_<l>
dconst_<d>
aconst_null
指令const系列
8位参数
bipush
16位参数
sipush
指令push系列
万能的
ldc
8位
ldc_w
long,double
ldc2_w
指令ldc系列
将一个常量加载到操作数栈
xstore
xstore_<n>
xastore
存储到局部变量表
n标识索引
后续需要byte参数紧跟
x标识类型
将一个数值从操作数栈存储到局部变量表
wide
扩充局部变量表的访问索引指令
对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
加载与存储指令
加法
sub
减法
mul
乘法
div
除法
rem
求余
neg
取反
inc
自增
shl
shr
ushr
位移
or
位或
and
位与
xor
异或
位运算
dcmpg
dcmpgl
fcmpg
fcmpl
lcmp
比较
算术指令
剔除布尔类型
数值类型进行转化
一般用于用户显式类型转换操作
f2d
float转化为double
int->long->float->double
转换规则
不会出现
可能会
long->float
long->double
高精度绝对不能使用基本数据类型进行计算
精度损失
byte,char,short->到int类型的宽化转化类型转换实际不存在。
补充说明
宽话类型转化
l2i
long->int
其余也一样
特殊举例:double->short
转化规则
会有精度损失,但是不会报异常
精度损失问题
0
NaN,不确定值
无限大
小的超过精度-0
大的超过精度+无穷大
NaN不变
double->float
窄化类型转化
类型转化指令
创建对象指令
基本数组
newarray
引用类数组
anewarray
多维数组
multianewarray
创建数组指令
将操作数栈地址复制一份
dup
将值放到栈中
getstatic
从栈中取值
putstatic
访问类字段
getfied
putfield
访问类实例字段
字段访问指令
把x类型加载到栈
xaload
把x类型从栈中赋值到堆
数组长度
数组操作指令
将判断结果压入栈
用来判断对象是否属于某个类
instanceof
用于检查类型是否可以强行装换,如果可以不会改变操作数栈,否则抛出ClassCastException异常
checkcast
类型检查指令
对象的创建与访问指令
用于调用对象实例方法
支持多态
用于调用接口方法
搜索实现接口的方法,并调用合适的实现者
调用需要特殊处理的实例方法
构造器
都是静态类型绑定的
动态绑定的方法
方法调用
void则为return
xreturn
返回值指令
方法调用与返回指令
弹出一个solt
pop
弹出两个solt
pop2
dup_x1
直接复制
不带_x
复制后插入
带x
交换栈顶两个元素
swap
什么都不做
nop
操作数栈管理指令
x:df
xcmpg
xcmpl
压入1
g
压入-1
NaN
l与g
栈顶为v2,顺位为v1
压入0
v2=v1
v1>v2
v1<v2
将栈中两个操作数弹出
比较指令
当栈顶的int类型值等于0时候跳转
ifeq
当栈顶int类型值不等于0时跳转
ifne
当栈顶值小于0时跳转
iflt
当栈顶值小于等于0时跳转
ifle
当栈顶int类型数值大于0
ifgt
大于等于0
ifge
为null
ifnull
不为null
ifnuonnull
条件跳转
=
if_icmpeq
!=
if_icmpne
<
if_icmplt
<=
if_icmple
>
if_icmpgt
>=
if_icmpge
引用类型等于
if_acmpeq
引用类型不相等
if_acmpne
比较条件跳转指令
case连续
由于连续,所以效率会高一些
tableswitch
case值不连续
lookupswitch
多条件分支跳转
got_w
用于指令跳转到偏移量,指令执行的目的就是跳转到偏移量指定位置。
无条件跳转
比较控制指令
会清空操作数栈的数据,将异常压入栈中
athrow指令
异常处理指令
隐式的
code 里没有体现
方法标识里体现
accges flags
方法级同步
monitorenter
monitorexit
如果是则进入同步代码块
进入监视器同步代码块时,判断是否为0或者是否>0且为自己持有
代码块同步
同比控制指令
按指令集用途大致分成9类
操作数栈,用来存放计算的操作数以及返回结果。
字节码程序可以将计算结果缓存在局部变量表中,形成一个数组
可以存this指针
可以存字节码中的局部变量
其中long与dubble需要占8个字节,2个槽位
操作数栈回忆
简单的来说,就是将Java字节码文件加载到内存中,并构建出类模板对象
通过class后缀文件取得
zip,jar包
数据库中
http网络协议传输
动态Class二进制信息
通过类全名,加载得到二进制流
解析二进制流数据结构
生成Class类实例,生成类模板
查找并加载类二进制数据,生成Class的实例。
1.8之前在永久代
本地内存里
1.8之后再元空间
类模板存储在方法区
引用方法区的中的类模板
堆区中
Class实例对象
存储
数组类本身不是由类加载器负责创建的
引用类型需要准遵从类加载过程
数组类的加载
加载Loading
确保字节码的合法,合理和规范的。
魔数检查
版本检查
长度检查
格式检查
是否继承final
是否有父类
抽象方法是否有实现
语义检查
跳转指令是否指向正确位置
操作数是否合理
字节码验证
符号引用的直接引用是否存在
验证Verification
静态变量分配内存,并将其初始化为默认值
不包括基本数据类型的字段使用static final修饰的情况,因为final在编译时就会被分配了,准备阶段会显式赋值。
准备Preparation
将类,接口,字段和方法的符号引用转化为直接引用
解析Resolution
连接Linking
为类的静态变量赋值
包括静态代码块里的内容
执行类的初始化方法<clinit>()方法
由父及子,静态先行。
由编译器生成
无静态类变量
有静态类变量但是无显式赋值
有静态类常量
字面量赋值
且显式赋值中不涉及到方法或者构造器方法调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行的
static final
哪些场景下不会生成<clinit>
多线程环节被加锁同步了
是否是隐式锁,看不到
<clinit>()线程安全性的问题
当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
当调用类的静态方法时,即当使用了字节码invokestatic指令
当使用类、接口的静态字段时(final修饰符特殊考虑),比如getstatic或者putstatic指令。
使用java.lang.reflect包中的方法反射类方法时。
当初初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口就要在其之前被初始化。
初始化该MethodHandle指向的方法所在的类。
当初次调用MethodHandle
主动使用
除了以上的情况属于主动使用,其他情况属于被动使用,被动使用不会引起类的初始化。
并不是在代码中出现的类,就不一定会被被加载或者初始化,如果不符合主动使用的条件,类就不会初始化。
子类引用父类的静态变量,不会导致子类初始化
数组定义引用类引用,不会触发类的初始化
引用常量不会触发此类或者接口的初始化,因为常量在链接阶段就已经被显式赋值了。
调用ClassLoader类或者loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
例如
被动使用
主动使用会调用<clinit>()
类的主动使用与被动使用
初始化Initialization
使用Using
用Java集合来存储类加载器
Class对象->类模板
对象->Class对象->类加载器
通过Class对象获取
类,类的加载器,类的实例
当类被加载,链接,初始化后,它的生命周期就开始了。
当Class类对象不再被引用,即不可触及,Class对象生命就结束了。
Sample类在方法区的数据也会被卸载,从而类的生命周期结束。
类的生命周期
不会被回收
总能间接或直接引用到
扩展类加载器也不太可能
要强制调用虚拟机卸载
类的卸载
垃圾回收
不可回收原因
卸载Unloading
类的加载器
负责将Class信息的二进制数据流读入到JVM内部,转换称为一个Class对象实例。
交给JVM区连接,初始化等
它不负责
然后
ClassLoader的作用
代码中通过ClassLoader加载class对象
Class.forName(name)
this.getClass().getClassLoader().loadClass()加载class对象
显式加载
没有直接通过ClassLoader来加载
隐式加载
类加载器的分类
由加载它的加载器和这个类本身称作类在虚拟机中的唯一性
何为类的唯一性
每个类加载器都有自己的命名空间,命名空间由该加载器所有的父类加载器所加载的类组成。
在同一命名空间中,不会出现类的完整名字相同的两个类
在不同命名空间汇总,有可能会出现类的完整名称相同的两个类。
命名空间
C/C++编写,嵌套JVM内部
不继承Java.lang.ClassLoader没有父加载器
加载扩展类和应用程序的类加载器, 并指定为他们的父类加载器
处于安全考虑,其只加载java,javax,sun 开头的类
启动类加载 BootStrap ClassLoader
使用Java语言编写
继承Classloader类
用户创建的JAR在此目录下,也会自动由扩展类加载器加载。
从Java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库
扩展类加载器Extension ClassLoader
用户自定义类装载器
Java语言编写
继承ClassLoader类
父类加载器为扩展类加载
负责加载环境变量classpath或者系统属性java.class.path
应用程序中的类加载默认是系统类加载
它是用户自定义类加载器的默认父加载器
通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
应用类加载器Application ClassLoader
包含关系
他们不是继承关系,但是可以宽化的说父类
组成
得到当前类的ClassLoader
clazz.getClassLoader()
获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获得系统的ClassLoader
ClasLoader.getSystemClassLoader()
得到ClassLoader的途径
复习
遵从双亲委派机制
加载名称为name的类,返回Class类实例
解析
解析操作
resolveClass(Class<?>)
查找二进制名称为name的类,返回结果为Class类实例,这是一个受保护的方法。JVM鼓励我们重写此方法,需要自定义加载双亲委派机制,该方法会在检查完父类加载器之后被loadClass()调用
在URLClassLoader重写
调用defineClass创建类
findClass(String)
根据二进制流创建类实例
官方注释信息系介绍:
获取类加载器的父类加载器
Abstract ClassLoader
SecureClassLoader
ExtClassLoader
AppClassLoader
继承
URLClassLoader
加载扩展类加载器
加载系统类加载器
可以看到给线程上下文添加了系统类加载器
Launcher
系统类加载器与扩展类加载器
前者将Class文件加载完会创建对应实例并初始化
后者只是加载Class文件到内存里
Class.forName()与ClassLoader.loadClass()
ClassLoader与现有类加载器的关系
重点知识
1.2之前不支持双亲委派机制
顶成类加载时不能知道底层类加载类
rt.java提供对外服务,可由应用层自行实现的接口称为SPI
SPI
线程上下文的加载器
热替换
热部署
破坏双亲委派机制
隔离加载类
修改加载方式
扩展加载源
防止源码泄露
好处
继承URLClassLoader
简单实现
推荐
重写findClass
重写loadClass
继承ClassLoader
自定义类加载器
再谈类加载器.
Class 文件结构
只查看LVMID,即本地虚拟机唯一id,不显示主类名称等。
jps -q
输出应用程序主类的全类名称,或者jar全路径
jps -l
输出虚拟机进程启动时传递给主类mian()的参数
jps -m
列出虚拟机进程启动时的JVM参数。
jps -v
可以综合使用
jps(查看正在运行的Java进程)
jstat -<opteion> [-t] [-h<lines>]<vmid>[<interval>[<count>]]
查看ClassLoader的相关信息:类装载,卸载数量,总空间,类转载所有消耗的时间
-class
-compiler
说明编译的方法
-printcompilation
显示堆信息
-gc
与gc类型,关注最大和最小空间
-gccapacity
关注已使用的空间占总空间百分比
-gcutil
额外输出最后一次或者当前正在产生GC的原因
-gccause
新生代GC
-gcnew
新生代最大最小空间
-gcnewcapacity
-geold
老年代最大最小空间
-gcoldcapacity
永久代最大最小空间
-gcpermcapacity
option参数
用于指定输出的统计数据周期,单位为毫秒
interval参数
用于指定查询总数
count参数
可以在输出信息前加一个Timestamp列,运行时间。
推导出GC时间占总运行时间的比例
比例超过20%,表示堆压力比较大,超过90%,表示堆几乎满了
可以比较启动时间与GC时间
后续可能会OOM
发现ou越来越高
经验
-t参数
可以周期的加表头信息
-h参数
基本语法
补充
jstat(查看JVM统计信息)
用来查看虚拟机配置参数,并且可以调整虚拟机参数配置
基本情况
jinfo [options] pid
输出全部的参数和系统属性
no option
输出对应名称的参数
-flag name
开启对应名称的参数
-flag [+-] name
设定对应名称参数
-flag name = value
输出全部参数
-flags
输出系统全部参数
-sysprops
options
系统初始化值
java -XX:+PrintFlagsInitial
java -XX:+PrintFlagFinal
扩展
jinfo(实时查看和修改JVM配置参数)
获取dump堆快照文件,二进制文件
可以获取Java进程的内存信息,包括Java堆内存各个区域使用情况,堆中对象统计信息,类加载信息等
jmap [option] <pid>
输出堆中对象的统计信息,包括类,实例数量和合计容量
特别的:-histo:live只统计存活的对象
-histo
输出堆空间详细信息,包括GC的使用,堆配置信息,内存等。
-heap
生成Java堆存储快照:dump文件
特别的:-dump:live只保持堆中存活对象
-dump
以ClassLoader为统计口径输出永久代的内存状态信息
-permstat
显示在F -Queue中等待Finalizer线程执行finalize方法的对象
-finalizerinfo
强制
-F
手动导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<filename.hprof>
自动导出
使用1:导出内存映像文件
jmap -heap pid
-jmap -histo
使用2:显示堆内存相关信息
查看系统的ClassLoader信息
jmap -permstat pid
查看堆积在finalizer队列中的对象
jmap -finalizerinfo
使用3:其他作用
需要在安全点执行,不到安全点,会一直等待下去
小结
jmap(导出内存映像文件&内存文件使用情况)
用于分析heap dump 文件
jhat(jDK自带堆分析工具)
线程快照,虚拟机指定进程中的每一条线程正在执行的方法堆栈集合
死锁:deadlock
等待资源:Waitting on conditon
留意
当正常输出的请求不被响应时,强制输出线程堆栈
除堆栈外,显示关于锁的附加信息
如果调用到本地方法的话,可以显示C/C++的堆栈
-m
帮助操作
-h
jstack:打印JVM中线程快照
可以用来导出堆,内存使用,查看进程,导出线程,执行GC,JVM运行时间等
多功能工具
列出所有的JVM进程
jcmd -l
针对进程支持的所有指令
jcmd pid help
显示指定进程的指令命令数据
jcm pid 具体命令
jcmd(多功能命令行)
代理服务器,建立与远程服务器的监听
jstatd(远程主机信息收集)
指令
jconsole
visual VM
JMC
JDK自带的工具
MAT
JProfler
Arthas
Btrace
第三方
工具概述
内存,线程,类
基本概述
jdk/bin
启动
本地连接
local
remote
advanced
三种连接
主要作用
JConsole
功能强大,多合一,故障诊断,性能监控可视化工具
环境变量,cpu,gc,堆,方法区,线程信息
插件安装
连接方式
生成dump文件
装入dump文件
实时监听线程状态
抽样区
主要功能
Visual VM
强大的JAVA堆内存分析工具,分析dump文件
hprof文件
可以看到所有的对象信息:对象实例,成员变量,存储与栈中的基本数据类型值和存储于堆中的其他对象的引用值
所有类信息,包括ClassLoder,类名称,父类,静态变量
GCRoot到所有的这些对象的引用路径
线程信息,包括线程的调用栈及此线程的线程局部变量TLS
dump文件内容
不是万能的,主流的可以识别
可以生成内存泄露的报表
两点说明
获取dump文件
获取堆dump文件
分析堆dump文件
案例:tomcat堆溢出
支持使用OQL语言查询对象信息
eclipase MAT
IDEA插件使用,收费的。
比MAT更加强大
安装配置
消耗大
Instrumentation重构方式
消耗小
不能提供特殊功能,方法次数等
Sampling抽样方式
数据采集方式
遥感监测Telemetries
内存试图LiveMemory
堆遍历 heap walker
cpu视图 cpu views
线程视图 threads
监视器&锁 Monitors & locks
具体使用
案例分析
JProfiler
安装和使用
查看帮助指令
help
基础指令
当前系统的实时数据面板
dashboard
查看当前JVM线程的信息
thread
jvm
其他
JVM相关
mc,redefine
sc
sm
jad
classloader
class/classloader相关
monitor
trace
watch
tt
monitor/watch/trace相关
诊断指令
Java Mission Control
Flame Graphs
可达分析算法。严格上说,对象不被程序使用了,但是GC不能回收它们,才叫内存泄露
宽泛的泄露,过长的生命周期。
内存泄露理解与分析
静态的List,Map等
静态集合类
单例对象存有外部对象的引用
单例模式
内部类长期引用外部类对象
内部类持有外部类
各种数据库连接等
各种连接,例如数据库连接,网络,IO连接
变量不合理的作用域
改变哈希值
缓存泄露
监听器和回调
Java内存的8种泄露情况
不合理的list使用,旧数据没有置空
案例1
案例2
内存泄露案例分析
内存泄露
GUI图形界面
系统调优
比较稳点
以-开头
例如-help且下面的指令
各种选项
64位不支持-client
补充-server与-client
类型1:标准参数选项
非标准化参数
功能还是比较稳点的,但官方说后续会更变
以 -X开头
运行 java -X 命令可以查看所有的X选项
-Xmixed 混合执行(默认)-Xint 解释器-Xcomp 编译器-Xms <size> 设置初始Java堆大小-Xmx <size> 设置最大Java堆大小
解释器模式
-Xint
编译器模式
-Xcomp
混合模式
-Xmixed
JVM的JIT编译模式的相关选项
初始堆大小
-Xms<size>
最大堆大小
-Xmx<size>
Java线程堆栈大小
-Xss<size>
特别地
类型2:-X参数选项
非标准的
使用的最多的参数类型
这类选项属于实验性,不稳定
以-XX开头
用于开发和调试JVM
-XX:+<option> 表示启用option属性
-XX:-<option> 表示禁用option属性
说明:因为有的指令默认是开启的,使用-关闭
Boolean类型格式
子类型1:数值类型-XX:<option>=<number>]
子类型2:非数值类型-XX:<name>=<string>
非Boolean类型(k-v类型)
输出所有参数的名称或者默认值
默认不包括Diagnostic和Experimenta的参数
可以配合-XX:+UnIockDiagnosticVMOptions和-XX:UnIockExperimentalVMOptions使用
-XX:+PrintFlagsFinal
类型3:-XX参数选项
JVM参数选项类型
Eclipse
IDEA
运行jar包
通过Tomcat运行war包
jinfo -flag 设置
程序运行过程中
添加JVM参数选项
打印出所有XX选项在运行程序员时的生效值
可以运行程序前打印出用户手动设置或者JVM自动设置的XX选项
打印出所有XX的默认值
-XX:PrintFlagsInital
打印JVM的参数
-XX:+PrintVMOptions
打印设置的XX选项及值
-XX:ThreadStackSize
栈
初始化堆大小
年轻代堆大小
建议使用堆3/8官方
年轻代大小
-XX:NewSize
年轻代最大值
-XX:MaxNewSize
Eden与Survivor比例
自动选择各区大小比例
设置老年代与年轻代比例
设置让大于此阈值的直接分配到老年代,大小
-XX:PretenureSIzeThradshold
GC跃迁年代
-XX:MaxTenuringThreshold
让JVM在每次MinorGC之后打印出当前使用的Survivor中对象年龄分布
-XX:+PrintTenuringDistribution
表示MinorGC结束后Survivor区域中占用空间的期望比例
-XX:TargetSurvivorRatio
设置永久代初始值
-XX:PermSize
永久代最大值
-XX:MaxPermSIze
初始大小
-XX:MetaspaceSize
最大值
-XX:MaxMeatapaceSIze
压缩对象指针
-XX:UseCompressdOops
压缩类指针
-XX:UseCompressedClassPointers
设置K拉萨市Metapace大小
-XX:CompressedClassSpaceSize
指定DirectMemory容量
-XX:MaxDIrectMemorySize
堆,栈,方法区等内存大小的设置
表示在内存出现OOM的时候,把Heap转储到Dump到文件一遍后续分析
-XX:HeapDumpOnOutOfMemoryError
b表示FullGC之前生成Heap文件转储
-XX:+HeapDumpBeforeFulllGC
指定heap转储文件的存储路径
-XX:HeapDumpPath=<Path>
指定一个可行性程序或者脚本路径,当发送OOM时候,执行这个文件
-XX:OnOutOfMemoryError
OutofMemory相关的选项
查看默认垃圾收集器
单线程高效垃圾收集器
Seria回收器
指定为新生代并行收集器
-XX:+UseParNewGC
限制线程数,默认与CPU数量一致
-XX:ParallelGCThreads=N
ParNew回收器
开启Paralle并行垃圾回收器
-XX:UserParalleGC
并行垃圾回收器,在有限时间内提高吞吐量
指定老年代GC
并行线程数
开启自适应
Paralle回收器
补充参数
特别说明
手动开启CMS
设定内存阈值
-XX:CMSInittiatingOccupanyFraction
用于指定FullGC后对内存进行压缩
设置多少次FullGC后进行压缩操作
-XX:CMSFullGCsBeforeCOmpaction
并发的垃圾收集器
CMS回收器
MixedGC调优参数
G1回收器
怎么选择垃圾回收器
垃圾收集器的相关选项
记录GC
-verbose:gc
-XX:+PrintGC
记录GC与GC完堆空间内存大小
-XX:PrintGCDetails
GC的时间戳
-XX:+PrintGCTimeStams
日期
-XX:PrintGCDateStams
每次GC前后记录堆空间
-XX:+PrintHeapAtGC
日志输出到指定卫路径
-Xlogc:<file>
常用参数
监控类的加载
-XX:TraceClassLoading
打印GC时线程停顿时间
-XX:+PrintGCApplicationStoppedTime
垃圾收集之前打印出应用未中断的执行时间
-XX:PrintGCapplicationConcurrentTime
记录回收了多少中不同引用类型的引用
-XX:PrintReferenceGC
让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布
启用GC日志文件自动转储
-XX:+UseGCLogFileRotation
GC日志文件的循环数
-XX:NumberOfGClogFiles=1
控制GC日志文件的大小
-XX:GCLogFileSize=1M
其他参数
GC日志相关选项
常用JVM参数选项
通过Java代码获取JVM参数
JVM运行时参数
GC日志参数
GC日志格式
GC日志分析工具
分析GC日志
JVM
定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
鸭子模型-策略类图
抽象模式-策略类图
类图
由客户端决定具体使用何种策略(业务,算法)来实现。
建议
策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。
观察者模式
java.uitl.Observer
当两个对象之间松耦合,它们依旧可以交互,但是不太清楚彼此的细节。
观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
观察者模式定义了对象之间一对多的关系。
主题用一个共同的接口来更新观察者。
观察者和可观察者之间用松耦合方式结合,可观察者不知道观察者的细节,只知道观察者实现了观察者的接口。
使用此模式时,你可以从被观察者处推或拉数据。
有多个观察者时,不可以依赖特定的通知次序。
Java有多种观察者模式的实现,包括了通用的java.util.Observable。
要注意java.util.Observable实现上所带来的一些问题。
如果有必要的话,可以实现自己的Observable,这并不难,不要害怕。
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。
动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
装饰者类图
使用装饰者,可以在不修改底层代码的前提下,给其补充新的职责。
装饰者可以在所委托被装饰者的行为之前、之后,加上自己的行为,以达到特定的目的。
完全遵循开放-关闭原则
完美的利用组合的方式来处理问题
继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
在我们设计中,应该允许行为可以被扩展,而无须修改现有代码。
组合和委托可用于在运行时动态地加上新的行为。
除了继承,装饰者模式也可以让我们扩展行为。
装饰者模式意味着一群装饰者嘞,这些类用来包装具体的组件。
装饰者类反映出被装饰的组件类型。
装饰者可以再被装饰者的行为前面或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
你可以用无数个装饰者包装一个组件。
装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。
装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。
装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体的类。
抽象工厂模式
简单工厂
工厂方法
披萨店
抽象工厂
通过子类决定该创建的对象时什么,来达到将对象创建的过程封装的目的。
将创建对象与使用对象的操作解耦合
将使用与实现解耦
举例:披萨店-披萨披萨店为高层组件披萨为低层组件
高层组件不依赖于底层组件。
不想考虑披萨店,先考虑有哪些披萨。然后披萨店可以选择不同的披萨。
思维方式转变,不从上到下,而从下至上。
依赖倒置原则最贴切的实践
变量不可以持有具体类的引用
不要让类派生自具体类
不要覆盖基类中已实现的方法
所有的工厂都是用来封装对象的创建
简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体的类解耦。
工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。
抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。
所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。
工厂方法允许类将实例化延迟到子类进行。
抽象工厂创建相关的对象家族,而不需要依赖它们的具体的类。
依赖倒置原则,指导我们避免依赖具体类型,而要尽量依赖抽象。
工厂模式是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。
工厂模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
确保一个类只有一个实例,并提供一个全局访问点。
单例模式类图
线程池、缓存、对话框、处理偏好设置和注册表的对象、日志对象、驱动对象。
使用场景举例
判断创建
单线程
同步方法
类对象
双重检查加锁
创建单例代码流程
单件模式确保程序中一个类最多只有一个事例。
单件模式也提供访问这个事例的全局点。
在Java中实现单件模式需要私有构造器、一个静态方法和一个静态变量。
确定在性能和资源上的限制,然后小心地选择适当的方案来实现单件,已解决多线程的问题。
如果使用多个类加载器,可能会导致单件失效而产生多个实例。
单例模式确保一个类只有一个实例,并提供一个全局访问点。
将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
命令模式类图
封装调用
解耦
接受与执行这些请求的对象
允许发出请求的对象
创建命令对象
存储命令对象
调用命令对象的命令指令
流程
命令对象给接受者绑定特定的一组动作来封装请求。
空对象,兜底方案对象
NoCommand
线程池里的等待队列
队列请求
一般来说,有两个对象,命令对象,接受者对象。
命令模式将发出请求对象和执行请求的对象解耦。
在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接受者和一个或一组动作。
调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
调用者可以接受命令当做参数,甚至在运行时动态地进行。
命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
宏命令是命令的一种简单延伸,允许调用多个命令。宏方法也可以支持撤销。
实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接受者。
命令也可以用来实现日志和事务系统。
命令模式将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。
适配器类图
类适配器与对象适配器
客户通过目标接口调用适配器的方法对适配器发出请求。
适配器使用被适配者接口把请求转换成被适配者的一个或多个调用接口。
客户接收到调用的结果,但并未察觉这一切是适配器在起装换作用。
例如Collection适配了Enumeration枚举类
适配器
后面迭代器Iterator出来后,适配兼容Enum做了适配器EnumerationIterator
Java源码举例
对于java来说,不存在类适配器。
对象类加载器,组合
类适配器使用的是多重继承的方式,Java不存在类适配器模型
对象适配器使用的对象组合的方式
对象适配器与类适配器
从代码上来看,适配器与装饰者是类似的。
主要是处理接口适配。
主要是给类新增一个行为。
装饰者
意图上有差距。
装饰者与适配器
当需要使用一个现有的类而其接口并不符合你的需要时,就使用适配器。
适配器可以改变接口以符合客户的期望。
实现一个适配器可能需要一番功夫,也可能不费功夫,视目标接口的大小与复杂程度而定。
适配器有两种形式:对象适配器与类适配器。类适配器需要用到多重继承。
适配器将一个对象包装起来以改变其接口;装饰者将一个对象包装起来以增加新的行为和责任;而外观将一群对象“包装”起来以简化其接口。
适配器模式将一个类的接口,转换成客户期望的另外一个接口。适配器让原本接口不兼容的类可以合作无间。
提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
外观模式
这很容易理解,但是请务必记得模式的意图。这个定义清楚地告诉我们,外观的意图是要提供一个简单的接口,好让一个系统更易于使用。从这个模式的类图可以感受到这一点。
当需要简化并统一一个很大的接口或者一群复杂的接口时,使用外观。
外观将客户从一个复杂的子系统中解耦。
实现一个外观,需要将子系统组合进外观中,然后将工作委托给子系统执行。
你可以为一个子系统实现一个以上的外观。
外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以再不改变算法结构的情况下,重新定义算法中的某些步骤。
模板方法
Java数组里的sort方法,让元素实现Compable接口,实现compareTo方法。
定义流程
定义算法流程
继承实现
封装不同算法
组合实现
策略模式
模板方法与策略模式
\"模板方法\"定义了算法的步骤,把这些步骤的实现延迟到子类。
模板方法模式为我们提供了一种代码复用的重要技巧。
模板反方法的抽象类可以定义具体方法、抽象方法和钩子。
抽象方法由子类实现。
钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它。
为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块。
你讲在真实世界代码中看到模板方法模式的许多变体,不要期待他们全都是一眼就可以被你认出的。
策略模式和模板方法都封装算法,一个用组合,一个用继承。
工厂方法是模板方法的一种特殊版本。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以再不改变算法结构的情况下,重新定义算法中的某些步骤。
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
迭代器接口
两部分,封装迭代器装机接口。封装迭代器元素。
迭代器模式
实现遍历我的对象,但是又不可窥见我对象的存储方式。
管理良好的集合
主要两个方法,一个获取下一个元素,判断是否还有下个元素。
依赖于迭代器接口
至于迭代器对象如何实现客户端无需关注
客户端只需要知道它是迭代器,迭代器实现的方法
迭代器方法封装了遍历过程
迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露其内部的表示。
把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。
不提供next方法,不能外部游走元素
内部迭代器
外部迭代器
迭代器分类
迭代器允许访问聚合的元素,而不需要暴露它的内部结构。
迭代器将遍历聚合的工作封装进一个对象中。
当使用迭代器的时候,我们依赖聚合提供遍历。
迭代器提供了一个通用的接口,让我们遍历聚合的项时,就可以使用多台机制。
我们应该努力让一个只分配一个责任。
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
允许你将对象组合成树形结构来表现“整体、部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
组合模式
组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别对象。
使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。
但是叶子节点与组合节点的角色不同,所以有些方法可能并不适合某种节点
因此你对于不需要的方法最好是抛出运行时异常
因此可以给默认方法设置为抛出异常,子类未实现此方法去调用则报异常。
所有的组件都必须实现MenuComponent接口
客户可以将组合与叶子节点一视同仁。
但是与之带来的还有安全性的丢失
以单一责任设计原则换区透明性
兜底方案,默认迭代
使用者需要判断是否为null
返回null
hashNext()返回永远为false
返回一个空迭代器
当没试下迭代器时有两种处理方式
因此对于客户端来说。空迭代器的处理方式更加优雅。可以用对待普通迭代器的方式处理。
空迭代器
当你有数个对象的集合,它们彼此之间有“整体/部分”的关系,并且你想用一致的方式对待这些对象时,那就需要我。
组合模式允许客户对个别对象以及组合对象一视同仁。
组合结构内的任意对象称为组件,组件可以是组合,也可以是叶节点。
在实现组合模式时,有许多设计上的折衷。你要根据需要平衡透明性和安全性。
组合模式允许你将对象组合成树形结构来表现“整体、部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
状态模型
通过切换不同的算法来创建业务功能
由客户端自己选择不同的策略
继承之外的一种弹性替代方案
通过切换不同的状态来实现业务功能
由状态对象内部自行控制状态切换
不用放置许多条件判断的代替方案
状态模式
策略模式和状态模式的双胞胎,在出生时才分开。
当某些状态需要改变时,不会影响到其他状态。
定义一个State接口
实现不同状态类,对应机器不同行为
将if else 语句改为委托形式
对修改关闭,对扩展开放
设计步骤
局部化每个状态的行为,封装变化的原则
为啥使用赢家类而不是直接修改售出类来实现释放两个糖果的按钮呢?如果促销结束,需要再次修改售出类。调整中奖几率。更换中奖商品等。都需要修改它。
改变行为这个事情是状态内部方案中的控制的,由状态模式内部决定。
状态模式允许一个对象基于内部状态而拥有不同的行为。
和程序状态机(PSM)不同,状态模式用类代表状态。
Context会将行为委托给当前状态对象。
通过将每个状态封装进一个类,我们把以后需要做的任何改变局部化了。
状态模式和策略模式有相同的类图,但是它们的意图不同。
策略模式通常会用行为或算法来配置Context类。
状态模式允许Context随着状态改变而改变行为。
状态装换可以由State类或者Context类控制。
使用过状态模式通常会导致设计中类的数目大量增加。
状态类可以被多个Context实例共享。
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
为另一个对象提供一个替身或占位符以控制对这个对象的访问。
代理模式
你的客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的底层细节。
服务对象
服务辅助对象
客户辅助对象
客户对象
客户对象 doBigThing -> 客户辅助对象
客户辅助对象 远程调用-> 服务辅助对象
服务辅助对象 真实调用-> 服务对象
客户辅助对象又称为桩(stub)
服务辅助对象又称为骨架(skeleton)
远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub和是实际的服务都实现此接口。
步骤一:制作远程接口
为远程接口中定义的远程方法提供了真正的实现。
步骤二:制作远程的实现
步骤三:利用rmic产生的stub和skeleton
步骤四:启动RMI registry
步骤五:开始远程服务
远程服务制作步骤
远程代理
虚拟代理
案例
远程访问对象
控制访问创建开销大的资源
基于权限控制对资源的访问
处理问题
在日志打印中,控制日志打印内容的开与关
使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。
代理模式为另外一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
远程代理管理客户和远程对象之间的交互。
虚拟代理控制访问实例化开销大的对象。
保护代理基于调用者控制对象方法的访问。
代理模式有许多变体,例如:缓存代理、同步代理、防火墙代理和写入时复制代理。
代理在结构上类似于装饰者,但是目的不同。
装饰者模式为对象加上行为,而代理则是控制访问。
Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。
就和其他的包装者一样,代理会造成你的设计中类的数目整加。
代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
模式通常被一起使用,并被组合在同一个设计解决方案中。
复合模式在一个解决方案中结合两个或者多个模式,已解决一般或重复发生的问题。
MVC是复合模式,结合了观察模式、策略模式和组合模式。
模型使用观察者模式,以便观察者更新,同时保持两者之间解耦。
控制器是视图的策略,视图可以使用不同的控制器实现,得到不同的行为。
视图使用组合模式实现用户界面,用户界面通常组合了嵌套的组件,像面板、框架和按钮。
这些模式携手合作,把MVC模型的三层解耦,这样可以保存设计干净又有弹性。
适配器模式用来将新的模式适配成已有的视图和控制器。
Model2 是MVC在Web上的应用。
在Model2中,控制器实现成Servlet,而JSP/HTML实现视图。
复合模式
桥接模式
生成器模式
责任链模式
蝇量模式
中介者模式
备忘录模式
原型模式
访问者模式
封装变化
针对接口编程,不针对实现编程
为了交互对象之间的松耦合设计而努力
我们的目标是允许类容易扩展,在不修改现有代码的情况下,可以搭配新的行为。这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
类应该对扩展开放,对修改关闭
依赖倒置原则
不能让高层组件依赖底层组件。无论高低组件,都应当依赖于抽象。
案例,披萨店由披萨风味觉得它的类型因此,披萨是低层组件,披萨店是高层组件
由低层组件定义了其行为的类,就叫高层组件
高层组件与低层组件鉴别
要依赖抽象,不要依赖具体的类。
如何不要赢得太多的朋友和影响太多的对象。
该对象本身
被当做方法的参数而传递进来的对象
此方法所创建或实例化的任何对象
对象的任何组件
只调用以下范围的方法。
最少知识原则:只和你的密友谈话。
别调用我,我会调用你。
好莱坞原则
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。
这个原则告诉我们,尽量让每个类保持单一责任。
一个类应该只有一个引起变化的原因
高内聚的体现
单一责任原则
设计原则
良好的OO设计必须具备可复用、可扩充、可维护三个特性。
模式不是代码,而是针对设计问题的通用解决方案。你可把它们应用到特定的应用中。
大多数的模式都允许系统局部改变独立于其他部分。
通常我们会把变化部分抽取出来封装。
OO思想
如果你发现自己处于某个情境下,面对着所裕达到的目标被一群约束影响着的问题,然而,你能够应用某个设计,克服这些约束并达到该目标,将你领向某个解决方案。
情境就是应用某个模式的情况。这应该是会不断出现的情况。
问题就是你想在某个情境下达到的目标,但也可以是某情境下的约束。
解决方案就是你锁追求的:一个通用的设计,用来解决约束,达到目标。
模式是在某情境下,针对某问题的某种解决方案。
设计模式
目标
约束
解决方案有两个方向
描述
1.最主要:他有名称,使用者能知道它是做什么的。
2.可以不必在每次调用它们的时候都创建一个新对象。
3.可以返回原返回类型的任何子类型的对象。
4.所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
5.方法返回的对象所属的类,在编写包含该静态工厂方法的类是可以不存在。
1.类如果不包含公有的或者受保护的构造器,就不能被子类实例化。
2.Javadoc不支持,程序员很难发现它们。
Bigintege me = Bigintege .valueOf(Intege .MAX_VALUE);
1.用静态工厂方法代替构造器
简而言之,重构构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且可阅读性差。
使用getset模式,无法把类做成不可变。
客户端代码可阅读性强,可较容易的强化被构建的对象。
增加了创建构建器的额外开销,客户端构建代码比较冗长。
2.遇到多个构造器参数时要考虑使用构建器
单例会使得测试变得困难。
对象可序列化时,要注意重写readResolve反复噶,否则每次反序列化时,都会创建一个新对象。
反射可以破坏单例。
单元素的枚举类型经常成为实现单例的最佳方法。
3.用私有构造器或者枚举类型强化单例(Singleton)属性
当某些工具类不希望被实例化,因为该工具类只有静态方法。
该类的子类任然可以被实例化。会误导客户端使用者。
企图通过将类做成抽象类来强制该类不可被实例化时行不通的。
让这个类包含一个私有构造器,它就不能被实例化
客户端不会误用,去实例化该类
无法被子类化,无法使用构造器
public class UtiltityClass{ private UtiltityClass(){ throw new AssertionError(); }}
4.通过私有够朝气强化不可实例化的能力
静态工具类和单例类不适合于需要引用底层资源的类。
当创建一个新的实例时,就将该资源传到构造器中。(简单的依赖注入)
资源工厂传给构造器
极大地提升了类的灵活性,可重用性和可测试性。
可能会使得大型项目凌乱不堪,但是这种凌乱可以用依赖注入框架解决,例如Spring
public class SpellChecker{ private final Lexicon dictionary; public SpellChecker(Lexicon dictitonary){ this.dictionary = dictionnary; }}
5.优先考虑依赖注入来引用资源
自动装箱使得基本类型和装箱类型之间的差别变得模糊起来,但是并没有完全消除。
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
减少创建对象的开销,减少额外对象的内存占用。
在某些场景下,例如拷贝对象,则避免使用重复对象,会有bug和安全漏洞。
6.避免创建不必要的对象
清空对象引用应该是一种例外,而不是一种规范行为。
只要类是自己管理内存,程序员就应该警惕内存泄漏问题。
内存泄漏的另外一个常见来源是缓存。
内存泄漏的第三个常见来源是监听器和其他回调。
7.消除过期的对象引用
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
清除方法(cleaner)没有终结反方法那么危险,但是仍然是不可预测、运行缓慢,一般情况下也是不必要的。
由于终结方法与清除方法是否执行是不可预知的,所以无法保证其执行时间。
永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
其二者非常消耗性能。
它可以阻止对象被回收,可以破坏原有的结构,例如私有构造器创建对象。
所以为了防止final类受到终结方法的攻击,要编写一个空的final的finalize方法。
终结方法的安全漏洞,打开了类的大门。
特殊情况,可以作为兜底操作,例如兜底的close,过很久释放资源和不释放资源还是有一定影响的。
总结:除非作为安全网或者终止非关键的本地资源。否则不要使用它!并且java9前尽可能不要使用终结反复噶。
8.避免使用终结方法和清除方法
资源需要实现 AutoCloseable ,提供单个返回viod的close接口
代码简洁、清晰,产生的异常更加有价值。更加容易编写正确代码。
static String firstLineOfFile(String path) throws IOException{ try(BufferedReader br = new BufferedReader( new FileReader(path))){ return br.readLine(); }}
9.try-with--resource优先于try-finally
Effective Java
租客,影片,订单
影片计费,影片分类扩展
构建可靠的测试环境
重构的第一步
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验的能力。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可以发现它。
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
运用多态取代条件语句
知识点
第一章:入门实例
重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为对的前提下,提高其可理解性,降低其修改成本。
重构:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
何为重构
改进软件设计
更加易于理解
帮助找到BUG
提供编程速度
为何重构
事不过三,三则重构
三次法则
添加功能使重构
修改错误时重构
复审代码时重构
何时重构
何时不该重构
不要过早发布接口。请修改你的代码所有权策略,使重构更顺畅。
第二章:重构原则
如果尿布臭了,就换掉它。
重构这件事情,比较主观,依赖于coder的经验和嗅觉。
设法将它们合二为一,程序变得更好。
类出现相同的表达式
函数模式
子类拥有功能一致的算法
模版方法
算法中部分流程一致
1.Duplicated Code (重复代码)
拥有短函数的对象会活得比较好、比较长。
面向对象编程,短函数的解释能力、共享能力、选择能力。极大提高了代码后续的可维护性。
需要注意,短函数,需要一个好名字,让开发者通过函数名就可以直观认识到业务内容。
通常条件表达式、循环处理结构,应该被提炼到新函数中去。
2.Long Method (过长的函数)
类业务太多的话,往往其内部拥有太多实例。往往此时也会出现重复代码。
需要将类拆分
3.Large Class (过大的类)
4.Long Parameter List (过长参数列)
一次引入新模块,导致需要多处修改
5.Divergent Change (发散式变化)
6.坏名字
7.全局数据(Global Data)
8.可变数据(Mutable Data)
每遇到变化,你都要在不同类做许多小改动。
利用内联函数或者内联类
避免产生大类,模块划分可以更加清晰
9.散弹式修改(Shotgun Surgery)
所谓模块化,就是将代码分区,最大化内部交互,最小化跨区交互
如果模块数据过度依赖其他模块函数处理。可能说明模块拆分的不合理。
将变化的行为和不变的行为隔离,但是会有多一层间接性的代价
策略模式和访问模式
10.依恋情结(Feature Envy)
相同属性,字段经常一起出现是,应当将他们归类。创建一个新类管理他们。
11数据泥团(Data Clumps)
含糊不清的类型,例如手机号码,使用的是String字符串类。
12.基本类型偏执(Primitive Obsession)
重复的switch会让我们修改时需要多重考虑才能新增case类型数据。推荐使用多态的形式来解决这个问题。
13.重复的switch(Repeated Switches)
循环一直是程序设计的核心要素。
java里可以使用管道操作来减少使用循环操作。
14.循环语句(Loops)
对于过度抽象出来的类,函数。应该慎重,如果不能保证其后续会有扩大情况,应该使用内联函数或者内联类去实现。
15.冗赘的元素(Lazy Element)
过早的提前准备,提前的钩子方法,特殊情况处理等。
依旧可以内联类或者内联函数处理。
16.夸夸其谈通用性(Speculative Generality)
当类中某些字段只有特殊情况下才会被使用,请搬离它们,给他们从新找个家。
17.临时字段(Temporary Field)
对象引用过多的业务流程
应该使用隐藏委托关系
更好的选择是,尝试提炼函数,将过多的函数提炼到独立函数中。
但是函数链不一定都是坏家伙,需要冷静思考其是不是已经将责任依赖划分的足够合理了。
18.过长的消息链(Message Chains)
对象特征之一封装
如果某个类接口一半函数都是委托给其他类,过度使用委托。
这时候要考虑是否能移除中间人,直接操作其他对象了。
可以将他们用内联函数代替
委托取代超类或者委托取代子类
19.中间人(Middle Man)
软件提倡高内聚低耦合,实际上交互数据的耦合是不可避免的。只能减少。
可以搬移函数或者搬移字段减少它们私下交流。
如果它们有共同兴趣,可以为他们单独抽离到一个新模块,提供管理良好的地方去。或者使用应酬委托关系,把另外一个模块编程两者中介。
20.内幕交易(Insider Trading)
使用类的好处就是后面方便替换。
21.异曲同工的类(Alternative Classes with Different Interfaces)
22.纯数据类(Data Class)
23.被拒绝的遗赠(Refused Bequest)
当你感觉要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
24.过多的注释(Comments)
坏味道
第三章:代码的坏味道
第四章:构建测试体系
概要
动机
做法
范例
重构的记录格式
第五章:重构列表
反向操作:内联函数
做法:新函数要说明做什么,而不是怎么做
提炼函数(Extract Method)
反向操作:提炼函数
动机:如果函数内部实现已经非常直观,无需抽离成函数了
检查函数,确保其不具备多态性
找出所有调用点
将所有调用点替换为函数本体
测试!!!
内联函数(Inline Method)
反向操作:内联变量
动机:表达式阅读性太差!
确保提炼的表达式没有副作用
确定作用域
新变量取代原函数表达式
提炼变量(Extract Variable)
反向操作:提炼变量
动机:某些情况下,表达足够精简,变量可能会妨碍代码重构
变量的表达式没有副作用
找到所有声明使用的地方
替换
删除变量声明
内联变量(Inline Variable)
函数名很重要
参数列表需要细心选择,减少不必要的耦合
没有简单可循的规则来定义参数
所以最好的名字和参数就是结合业务,及时调整!!!
Change是最好的!
动机:函数是我们将程序拆分成小块的主要方式
对于函数的修改,名称以及参数列表的改动其影响范围都是比较大的,遵循小步慢跑的原则渐进式地逐步修改
移除参数,确保函数体已经不再使用
找出所有调用点,调整为新函数声明
简单做法:
可以将新函数在进行拆分
提炼函数本身
新增参数,参考简单做法
对旧函数改用内联函数
调整代码
迁移式做法:
对于已经发布的API,需要保留旧接口,给客户端时间调整。
改变函数声明(Change Function Declaration)
动机:重构的作用就是调整程序中的元素。降低调整组织数据的难度提高对大作用域(多个函数内使用)的数据的管控能力动态添加字段,修改校验规则等。封装数据很重要,不过,不可变数据更重要
创建封装函数,在其中访问和更新变量值
执行静态检查,java可以选择重新编译代码
逐步知道使用该变量的代码,改用何时的封装函数替换后及时测试!!!
限制变量的可见性
如果变量的值是一个记录,考虑使用封装记录
做法:
可采用拷贝复制的方式
有时还需要控制对参数的修改权限。设定为不可修改类型。
复制对于性能的影响通常是可以忽略不计
数据封装很有价值,但往往并不简单。到底应该封装什么,以及如何封装,取决于数据被使用的方式,以及我想要修改数据的方式。不过,一言以蔽之,数据被使用得越广,就越是值得花精力给它一个体面的封装。
封装变量(Encapsulate Variable)
名言,如果你发现有更加好的名称,别吝啬,及时调整。
使用范围越广,名字的好坏就越重要。
动机:好的命名是整洁编程的核心
机制:如果变量被广泛使用,考虑运用封装变量将其封装起来。找出素有使用该变量的代码,逐一修改。测试!!!
变量改名(Rename Variable)
动机:当一群数据泥团经常一起出现时,将他们柔到一个类统一管理很重要
目前没有一个合适的数据结构那就重新创建一个
通常使用类来承载
使用改变函数声明给原来的函数新增一个参数类型是新建的数据结构
测试
调整所有调用方代码,传入新的数据结构的适当实例
逐步替换原函数中每项参数,然后删除原参数。
引入参数对象(Introduce Parameter Object)
动机:类,在大多数现代编程语音中都是基本的构造。如果发现一组数据总是离不开某个函数,那么是时候为它们创建一个家了。
运用封装记录对多个函数共用的数据记录加以封装。
对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。
函数组合成类(Combine Functions into Class)
动机:在软件中,经常需要把数据“喂”给程序,让它再计算出各种派生信息。
单纯的提炼函数也可以处理派生类中重复代码但是后续要使用和管理就不那么方便,我需要找到相关逻辑需要花费的时间就比较长。
创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,将其使用这个新字段。
函数组合成变换(Combine Functions into Transform)
动机:看到一段代码同时处理两件不同的事情,就会想着拆分成两个模块来处理。
将第二阶段的代码提炼成独立的函数
引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
逐一检查提炼出的第二阶段函数的每个参数。如果第一阶段有使用,就将其移入到中转数据结构中。
对第一阶段代码运用提炼函数,将提炼函数返回中转数据结构
拆分阶段(Split Phase)
第六章:第一组重构
特例是不可变的数据结构。
通常建议使用类去聚合这些数据结构。给他一个家
简单数据结构,会给后续维护者带来维护困难。
对持有记录的变量使用封装变量,将其封装到一个函数中。
创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令其使用这个访问函数。
测试。
新建一个函数,让它返回该类的对象,而非那条原始的记录。
对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问函数还不存在,那创建一个。每次更改之后运行测试。
如果该记录比较复杂,例如是一个嵌套结构,那么先重点关注客户端对数据的更新操作,对于读操作可以考虑返回一个数据副本或者只读的数据代理。
移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除。
如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录或者封装集合手法。
更加关注更新操作。
实践经验
1.封装记录(Encapsuate Record)
封装程序中所有可变的数据。
一般来说,封装函数需要包括:修改、添加、删除、建议返回的函数是拷贝体,而不是本体。
最常见做法,提供一个取值函数,返回集合的副本。
如果集合的引用尚未被封装起来,先用封装变量封装它。
在类上添加用于“添加集合元素”和“移除集合元素”的函数。
如果存在对该集合的设值函数,尽可能先用移除设值函数移除它。如果不能移除该设值函数,至少让它返回集合的一份副本。
执行静态检查。
找到集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加、删除元素的函数。测试。
修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本。
2.封装集合(Encapsuate Collection)
需要对数据进行复杂处理的基本类型
如果变量尚未被封装起来,先使用封装变量封装它。
为这个数据值创建一个简单的类。类的结构函数应该保存这个数据值,并为它提供一个取值函数。
修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明。
修改取值函数,令其调用新类的取值函数,并返回结果。
考虑对第一步得到的访问函数使用函数改名,以便更好反映其用途。
考虑应用将引用对象改为值对象或将值对象改为引用对象,明确指出新对象的角色是值对象还是引用对象。
3.以对象取代基本类型(Replace Primitive with Object)
代码重复,提高可读性,避免函数内过多局部变量
适用于:计算结果后续不会有改变的变量。
检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值。
如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它。
将为变量赋值的代码提炼成函数。
如果变量和函数不能使用同样名字,那么先为函数取个临时名字。|确保待待提炼函数没有副作用。若有,先应用将查询函数和修改函数分离手法隔离副作用。
内联变量手法移除临时变量。
4.已查询取代临时变量(Replace Temp with Query)
如果一个类有大量函数和数据,那么需要仔细查看,观察是否能将不同职责进行拆分成小类。
决定如何分解类所负的责任。
创建一个新的类,用以表现从旧类中分离出来的责任。
如果旧类剩下的责任与旧类的名称不符,为旧类改名。
构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系。
对于你想搬移的每个字段,运用搬移字段搬移之。每次更改后运行测试。
使用搬移函数将必要函数搬移到新类。先搬移较低层函数。每次更改后进行测试。
检查两个类的结构,去掉不再需要的函数,必要时为函数重新取个合适新环境名字。
决定是否公开新的类。如果确实需要,考虑对新类应用引用对象改为值对象使其成为一个值对象。
5.提炼类(Extract Class)
一个类不再承担足够责任,不再有单独存在的理由,可以萎缩它,将它移到另外一个类中去。
对于待内联类中所有public 函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类。
修改源类public方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止。
删除源类,为它举行一个简单的“丧礼”
6.内联类(Inline Class)
如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。我可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即使将来委托关系发生变化,变化也只会影响服务对象,而不会直接波及所有客户端。
对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关访问函数。
7.隐藏委托关系(Hide Delegate)
随着受托类的特性(功能)越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户直接调用受托类。
为受托对象创建一个取值函数。
对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试
替换完委托方法的所有调用点后,你就可以删掉这个委托方法了。
这能通过可自动化的重构手法来完成,你可以先对受托字段使用封装变量(132),再应用内联函数(115)内联所有使用它的函数。
8.移除中间人(Remove Midde Man)
使用这项重构手法之前,我得确定自己已经尽可能分解了原先的函数。替换一个巨大且复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,我才能很有把握地进行算法替换工作。
整理一下待替换的算法,保证它已经被抽取到一个独立的函数中。
先只为这个函数准备测试,以便固定它的行为。
准备好另一个(替换用)算法。
运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准。
9.替换算法(Substitute Algorithm)
第七章:封装
模块化是优秀软件设计的核心。
任何函数都需要具备上下文环境才能存活。
如果函数与上下文已经不符合、或者经常和其他模块联动,那么这个函数可能需要有个新的家。
检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移。
如果发现有些被调用的函数也需要搬移,我通常会先搬移它们。这样可以保证移动一组函数时,总是从依赖最少的那个函数入手。
如果该函数拥有一些子函数,并且它是这些子函数的唯一调用者,那么你可以先将子函数内联进来,一并搬移到新家后再重新提炼出子函数。
检查待搬移函数是否具备多态性。
在面向对象的语言里,还需要考虑该函数是否覆写了超类的函数,或者为子类所覆写。
将函数复制一份到目标上下文中。调整函数,使它能适应新家。
如果函数里用到了源上下文(source context)中的元素,我就得将这些元素一并传递过去,要么通过函数参数,要么是将当前上下文的引用传递到新的上下文那边去。搬移函数通常意味着,我还得给它起个新名字,使它更符合新的上下文。
设法从源上下文中正确引用目标函数。
修改源函数,使之成为一个纯委托函数。
考虑对源函数使用内联函数(115)
也可以不做内联,让源函数一直做委托调用。但如果调用方直接调用目标函数也不费太多周折,那么最好还是把中间人移除掉。
1.搬移函数(Move Function)
编程活动中你需要编写许多代码,为系统实现特定的行为,但往往数据结构才是一个健壮程序的根基。
代码凌乱,势必难以理解;不仅如此,坏的数据结构本身也会掩藏程序的真实意图。
2.搬移字段(Move Field)
第八章:搬移特性
重构:改善既有代码的设计
本地客户端协议
远程客户端协议
客户端通信管理器
查询解析和授权
查询重写
查询优化
执行查询计划
数据定义与表结构
查询管理器
访问方法
缓存区管理
锁管理
日志管理
存储管理器
入场控制
调度与排期
进程管理器
目录管理
内存管理
管理、监控、工具
备份与恢复
批量工具
共享组件和实用工具
结构
为调用者(客户端或者中间件)建立连接并记录其连接地址,对客户端的 sql 语句做出回应,并在适当的时候返回数据以及控制信息。
一个DBMS工作者对应一个进程
一个DBMS工作者对应一个线程
进程池、线程池方式
目前主流的三种模型
包含了一个正在执行的程序活动单元(线程)以及专属的地址空间。
一个操作系统进程
是操作系统程序执行单元,他没有私有的地址空间和上下文。
一个操作系统线程
是一个应用层次上的结构,它支持单系统进程中的多线程。
只支持异步的I/O操作。
不调用会导致中断的系统操作。
两个解决方式
应用层实现。任何中断操作会导致整体中断。
轻量级线程包
JDBC
ODBC
是应用程序实现与数据库系统通信API的软件组件。
属于是大家遵从一个大的方向,但是又各家各有特色。
DBMS客户端
一般是与DBMS客户端一一对应的。
是指DBMS中为客户端工作的线程。
DBMS工作者
一些定义
系统内核支持线程,且进程支持多线程
系统线程支持
仅有一个CPU的机器。(目前不现实)
单处理机
理解前两个假设
图解1
DBMS工作者直接映射到系统进程,实现简单。
由系统调度管理控制运行时间、提供保护措施来派出标准错误、较好支持锁机制、缓冲池等。
对于并发连接来说,这种模式不太高效。
进程切换需要切换安全的上下文环境、存储空间变量、文件和网络句柄列表以及其他一些进程上下文
IBM DB2、PostgreSQL 和 Oracle
支持的常见DB
每个DBMS工作者拥有一个进程
拥有一个调度线程负责监听客户端的连接
操作系统不提供溢出、指针保护,调试困难移植性差
问题:
很好地扩展到高并发系统中
优点:
IBM DB2、微软 SQL Server、MySql、Informix 和 Sybase。
支持的DB
每个DBMS工作者拥有一个线程
对于每个DBMS工作者拥有一个进程缺点,进行的变体
池化思维减小创建进程与回收进程的开销
进程池
三种模型
读写共享的数据。
线程模型里,缓冲池主要是堆数据存在
进程的共享空间。
存查询结果,将查询结果落库
数据库I/O中断请求:缓冲池
数据库请求
周期性的按FIFO顺序存储日志内容到磁盘
对于线程模型,任然是堆数据结构
进程模型则是由独立的进程负责管理日志。
提交事务前需要等待日志刷新
日志 I/O 请求:日志尾部
日志请求
磁盘I/O缓冲区
为支持预提取功能,DBMS 工作者使用客户端通信套接字作为元组队列
客户端通信缓冲区
锁表由所有的 DBMS 工作者共享,由锁管理器实现数据库锁机制。
锁表
缓冲区
共享数据和进程空间
单处理机和轻量级线程
这个架构为我们提供了快速的任务切换和易移植性,它的代价是需要在 DBMS 中重复实现许多操作系统逻辑
DBMS线程
支持进程池
Oracle默认方式
PostgreSQL
每个 DBMS 工作者拥有一个进程:
默认使用
IBM DB2
MySQL
每个 DBMS 工作者拥有一个线程:
Oracle
DBMS 工作者共用进程池
微软 SQL Server
DBMS 工作者共用线程池
进程/线程池:
标准实践
当资源环境(CPU、内存)负载高时、需要由准入控制器,合理调控系统性能衰退,在负载低时合理增加。
1:通过进程来确保客户端链接数在临界值以下。
1:所需磁盘IO
2:操作、元组数据的CPU负载
3:内存使用情况,包括连接、排序、哈希等。
由优化器查询计划决定
2:关系查询处理器上实现。
因此,许多DBMS 把内存使用情况和活跃的 DBMS 工作者的数量作为主要的准入控制标准。
准入控制
进程模型
共享内存机器的进程模型很自然地遵循单处理机的方式。
其主要思路是,多个CPU共同使用同块内存区域。
三种进程模式在这种架构下都能很好的兼容
一个无共享的并行系统是由多个独立计算机的集群组成的,这些计算机可以高速地通过网络进行互连通信,或者逐渐频繁地在商业网络组件上通信。对于给定的一个系统,无法直接访问另一个系统的内存和硬盘。
无共享系统并没有提供抽象的硬件共享,协调不同机器的任务完全留给了 DBMS。
无共享
共享磁盘
非均衡内存访问
线程和多处理器
标准的操作规程
讨论和附加材料
并行架构:进程与内存协调
关系查询处理器
存储管理
事务:并发控制和恢复
共享组件
Architecture of a Database System数据库系统架构
开发
阅读理解
数据库概念1
核心组件
工具
数据查询流程
客户端管理器->查询管理器->数据管理器->客户端管理器
客户端管理器
查询解析器
查询重写器
统计
查询优化器
查询执行器
缓存管理器
事务管理器
并发控制
锁管理器
日志管理器
数据管理器
全局
要么全成功,要么全失败eg: A转50给B(1)A没扣,B没得(2)A扣了,B得了不会出现(1)A没扣,B得了(2)A扣了,B没得
原子性(Atomicity)
合法数据约束数据不会凭空产生,也不会凭空消失。
一致性(Consistency)
事务间同时执行,不会相互干扰eg: A转50给B,C转100给B二者不会干扰,例如A与C均扣钱了,但是B只加了50或100
隔离性(Isolation)
事务提交,就一定会保存到数据库。
持久性(Durability)
ACID
数据库
笔记
0 条评论
回复 删除
下一页