1.Oracle JDK是基于OpenJDK源代码构建的,因此Oracle JDK和OpenJDK之间没有重大的技术差异。
2.Oracle JDK将更多地关注稳定性,它重视更多的企业级用户,而OpenJDK经常发布以支持其他性能,这可能会导致不稳定。
3.Oracle JDK具有良好的GC选项和更好的渲染器,而OpenJDK具有更少的GC选项,并且由于其包含自己的渲染器的分布,因此具有较慢的图形渲染器选项。
4.Oracle JDK以前的1.0版以前的版本是由Sun开发的,后来被Oracle收购并为其他版本维护,而OpenJDK最初只基于Java SDK或JDK版本7。
5.Oracle JDK发布时大多数功能都是开源的,其中一些功能免于开源,并且根据Sun的许可授权,而OpenJDK发布了所有功能,如开源和免费。
6.Oracle JDK完全由Oracle公司开发,而Open JDK项目由IBM,Apple,SAP AG,Redhat等顶级公司加入和合作。
添加图片注释,不超过 140 字(可选)结论
Oracle JDK可用于开发Java Web应用程序,独立应用程序以及许多其他图形用户界面以及其他开发工具。Oracle JDK执行的所有操作或任务也可以由OpenJDK执行,但只有Oracle与OpenJDK之间的区别在于Open JDK在现有Oracle JDK之上的许可和其他工具集成和实现。使用OpenJDK的优点是可以根据应用程序的要求修改性能,可伸缩性和实现,以根据需要调整Java虚拟机。
OpenJDK的优势更多,Oracle JDK的使用在Oracle JDK实现中使用的标准方面也有一些好处,这将确保应用程序稳定和良好维护。
1.openjdk提供了源代码,adoptopenjdk提供了该源代码的构建。
2.OpenJDK是一个open-source项目,提供了Java platform实现的源代码(而非内部版本),
AdoptOpenJDK是由Java社区的一些著名成员创建的组织,旨在为Java技术的用户免费提供二进制版本和安装程序。
Alibaba DrAGonwell 是 OpenJDK 的下游,每个 Alibaba DrAGonwell 发行版都会同步上游最新更新。同时,阿里巴巴也会积极将 AJDK 上的技术积累贡献到 OpenJDK
Base64加密算法(编码方式)
MD5加密(消息摘要算法,验证信息完整性)
对称加密算法
非对称加密算法
数字签名算法
数字证书
按加密算法是否需要key被分为两类:
不基于key的有: Base64算法、MD5
基于key的有: 对称加密算法、非对称加密算法、数字签名算法、数字证书、HMAC、RC4(对称加密)
按加密算法是否可逆被分为两类:
单向加密算法(不可解密):MD5、SHA、HMAC
非单向加密算法(可解密):BASE64、对称加密算法、非对称加密算法、数字签名算法、数字证书
Base64应用场景:图片转码(应用于邮件,img标签,http加密)
MD5应用场景:密码加密、imei加密、文件校验
非对称加密:电商订单付款、银行相关业务
BASE64的加密解密是双向的,可以求反解。MD5、SHA以及HMAC是单向加密,任何数据加密后只会产生唯一的一个加密串,通常用来校验数据在传输过程中是否被修改。其中HMAC算法有一个密钥,增强了数据传输过程中的安全性,强化了算法外的不可控因素。
单向加密的用途主要是为了校验数据在传输过程中是否被修改
object类中方法说明如下:
1 registerNatives() //私有方法
2 getClass() //返回此 Object 的运行类。
3 hashCode() //用于获取对象的哈希值。
4 equals(Object obj) //用于确认两个对象是否“相同”。
5 clone() //创建并返回此对象的一个副本。
6 toString() //返回该对象的字符串表示。
7 notify() //唤醒在此对象监视器上等待的单个线程。 8 notifyAll() //唤醒在此对象监视器上等待的所有线程。
9 wait() //用于让当前线程失去操作权限,当前线程进入等待序列
==:基本类型比较值是否相同 引用类型比较内存地址是否相同 equals:引用类型:object类比较的是地址值但是其他类大都重写了equals方法比较成员变量的值是否相同
hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致,
因为重写的equal()里一般比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠
基本类型共有八种,它们分别都有相对应的包装类
四种整数类型(byte、short、int、long)
两种浮点数类型(float、double)
一种字符类型(char)
一种布尔类型(boolean)
记忆:8位:Byte(字节型) 16位:short(短整型)、char(字符型) 32位:int(整型)、float(单精度型/浮点型) 64位:long(长整型)、double(双精度型) 最后一个:boolean(布尔类型
int是java的8种内置的原始数据类型。Java为每个原始类型都提供了一个封装类,Integer就是int的封装类。
int变量的默认值为0,Integer变量的默认值为null,这一点说明Integer可以区分出未赋值和值为0的区别
1、String是字符串常量,StringBuffer和StringBuilder都是字符串变量。后两者的字符内容可变,而前者创建后内容不可变。
2、String不可变是因为在JDK中String类被声明为一个final类。
3、StringBuffer是线程安全的,而StringBuilder是非线程安全的。
补充说明:线程安全会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高。如果对系统中的线程是否安全很掌握,可用StringBuffer,在线程不安全处加上关键字Synchronize。
1、Vector、ArrayList都是以类似数组的形式存储在内存中,LinkedList则以链表的形式进行存储。
2、List中的元素有序、允许有重复的元素,Set中的元素无序、不允许有重复元素。
3、Vector线程同步,ArrayList、LinkedList线程不同步。
4、LinkedList适合指定位置插入、删除操作,不适合查找;ArrayList、Vector适合查找,不适合指定位置的插入、删除操作。
5、ArrayList在元素填满容器时会自动扩充容器大小的约50%,而Vector则是100%,因此ArrayList更节省空间。
因为vector是线程安全的,所以效率低,这容易理解,类似StringBuffer
Vector空间满了之后,扩容是一倍,而ArrayList仅仅是一半
Vector分配内存的时候需要连续的存储空间,如果数据太多,容易分配内存失败
只能在尾部进行插入和删除操作,效率低
反射机制的定义:是在运行状态中,对于任意的一个类,都能够知道这个类的所有属性和方法,对任意一个对象都能够通过反射机制调用一个类的任意方法,这种动态获取类信息及动态调用类对象方法的功能称为java的反射机制。
反射的作用:
1、动态地创建类的实例,将类绑定到现有的对象中,或从现有的对象中获取类型。
2、应用程序需要在运行时从某个特定的程序集中载入一个特定的类
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
继承的特点:
1、子类拥有父类非private的属性和方法
2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展
3、子类可以用自己的方式实现父类的方法(方法重写)
4、构造函数不能被继承
5、继承使用extends关键字实现
多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
多态的作用:消除类型之间的耦合关系。
现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。
多态存在的三个必要条件
1要有继承或实现,即父类引用变量指向了子类的对象或父类引用接受自己的子类对象;
2要有重写;
3父类引用指向子类对象。
多态弊端: 提高扩展性,但是只能使用父类引用指向父类成员。
注意:
在多态的情况下,字符类存在同名的成员(成员变量和成员函数)时,访问的是父类的成员,只有是同名的非静态成员函数时,才访问子类的成员函数;
多态用于形参类型时,可以接受多个类型的数据;
多态用于返回类型时,可以返回多个类型的数据,使用了多态的方法,定义的变量类型要与返回的类型一致。
幂等的意思是重复操作,接口的幂等性也就是接口被重复调用了,在前端不进行限制的情况下,同一个接口可能重复调用多次,为了避免类似重复下单的问题,可以通过以下几种方式来解决幂等性问题:
A.全局唯一ID,根据业务操作和内容生成全局唯一的ID,然后在执行操作前先判断是否已经存在该ID,如果不存在则将该ID进行持久化(存在数据库或者redis中),如果已经存在则证明该接口已经被调用过了.比如下单时可以生产一个流水号来作为该订单的唯一标识.
B.可以使用select+insert来进行判断,因为一般订单的ID都是唯一索引,在高并发场景下不推荐.
C.可以使用乐观锁解决,在表中可以添加一个version字段.
D.token机制,将token放在redis中.
JWT组成:
header+payload+signature 三者通过点【.】连接起来就是JWT了
a)header头部:包含token类型和采用的加密算法
{"alg":"HS256", "typ":"JWT"}
b)payload负载:存放信息的地方,可以把用户ID等信息存放在payload中。
常用的信息有:iss(签发者), exp(过期时间), sub(面向用户), aud(接收方), iat(签发时间)等。
c)signature签名:使用编码后(base64编码)的header和payload再加上我们提供的一个公钥,然后使用header中指定的签名算法进行签名。作用是保证JWT没有被窜改过。
JWT只适合向web端传递一些非敏感信息,因为base64编码是可逆的,很容易被破解
JWT的使用过程
1)前端通过表单将用户名和密码发送到后端接口。
2)后端核对用户名和密码后,将用户的id及其他非敏感信息作为JWT Payload,将其与头部分别进行base64编码后签名,生成JWT
具体源代码见rest_framework_jwt下面的serializer中,有兴趣研究的可以去跟踪看看
添加图片注释,不超过 140 字(可选)3)后端将JWT字符串作为登录成功的结果返回给前端,前端可以将JWT存到localStorage或者sessionStorage中,退出登录时,前端删除保存的JWT信息即可。
4)前端每次在请求时,将JWT放到header中的Authorization
5)后端验证JWT的有效性
6)验证通过后,进行其他逻辑操作
悲观锁:悲观的认为所有的线程都会导致数据错误,每一个线程都需要排队等待。优点:数据一致性,缺点:效率低 Synchronized、Lock
二乐观锁:AtomicInteger、CAS算法
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
轻量级锁 - 多个线程竞争的时候,锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
重量级锁 自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:修饰实例方法:、修饰静态方法、修饰代码块。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步代码块,锁是synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁
能够显示地获取和释放锁,锁的运用更灵活
可以方便地实现公平锁
能够响应中断,非阻塞获取锁
1)类型不同:Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)异常处理方式不同:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)等待方式不同:Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)判断方式不同:通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到。
5)效率不同:Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率
添加图片注释,不超过 140 字(可选)cas是另一个无锁解决方案,更准确的是采用乐观锁技术,实现线程安全的问题。cas有三个操作数----内存对象(V)、预期原值(A)、新值(B)
预期值和原值比较
CAS缺点
自循环时间长,开销大 解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来
只能保证一个共享变量的原子操作
解决方法:CAS操作是针对一个变量的,如果对多个变量操作
可以加锁来解决。
封装成对象类解决。
3、ABA问题
解决方法:CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference
减少线程持有锁的时间 可以把同步方法修改为同步代码块,可以减少线程持有锁的时间
减少锁粒度:锁分段技术、 将一个对象的数据分割成多个部分,并为每个部分分配一把锁
操作互不影响:锁分离技术
锁分离的应用就是 读写锁ReentranceReadWriteLock,读读互不影响,可以同时有多个线程读取数据
还有锁粗化和锁消除。
锁粗化就是把执行时间非常短的方法直接加入到同步方法中,减少重新线程重新因为竞争同步锁带来的资源损耗。
锁消除就是消除掉 JDK 代码中自带的锁,比如消除掉StringBuffer中的同步锁
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象
JDK1.7的时候是使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n)
所以jdk8就引入了红黑树这个概念,如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树,时间复杂度就只有 O(log n)
总结:hashmap的初始容量是16,在第一次put的时候进行的扩容初始化,链表长度大于8时才会转换为红黑树,红黑树长度小于6时会退化为链表,扩容因子是0.75,初始化扩容阈值为大于12。
容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。
主要是两方面,
第一如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。
第二就是如果多个线程同时检测到元素个数超过数组大小*loadFactor,这样就会发生多个线程同时对Node数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失
JDK1.7中,ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
JDK1.8放弃了锁分段的做法,采用CAS和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果
好处:减少内存开销
1、HashTable线程同步,HashMap非线程同步。
2、HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。
3、HashTable使用Enumeration,HashMap使用Iterator。
4、HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。
List、Set和Map的常见集合类型总结
添加图片注释,不超过 140 字(可选)有序、可以重复。
ArrayList 底层是数组,查询快,增删慢,线程不安全,效率高,适合get和set方法;
Vector 底层是数组,查询快,增删慢,线程安全,效率低;
LinkedList 底层是双向循环链表,查询慢,增删快,线程不安全,效率高,适合增加和删除方法。
无序、不可重复。
HashSet:(无序,唯一)底层是哈希表,通过 hashCode()和 equals()保证元素唯一;
LinkedHashSet:(有序,唯一)底层是链表和哈希表 ,链表保证元素的有序, 哈希表证元素的唯一性;
TreeSet:(有序,唯一)底层是红黑树,排序通过自然排序和比较强排序。
ArrayList是一个数组结构的存储容器,默认情况下数组长度是10个,也可以在创建ArrayList对象的时候指定初始长度随着在程序里面不断往ArrayList里面添加数据超过十个的时候,ArrayList中就没有足够的容量去存储后续的数据,这时候ArrayList会触发自动扩容机制,自动扩容机制流程是
首先创建一个新的数据这个数组的长度是原来数组长度的1.5倍
然后使用Arrays.copyOf方法把老数组里面的数据拷贝到新的数组里面
扩容完成以后再把当前需要添加的元素加入到新的数组里面,从而完成动态扩容这样一个过程
ArrayList 的 add 操作源码
elementData[size++] = e;
ArrayList 的不安全主要体现在两个方面:
其一:
elementData[size++] = e;不是一个原子操作,是分两步执行的
elementData[size] = e;
size++;
单线程执行这段代码完全没问题,可是到多线程环境下可能就有问题了。可能一个线程会覆盖另一个线程的值
其二:
ArrayList 默认数组大小为 10。假设现在已经添加进去 9 个元素了,size = 9
但因为数组还没有扩容,最大的下标才为 9,所以会抛出数组越界异常
1方便解耦,便于开发(Spring就是一个大工厂,可以将所有对象的创建和依赖关系维护都交给spring管理)
2.spring支持aop编程(spring提供面向切面编程,可以很方便的实现对程序进行权限拦截和运行监控等功能)
3.声明式事务的支持(通过配置就完成对事务的支持,不需要手动编程)
4.方便程序的测试,spring 对junit4支持,可以通过注解方便的测试spring 程序
5.方便集成各种优秀的框架()
6.降低javaEE API的使用难度(Spring 对javaEE开发中非常难用的一些API 例如JDBC,javaMail,远程调用等,都提供了封装,使这些API应用难度大大降低)
七种传播行为和四种隔离级别
七种传播行为:RSMRNNN
REQUIRED(spring默认的) :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。 NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。 NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。 NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED 。 指定方法:通过使用 propagation 属性设置,例如:@Transactional(propagation = Propagation.REQUIRED)
事务传播 - Propagation REQUIRED: 使用当前的事务,如果当前没有事务,则自己新建一个事务,子方法是必须运行在一个事务中的; 如果当前存在事务,则加入这个事务,成为一个整体。 举例:领导没饭吃,我有钱,我会自己买了自己吃;领导有的吃,会分给你一起吃。 SUPPORTS: 如果当前有事务,则使用事务;如果当前没有事务,则不使用事务。 举例:领导没饭吃,我也没饭吃;领导有饭吃,我也有饭吃。 MANDATORY: 该传播属性强制必须存在一个事务,如果不存在,则抛出异常 举例:领导必须管饭,不管饭没饭吃,我就不乐意了,就不干了(抛出异常) REQUIRES_NEW: 如果当前有事务,则挂起该事务,并且自己创建一个新的事务给自己使用; 如果当前没有事务,则同 REQUIRED 举例:领导有饭吃,我偏不要,我自己买了自己吃 NOT_SUPPORTED: 如果当前有事务,则把事务挂起,自己不适用事务去运行数据库操作 举例:领导有饭吃,分一点给你,我太忙了,放一边,我不吃 NEVER: 如果当前有事务存在,则抛出异常 举例:领导有饭给你吃,我不想吃,我热爱工作,我抛出异常 NESTED: 如果当前有事务,则开启子事务(嵌套事务),嵌套事务是独立提交或者回滚; 如果当前没有事务,则同 REQUIRED。 但是如果主事务提交,则会携带子事务一起提交。 如果主事务回滚,则子事务会一起回滚。相反,子事务异常,则父事务可以回滚或不回滚。 举例:领导决策不对,老板怪罪,领导带着小弟一同受罪。小弟出了差错,领导可以推卸责任。
四种隔离级别:
DEFAULT :默认值,表示使用底层数据库的默认隔离级别。大部分数据库为READ_COMMITTED(MySql默认隔离级别为REPEATABLE_READ)
READ_UNCOMMITTED :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
READ_COMMITTED :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
REPEATABLE_READ :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
1.数据库引擎不支持事务
2.异常被catch没有抛出异常 导致@Transactional失效
3.如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。
同一个类中,没有被@Transactional注解的方法A调用注解了的方法B,则改注解不生效
4.@Transactional 注解属性 propagation 设置错误,以下三种 propagation,事务将不会发生回滚。
5.TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
6.TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
7.TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
8.@Transactional 注解属性 rollbackFor 设置错误
Spring默认在抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error时才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
这个事务注解很简单。其实就是通过拦截器拦截注解,然后加载注解参数,根据隔离级别来判断是否需要创建事务, 如果有多个事务隔离级别,就会涉及到事务保存点, 或者是不同事务之间还会涉及掉事务挂起
1:事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入DataSource实例的某个与DataSourceTransactionManager相关的某处容器中。
在接下来的整个事务中,客户代码都应该使用该connection连接数据库,执行所有数据库命令[不使用该connection连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚](物理连接connection逻辑上新建一个会话session;DataSource与TransactionManager配置相同的数据源)
2:事务结束时,回滚在第1步骤中得到的代理connection对象上执行的数据库命令,然后关闭该代理connection对象(事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用)
原理就是通过Java的反射技术来实现的!通过反射(三种反射方式:new对象、通过路径、通过类名)我们可以获取类的所有信息(成员变量、类名等等等)!
再通过配置文件(xml)或者注解来描述类与类之间的关系
我们就可以通过这些配置信息和反射技术来构建出对应的对象和依赖关系了使用
不用自己组装,拿来就用。
享受单例的好处,效率高,不浪费空间。
便于单元测试,方便切换mock组件。
便于进行AOP操作,对于使用者是透明的。
统一配置,便于修改。
aop底层是采用动态代理机制实现的:接口+实现类
接口:spring采用jdk的动态代理proxy
实现类:spring采用cglib实现字节码增强
就是由代理创建出一个和impl实现类平级的一个对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和impl相同的功能,这个就是aop的横向机制原理,这样就不需要修改源代码
1)客户端发送请求到 DispatcherServlet
2)DispatcherServlet 查询 handlerMapping 找到处理请求的 Controller
3)Controller 调用业务逻辑后,返回 ModelAndView
4)DispatcherServlet 查询 ModelAndView,找到指定视图
5)视图将结果返回到客户端
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的
在spring程序main方法中 添加@SpringBootApplication或者@EnableAutoConfiguration 会自动去maven中读取每个starter中的spring.factories文件 该文件里配置了所有需要被创建spring容器中的bean
1.@Autowired:Spring注解| @Resource:JDK注解 (推荐使用@Resource)
2.@Autowired 通过zhi类型dao,zhuan动装配(byType 默认按shu照Bean中的daoClass类型)
@Resource 先通过参数名(byName 例如默认按照Bean中的id...),而后是类型
3.@Autowired + @Qualifier("...") = @Resource(name="...")
通常情况下,我们只会写一个实现类,所以建议使用autowire注解 简单方便
工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
策略模式:枚举 客户端创建一个Context对象a,创建策略对象并当做参数传递给a,然后客户端使用a方法通过某种方法得到想要的值返回给客户端。
一 它们的用途不一样。简单工厂模式是创建型模式,它的作用是创建对象。策略模式是行为型模式,作用是在许多行为中选择一种行为,关注的是行为的多样性。
二 解决的问题不同。简单工厂模式是解决资源的统一分发,将对象的创立同客户端分离开来。策略模式是为了解决策略的切换和扩展。
三 工厂相当于黑盒子,策略相当于白盒子
简单工厂模式
又叫做静态工厂方法(StaticFactory Method)模式,但不属于23种GOF设计模式之一。 简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。 spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。如下配置,就是在 HelloItxxz 类中创建一个 itxxzBean。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。 spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是是任意的java对象。 Spring下默认的bean均为singleton,可以通过singleton=“true|false” 或者 scope=“?”来指定
代理模式
在Spring的Aop中,使用的Advice(通知)来增强被代理类的功能。Spring实现这一AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)对类进行方法级别的切面增强,即,生成被代理类的代理类, 并在代理类的方法前,设置拦截器,通过执行拦截器重的内容增强了代理方法的功能,实现的面向切面编程
策略模式
Spring中的策略模式使用多如牛毛,所谓策略模式就是定义了算法族,分别封装起来,让他们之前可以互相转换,此模式然该算法的变化独立于使用算法的客户。Spring的事务管理机制就是典型的策略模式,Spring事务策略是通过PlatformTransactionManager接口实现的
SpringSecurityOauth2包含了认证服务器(@EnableAuthorizationServer)和资源服务器,认证服务器(@EnableResourceServer)是用来生成令牌(token),资源服务器是验证该请求是否存在令牌,如果存在才能通过,否则不通过
认证服务器有4中授权模式:
1.用户名和密码模式
2.授权码模式
3.客户端模式
4.简化模式
原理:
用户发起获取token的请求。
过滤器会验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作。
过滤器通过clientId查询生成一个Authentication对象。
然后会通过username和生成的Authentication对象生成一个UserDetails对象,并检查用户是否存在。
以上全部通过会进入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
之后调用AbstractTokenGranter中的grant方法。
grant方法中调用AbstractUserDetailsAuthenticationProvider的authenticate方法,通过username和Authentication对象来检索用户是否存在。
然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
然后将OAuth2AccessToken对象包装进响应流返回。
1、实例化一个Bean--也就是我们常说的new;
2、按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;
3、如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值
4、如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);
5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);
6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;
7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。
8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;
注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述。
9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;
10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
避免对索引字段进行计算操作 避免在索引字段上使用not,<>,!= 避免在索引列上使用IS NULL和IS NOT NULL 避免在索引列上出现数据类型转换 避免在索引字段上使用函数 避免建立索引的列中使用空值
结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率
间隙锁,锁的就是两个值之间的空隙。间隙锁是在可重复读隔离级别(Repeatable Read)下才会生效。在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样;在普通索引和唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序
A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
C一致性一般由代码层面来保证
I隔离性由MVCC来保证
D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复
1、事务的支持不同(innodb支持事务,myisam不支持事务)
2、锁粒度(innodb行锁应用,myisam表锁)
3、存储空间(innodb既缓存索引文件又缓存数据文件,myisam只能缓存索引文件).bdl .myl,.myi
4、存储结构
(myisam:数据文件的扩展名为.MYD myData ,索引文件的扩展名是.MYI myIndex)
(innodb:所有的表都保存在同一个数据文件里面 即为.Ibd)
自增ID达到上限用完了之后,分为两种情况:
1:如果设置了主键,那么将会报错主键冲突。
2:如果没有设置主键,数据库则会帮我们自动生成一个全局的row_id,新数据会覆盖老数据解决方案:
表尽可能都要设置主键,主键尽量使用bigint类型,21亿的上限还是有可能达到的,比如魔兽,虽然说row_id上限高达281万亿,但是覆盖数据显然是不可接受的
1、唯一索引:确保数据唯一性
2、非唯一索引:这些字段可以重复,不要求唯一.
3、主键索引:是唯一索引的特定类型,创建主键时自动创建.
4、聚簇索引: 表中记录的物理顺与键值顺序相同,表数据和主键一起存储.
5、非聚簇索引: 表数据和索引分两部分存储
1、主键一定会创建一个唯一索引,但是有唯一索引的列不一定是主键; 2、主键不允许为空值,唯一索引列允许空值; 3、一个表只能有一个主键,但是可以有多个唯一索引; 4、主键可以被其他表引用为外键,唯一索引列不可以; 5、主键是一种约束,而唯一索引是一种索引,是表的冗余数据结构,两者有本质的差别
1、表的主键、外键必须有索引
2、数据量超过300的表应该有索引
3、经常与其他表进行连接的表,在连接字段上应该建立索引
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引
5、索引应该建在选择性高的字段上
1、如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
2、如果是聚合索引,根据最左前缀原则,如果不用第一个字段,则不会使用索引
3、如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
4、如果mysql估计使用全表扫描要比使用索引快,则不使用索引
索引index1:(a,b,c)有三个字段,索引是有序的,index1索引在索引文件中的排列是有序的,首先根据a来排序,然后才是根据b来排序,最后是根据c来排序
1、合理的建立索引能够加速数据读取效率,不合理的建立索引会拖慢数据库的响应速度。
2、索引越多,更新数据的速度越慢。
不要在选择的栏位上放置索引,这是无意义的。应该在条件选择的语句上合理的放置索引,比如where,order by。 例子: SELECT id,title,content,cat_id FROM article WHERE cat_id = 1; 上面这个语句,你在id/title/content上放置索引是毫无意义的,对这个语句没有任何优化作用。但是如果你在外键cat_id上放置一个索引,那作用就相当大了
hash:虽然可以快速定位,但是没有顺序,IO复杂度高。
二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
红黑树:树的高度随着数据量增加而增加,IO代价高
B-Tree索引:B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中
B+Tree索引:B -Tree的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高
因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出)
指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低
B+树的数据都存储在叶子节点
B+树能显著减少IO次数,提高效率
B+树的查询效率更加稳定,因为数据放在叶子节点
B+树能提高范围查询的效率,因为叶子节点指向下一个叶子节点
使用B-Tree的好处
B树可以再内部节点存储键值和相关记录数据,因此把频繁访问的数据放在靠近根节点的位置将大大提高热点数据的查询效率。B树一般在特定数据重复多次查询的场景中更加高效
使用B+Tree的好处
B+树内部节点不存储数据,只存储键值,这样,每个节点就能存储更多的键值,一次性也就能将更多的键值读入内存,减少了对磁盘的IO操作次数,且B+树的叶节点有一条链相连,所以对于区间内查询数据比较高效
简单版回答如下:
Hash哈希,只适合等值查询,不适合范围查询。
一般二叉树,可能会特殊化为一个链表,相当于全表扫描。
红黑树,是一种特化的平衡二叉树,MySQL 数据量很大的时候,索引的体积也会很大,内存放不下的而从磁盘读取,树的层次太高的话,读取磁盘的次数就多了。
B-Tree,叶子节点和非叶子节点都保存数据,相同的数据量,B+树更矮壮,也是就说,相同的数据量,B+树数据结构,查询磁盘的次数会更少。
B-树内部节点是保存数据的;而B+树内部节点是不保存数据的,只作索引作用,它的叶子节点才保存数据。
B+树相邻的叶子节点之间是通过链表指针连起来的,B-树却不是。
查找过程中,B-树在找到具体的数值以后就结束,而B+树则需要通过索引找到叶子结点中的数据才结束
B-树中任何一个关键字出现且只出现在一个结点中,而B+树可以出现多次。
B±Tree定义
其定义基本与B-Tree相同
除了:
非叶子节点仅用来做索引,数据都保存在叶子节点中。
所有叶子节点均有一个链指针指向下一个叶子节点 3、非叶子节点的子树指针与关键字个数相同。
非叶子节点的子树指针p[i],指向关键字值(K[i],K[i+1])的子树。
间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间
中心思想:
间隙锁锁的是索引叶子结点的next指针
解决的问题:
解决了mysql RR REPEATABLE_READ(可重复读)级别下是幻读的问题
四种隔离级别:
产生间隙锁的条件:
使用普通索引锁定; 使用多列唯一索引; 使用唯一索引锁定多行记录。
打开间隙锁设置 innodb_locks_unsafe_for_binlog:默认值为OFF,即启用间隙锁
行锁是建立在索引的基础上。 普通索引的数据重复率过高导致索引失效,行锁升级为表锁
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。
不同的进程使用不同的内存空间,而线程共享同一进程的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
线程作为操作系统能够进行运算调度的最小单位,进程作为资源分配的最小单位。
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
有4种方式:继承Thread类、实现Runnable接口、实现Callable接口、使用Executor框架来创建线程池
sleep()方法没有释放锁,而 wait()方法释放了锁。
sleep()方法属于Thread类的静态方法,作用于当前线程;而wait()方法是Object类的实例方法,作用于对象本身。
执行sleep()方法后,可以通过超时或者调用interrupt()方法唤醒休眠中的线程;执行wait()方法后,通过调用notify()或notifyAll()方法唤醒等待线程
start() 可以启动一个新线程,run() 不能
start() 不能被重复调用,run() 可以
start() 中的 run 代码可以不执行完就继续执行下面的代码,进行线程切换,而调用 run() 方法必须等待其代码全部执行完才能继续执行下面的代码
start() 实现了多线程,run() 没有
①corePoolSize:线程池的基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
②maximumPoolSize:最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
③keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
⑤unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
⑥workQueue:用于保存等待执行任务的阻塞队列,提交的任务将会被放到这个队列里。
⑦threadFactory:线程工厂,用来创建线程。主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
⑧handler:拒绝策略,即当线程和队列都已经满了的时候,应该采取什么样的策略来处理新提交的任务。默认策略是AbortPolicy(抛出异常),其他的策略还有:CallerRunsPolicy(只用调用者所在线程来运行任务)、DiscardOldestPolicy(丢弃队列里最近的一个任务,并执行当前任务)、DiscardPolicy(不处理,丢弃掉)
需要依赖线程的执行结果,可以在这里调用join方法等待线程执行结束,用join来保证线程的顺序性的
首先是因为 ThreadLocal 是基于 ThreadLocalMap 实现的,其中 ThreadLocalMap 的 Entry 继承了 WeakReference ,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说, Entry 中的 key 是一个弱引用类型,对于弱引用来说,它只能存活到下次 GC 之前 ;如果此时一个线程调用了 ThreadLocalMap 的 set 设置变量,当前的 ThreadLocalMap 就会新增一条记录,但由于发生了一次垃圾回收,这样就会造成一个结果: key 值被回收掉了,但是 value 值还在内存中,而且如果线程一直存在的话,那么它的 value 值就会一直存在
redis 内存淘汰机制有以下几个: • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。 • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。 • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。 • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。 • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。 • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除
一个不做处理 直接报异常
两个从redis内存中删除: 1 移除最少使用的 2 一个是随机移除
三个从redis内存中快要过期的兼空间里移除 1移除最少使用 2 随机移除 3移除快要过期的
Lfu 根据次数清理
限定 Redis 占用的内存,Redis 会根据自身数据淘汰策略,留下热数据到内存。所以,计算一下 50W 数据大约占用的内存,然后设置一下 Redis 内存限制即可,并将淘汰策略为volatile-lru或者allkeys-lru
主从复制、哨兵模式、集群
缓存穿透:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将
导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。
解决方案:1.查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短;2.布
隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据
会被这个 bitmap 拦截掉,从而避免了对 DB 的查询。
缓存击穿:对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对
这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并
回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。解决方案:1.使用互斥锁:当缓存失效时,不立即去 load db,先使用如 Redis 的 setnx 去设
置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的
方法。2.永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部
转发到 DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个
key 缓存。
解决方案:将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,
比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效 的事件
redis的事务先以MULTI开启事务,将多个命令入队到事务中,然后通过EXEC命令触发执行队伍中的所有命令,如果想取消事务可以执行discard命令
AOF:以独立日志记录写命令方式存储日志,重启时再重新执行日志中记录的数据即可完成恢复.优势是数据安全,不容易丢数据,而且aof中的内容可以读懂,如果不小心敲了flushall命令,还可以将aof文件尾部的flushall命令删掉,然后就可以恢复数据了.缺点是占用存储空间大,数据恢复较慢.
RDB:将内存中的数据以快照的方式写进二进制文件中.优势是数据恢复快,方便备份,缺点是比较耗内存,可能会造成数据丢失
Redis6.0多线程
流程简述如下:
1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列
2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程
3、主线程阻塞等待 IO 线程读取 socket 完毕
4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行
5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
6、解除绑定,清空等待队列
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手
第一次挥手:客户端发送一个FIN报文,报文中指定一个序列号(seq=x+2),此时客户端处于FIN_WAIT状态
第二次挥手:服务端收到FIN,会发送一个ACK报文,且把客户端的序列号值+1(x+3)作为ACK报文的序列号,表明已经收到客户端的报文了,此时客户端的处于CLOSE_WAIT状态
第三次挥手:如果服务器也想断开连接,和客户端第一次挥手一样,发送FIN报文,且指定一个序列号(seq=y+1),此时服务端处于LAST_ACK的状态
第四次挥手:客户端收到FIN后,一样发送一个ACK作为应答,且把服务端的序列号值+1(y+2)作为自己ACK报文的序列号值,此时客户端处于RIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态
服务端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态
两者区别:
1) TCP提供面向连接的传输,通信前要先建立连接(三次握手机制);
UDP提供无连接的传输,通信前不需要建立连接。 2) TCP提供可靠的传输(有序,无差错,不丢失,不重复);
UDP提供不可靠的传输。 3) TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组;
UDP是面向数据报的传输,没有分组开销。 4) TCP提供拥塞控制和流量控制机制; UDP不提供拥塞控制和流量控制机制。
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
优点:
1、服务拆分粒度更细,有利于资源重复利用,有利于提高开发效率
2、可以更精准的制定优化服务方案,提高系统的可维护性
3、微服务架构采用去中心化思想,服务之间采用Restful等轻量级通讯,比ESB更轻量
4、适于互联网时代,产品迭代周期更短
缺点:
1、微服务过多,治理成本高,不利于维护系统
2、分布式系统开发的成本高(容错,分布式事务等)对团队挑战大
总的来说优点大过于缺点,目前看来SpringCloud是一套非常完善的分布式框架,目前很多企业开始用微服务、Spring Cloud 的优势是显而易见的。因此对于想研究微服务架构的同学来说,学习 Spring Cloud 是一个不错的选择。
Eureka : 就是服务注册中心(可以是一个集群),对外暴露自己地址;
提供者 : 启动后向Eureka注册自己信息(地址,提供什么服务)
消费者 : 向Eureka 订阅服务,Eureka会将对应服务的服务列表发送给消费者,并且定期更新
心跳(续约): 提供者定期通过http方式向Eureka刷新自己的状态
springCloudConfig分服务端和客户端,服务端负责将本地,git或者svn中存储的配置文件发布成REST风格的接口,客户端可以从服务端REST接口获取配置。但客户端并不能主动感知到配置的变化,从而主动去获取新的配置,这需要每个客户端通过POST方法触发各自的/refresh接口。而我们上面说的SpringCloudBus就发挥了其作用了
SpringCloudBus通过一个轻量级消息代理连接分布式系统的节点(有点像消息队列那种)。这可以用于广播状态更改(如配置更改)或其他管理指令。SpringCloudBus提供了通过post方法访问的endpoint/bus/refresh(spring boot 有很多监控的endpoint,比如/health),这个接口通常由git的钩子功能(监听触发)调用,用以通知各个SpringCloudConfig的客户端去服务端更新配置。
一 第一步
在开发微服务时,会在主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。
二 第二步
当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法并调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。
三 第三步
然后又RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。
Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,基于重试器发送HTTP请求:Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求
(1)使用feign客户端调用其他微服务时,发送POST请求时,对象信息没有传递成功。关键在于加上注解:@RequestBody
(2)使用feign客户端调用其他微服务时,报错超时:e=feign.RetryableException: Read timed out executing POST
ribbon.ReadTimeout=60000
ribbon.ConnectTimeout=60000
服务注册与发现是是针对于三大角色的:①:服务提供者 ②:服务消费者 ③:注册中心
针对他们之间的关系描述就是服务的注册与发现的运行原理:
1、各微服务在服务启动的时候,服务提供者将服务的网络地址(IP地址和端口),服务名称,服务自身状态以及访问协议等信息注册到注册中心,注册中心存储该数据 2、服务消费者从注册中心查询到服务提供者的实例信息,并通过该地址调用服务提供者的接口 3、各个微服务与注册中心通过一定的机制通信(例如心跳检测机制),如果注册中心与某微服务长时间无法通信,就会注销该实例,即该服务对应的实例会被注册中心剔除 4、微服务网络地址发生变化(例如增加实例或IP变动等),会重新注册到注册中心,这样,服务消费者就无需人工修改提供者的网络地址了
Nacos 宕机后,仍然可以调用成功,注册中心宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表
Nacos支持CP+AP模式,即Nacos可以根据配置识别为CP模式或AP模式,默认是AP模式。如果注册Nacos的client节点注册时ephemeral=true,那么Nacos集群对这个client节点的效果就是AP,采用distro协议实现;而注册Nacos的client节点注册时ephemeral=false,那么Nacos集群对这个节点的效果就是CP的,采用raft协议实现。根据client注册时的属性,AP,CP同时混合存在,只是对不同的client节点效果不同。Nacos可以很好的解决不同场景的业务需求。
Nginx是反向代理同时可以实现负载均衡,nginx拦截客户端请求采用负载均衡策略根据upstream配置进行转发,相当于请求通过nginx服务器进行转发。Ribbon是客户端负载均衡,从注册中心读取目标服务器信息,然后客户端采用轮询策略对服务直接访问,全程在客户端操作
Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息
RoundRobinRule: 默认轮询的方式
RandomRule: 随机方式
WeightedResponseTimeRule: 根据响应时间来分配权重的方式,响应的越快,分配的值越大。
BestAvailableRule: 选择并发量最小的方式
RetryRule: 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
ZoneAvoidanceRule: 根据性能和可用性来选择。
AvailabilityFilteringRule: 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值)
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要是对dubbo进行各种配置的
第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层
工作流程:
添加图片注释,不超过 140 字(可选)Provider(提供者)绑定指定端口并启动服务
指供者连接注册中心,并发本机IP、端口、应用信息和提供服务信息发送至注册中心存储
Consumer(消费者),连接注册中心 ,并发送应用信息、所求服务信息至注册中心
注册中心根据 消费 者所求服务信息匹配对应的提供者列表发送至Consumer 应用缓存。
Consumer 在发起远程调用时基于缓存的消费者列表择其一发起调用。
Provider 状态变更会实时通知注册中心、在由注册中心实时推送至Consumer
1)第一步,provider提供者向注册中心去注册
2)第二步,consumer消费者从注册中心订阅服务,注册中心会通知consumer注册好的服务
3)第三步,consumer消费者调用provider提供者
4)第四步,consumer消费者和provider提供者都异步的通知监控中心
(2)注册中心挂了可以继续通信吗?
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信
一致性Hash均衡算法,轮询,随机调用,最少活动调用法.
获取分布式锁的流程:
在获取分布式锁的时候在locker节点(locker节点是Zookeeper的指定节点)下创建临时顺序节点,释放锁的时候删除该临时节点。
客户端调用createNode方法在locker下创建临时顺序节点,然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。
客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。
如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
我的解释:
A在Locker下创建了Node_n —>循环 ( 每次获取Locker下的所有子节点 —> 对这些节点按节点自增号排序顺序 —> 判断自己创建的Node_n是否是第一个节点 —> 如果是则获得了分布式锁 —> 如果不是监听上一个节点Node_n-1 等它释放掉分布式锁。)
分布式锁要解决的是分布式环境下,并行相同代码的加锁功能;了解过redis分布式锁的人肯定知道,一开始redis作为分布式锁用的是setnx,再这基础上设置个定时过期时间,但这种方式有什么问题呢?
实际上看懂上图的人也就明白了那有什么问题,首先是原子性问题,setnx+过期时间这两个操作必须是原子性的,所以这可以用lua脚本解决
再然后是释放锁的时机该如何定?
不管我们定多少过期时间,都不能保证,在这段时间内锁住的代码执行完成了,所以这个时间定多少都不好;
如果不定时间,当执行完成后释放锁,问题就是如果执行到一半机器宕机,那这把锁就永远放不掉了
那Redisson是如何解决上述问题的呢?
它对代码进行了精简的封装,我们的使用非常简单,甚至我们不用主动设置过期时间
它设计了个watch dog看门狗,每隔10秒会检查一下是否还持有锁,若持有锁,就给他更新过期时间30秒;通过这样的设计,可以让他在没有释放锁之前一直持有锁,哪怕宕机了,也能自动释放锁
而不能获得锁的客户端则是不断循环尝试加锁
通过记录锁的客户端id,可以把它设计成可重入锁
存在的问题:
redis作为分布式锁再大多数情况下是没问题的,但是我们知道CAP原理,一致性,可用性,分区容错性
在redis分布式架构中,我们其实保证的是AP模型,也就是尽可能的保证了redis的可用性,这在一般系统中当然是没问题的,哪怕有时候一致性有点问题(实际读到的数据不正确,或已经写入没读到)毕竟是作为缓存的存在,一定延迟可以接受,没读到可以再读数据库,这是没问题的。
但在分布式锁中,一旦出现该读到没读到,那就是重复锁的问题了,相当于分布式锁没起到作用。
这种情况发生在什么时候呢?redis集群主节点再获取锁后,没来得及复制数据给从节点,此时宕机了,从节点接替主节点进行读写,此时新的主节点没有持有该锁,那么其他想要获取该锁的服务也可以获取到该锁,导致了重复锁的问题。
一般来讲这种情况发生的概率是很小的,看你系统的规模而定,我觉得像阿里这种规模就应该不会用redis来作为分布式锁了
CP模型的分布式锁可以用zookeeper,可能性能略低于redis,但能保证安全,可以提供顺序锁等额外功能
一、2PC提交协议是什么?
1.1准备阶段
1.2 提交阶段
二、2PC提交协议有什么缺点?
单点故障 同步阻塞
三、3PC提交协议是什么?
3.1CanCommit阶段
3.2PreCommit阶段
3.3doCommit阶段
XID:一个全局事务的唯一标识,由ip:port:sequence组成
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
Seata有三个组成部分:事务协调器TC:协调者、事务管理器TM:发起方、资源管理器RM:参与方
(1)发起方会向协调者申请一个全局事务id,并保存到ThreadLocal中(为什么要保存到ThreadLocal中?弱引用,线程之间不会发生数据冲突)
(2)Seata数据源代理发起方和参与方的数据源,将前置镜像和后置镜像写入到undo_log表中,方便后期回滚使用
(3)发起方获取全局事务id,通过改写Feign客户端请求头传入全局事务id。
(4)参与方从请求头中获取全局事务id保存到ThreadLocal中,并把该分支注册到SeataServer中。
(5)如果没有出现异常,发起方会通知协调者,协调者通知所有分支,通过全局事务id和本地事务id删除undo_log数据,如果出现异常,通过undo_log逆向生成sql语句并执行,然后删除undo_log语句。如果处理业务逻辑代码超时,也会回滚。
XA:XA是基于二阶段事务实现的一种标准协议,目前主流数据库都已支持该协议.由于是二阶段提交,缺点很明显,不适合高并发场景.
补偿机制TCC:try,commit,cancel的缩写,try阶段进行检测,commit提交执行,只要try阶段成功了commit就一定会被执行,cancel业务出现错误时执行,回滚事务,释放资源.
消息中间件MQ:可以通过阿里的消息中间件RocketMQ来进行解决.RocketMQ支持带事务的消息,具体思路大概是这样的:
第一步:消息生产者向消息集群发送prepared消息,获取消息地址.
第二步:本地提交事务,并向集群发送确认消息.
第三步:通过第一步获取到的地址,访问消息并改变消息状态.
有九个内置对象:request、response、out、session、application、pageContext、config、page、exception
作用如下:
(1) HttpServletRequest类的Request对象
作用:代表请求对象,主要用于接受客户端通过HTTP协议连接传输到服务器端的数据。
(2) HttpServletResponse类的Respone对象
作用:代表响应对象,主要用于向客户端发送数据
(3) JspWriter类的out对象
作用:主要用于向客户端输出数据;
Out的基类是JspWriter
(4) HttpSession类的session对象
作用:主要用于来分别保存每个用户信息,与请求关联的会话;
会话状态维持是Web应用开发者必须面对的问题。
(5) ServletContex类的application对象
作用:主要用于保存用户信息,代码片段的运行环境;
它是一个共享的内置对象,即一个容器中的多个用户共享一个application对象,故其保存的信息被所有用户所共享.
(6) PageContext类的PageContext对象
作用:管理网页属性,为JSP页面包装页面的上下文,管理对属于JSP中特殊可见部分中已命名对象的访问,它的创建和初始化都是由容器来完成的。
(7) ServletConfig类的Config对象
作用:代码片段配置对象,表示Servlet的配置。
(8) Object类的Page(相当于this)对象
作用:处理JSP网页,是Object类的一个实例,指的是JSP实现类的实例,即它也是JSP本身,只有在JSP页面范围之内才是合法的。
(9)Exception
作用:处理JSP文件执行时发生的错误和异常
1.加载和实例化
2.初始化
3.请求处理
4.服务终止
当第一个用户请求到来时,服务器根据部署描述符找到所访问的Servlet类,并查找当前容器,发现没有此类的实例,说明是第一次访问此Servlet,所以容器开始加载Servlet,并实例化此对象,实例化之后会立即调用init()方法,而且init()方法由始至终只被调用一次,所以比较适合进行对象的初始化工作,比如读取配置文件,初始化属性等。当调用完init()方法后,Servlet实例处于ready状态,这时开始调用其service()方法,并等待着下一次请求的到来
service()方法是服务方法,只要再有请求到来,服务器都会产生一个新的线程,调用service()方法
如果实例长时间未被访问,或者web服务器关闭,则容器会负责调用destroy()方法,该方法可以将一些资源释放
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
GC是垃圾收集的意思,内存处理是开发人员容易出现问题的地方,忘记或者错误地内存回收会导致程序或者系统的不稳定甚至崩溃,Java提供的垃圾回收机制可以自动检测对象是否超过作用域从而达到自动回收的目的
回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收
GC回收器的基本原理是什么
对于GC来说,当创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行
GC是垃圾回收机制,java中申请的内存可以被垃圾回收装置进行回收,GC可以一定程度的避免内存泄漏,但是会引入一些额外的开销。
GC中主要回收的是堆和方法区中的内存,栈中内存的释放要等到线程结束或者是栈帧被销毁,而程序计数器中存储的是地址不需要进行释放。
标记-清除算法
分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。
执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。
存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。
标记-复制算法
为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。
实现简单、运行高效,解决了内存碎片问题。代价是可用内存缩小为原来的一半,浪费空间。
HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。
标记-整理算法
标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。
老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。
标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
a) 年老代(Tenured)被写满; b) 持久代(Perm)被写满; c) System.gc()被显示调用; d) 上一次GC之后Heap的各域分配策略动态变化
(1)Minor GC
即新生代GC,指的是发生在新生代的垃圾收集。当年轻代空间不足的时候会触发 Minor GC(这里指的是伊甸区空间不足),因为Java对象大多都具备朝生夕灭的特性,因此Minor GC非常频繁,一般回收速度也比较快,采用复制算法
(2)Major GC
即老年代GC,指的是发生在老年代的垃圾收集。当老年代空间不足的时候,会先尝试触发 Minor GC,如果之后空间仍然不足,则触发 Major GC。经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。采用标记算法
(3)Full GC
针对新生代、老年代的GC
只不过一般 Minior GC 工作量比较小. 开销也小, 对于程序影响不大.
一般我们可以把 Major GC 近似视为 Full GC
常见的 GC 日志开启参数包括:
1、 -Xloggc:filename,指定日志文件路径
2、 -XX:+PrintGC,打印 GC 基本信息
3、 -XX:+PrintGCDetails,打印 GC 详细信息
4、 -XX:+PrintGCTimeStamps,打印 GC 时间戳
5、 -XX:+PrintGCDateStamps,打印 GC 日期与时间
6、 -XX:+PrintHeapAtGC,打印 GC 前后的堆、方法区、元空间可用容量变化
7、 -XX:+PrintTenuringDistribution,打印熬过收集后剩余对象的年龄分布信息,有助于 MaxTenuringThreshold 参数调优设置
8、 -XX:+PrintAdaptiveSizePolicy,打印收集器自动设置堆空间各分代区域大小、收集目标等自动调节的相关信息
9、 -XX:+PrintGCApplicationConcurrentTime,打印 GC 过程中用户线程并发时间
10、 -XX:+PrintGCApplicationStoppedTime,打印 GC 过程中用户线程停顿时间
11、 -XX:+HeapDumpOnOutOfMemoryError,堆 oom 时自动 dump
12、 -XX:HeapDumpPath,堆 oom 时 dump 文件路径
Java 9 JVM 日志模块进行了重构,参数格式发生变化,这个需要知道。
GC 日志输出的格式,会随着上面的参数不同而发生变化。关注各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量、用户线程停顿时间。
借助工具可视化工具可以更方便的分析,在线工具 GCeasy;离线版可以使用 GCViewer。
如果现场环境不允许,可以使用 JDK 自带的 jstat 工具监控观察 GC 情况。
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器, 它重点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
Bootstrap ClassLoader(启动类加载器) Extention ClassLoader(扩展类加载器) App ClassLoader(应用类加载器)
1、 jps -v 可以查看 jvm 进程显示指定的参数
2、 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有参数的值
3、 jinfo 可以实时查看和调整虚拟机各项参数
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。
1、 强引用,就是普通的对象引用关系,如 String s = new String("ConstXiong")
2、 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现
3、 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现
4、 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现
Java 语言中一个显著的特点就是引入了垃圾回收机制,使 C++ 程序员最头疼的内存管理的问题迎刃而解,它使得 Java 程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java 中的对象不再有“作用域”的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。
分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。
新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:
1、 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;
2、 Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;
3、 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代
4、 Survivor 区相同年龄所有对象大小的总和 (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%
5、 Survivor 区内存不足会发生担保分配
6、 超过指定大小的对象可以直接进入老年代
Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC
FullGC,整个堆的垃圾收集,触发条件:
1、 每次晋升到老年代的对象平均大小>老年代剩余空间
2、 MinorGC后存活的对象超过了老年代剩余空间
3、 元空间不足
4、 System.gc() 可能会引起
5、 CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
6、 堆内存分配很大的对象
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。
新生代
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden区、 ServivorFrom、 ServivorTo 三个区。
Eden 区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
ServivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo
保留了一次 MinorGC 过程中的幸存者。
MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法。
eden、 servicorFrom 复制到 ServicorTo,年龄+1
首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
清空 eden、 servicorFrom
然后,清空 Eden 和 ServicorFrom 中的对象;
ServicorTo 和 ServicorFrom 互换
最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。
基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。
ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。
STW并不会只发生在内存回收的时候。现在程序员这么卷,碰到几次safepoint的问题几率也是比较大的。
当发生GC时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为JVM是安全的(safe),整个堆的状态是稳定的。
如果在GC前,有线程迟迟进入不了safepoint,那么整个JVM都在等待这个阻塞的线程,造成了整体GC的时间变长。
jps:
用来显示本地的 Java 进程,可以查看本地运行着几个 Java 程序,并显示他们的进程号。 命令格式:jps
jinfo:
运行环境参数:Java System 属性和 JVM 命令行参数,Java class path 等信息。 命令格式:jinfo 进程 pid
jstat:
监视虚拟机各种运行状态信息的命令行工具。 命令格式:jstat -gc 123 250 20
jstack:
可以观察到 JVM 中当前所有线程的运行情况和线程当前状态。 命令格式:jstack 进程 pid
jmap:
观察运行中的 JVM 物理内存的占用情况(如:产生哪些对象,及其数量)。 命令格式:jmap [option] pid
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂。整个过程分为以下 4 个阶段:
初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
不是。当新生代内存不够时,老年代分配担保。而大对象则是直接在老年代分配。
进程占用的内存,可以使用top命令,看RES段占用的值。如果这个值大大超出我们设定的最大堆内存,则证明堆外内存占用了很大的区域。
使用gdb可以将物理内存dump下来,通常能看到里面的内容。更加复杂的分析可以使用perf工具,或者谷歌开源的gperftools。那些申请内存最多的native函数,很容易就可以找到。
当操作系统内存不足的时候,会将部分数据写入到SWAP交换分中,但是SWAP的性能是比较低的。如果应用的访问量较大,需要频繁申请和销毁内存,就容易发生卡顿。一般高并发场景下,会禁用SWAP。
设定堆内存大小
1、 -Xmx:堆内存最大限制。设定新生代大小。新生代不宜太小,否则会有大量对象涌入老年代
2、 -XX:NewSize:新生代大小
3、 -XX:NewRatio 新生代和老生代占比
4、 -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
5、 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
(这个话题很大,可以从实践环节中随便摘一个进行总结,下面举例一个最普通的)
你可以来一个中规中矩的回
内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动后,使用jstat命令,发现Old区在一直增长。我使用jmap命令,导出了一份线上堆栈,然后使用MAT进行分析。通过对GC Roots的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学做缓存用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 guava的Cache,并设置了弱引用,故障就消失了。
这个回答不是十分出彩,但着实是常见问题,让人挑不出毛病。
加载、验证、准备、解析、初始化。
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐 )
这通常会使用另外一个参数:-XX:+PrintCommandLineFlags可以打印所有的参数,包括使用的垃圾回收器。
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
具体可能会聊聊jdk1.7以前的PermGen(永久代),替换成Metaspace(元空间)
1、 原本永久代存储的数据:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap
2、 Metaspace(元空间)存储的是类的元数据信息(metadata)
3、 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
4、 替换的好处:一、字符串存在永久代中,容易出现性能问题和内存溢出。二、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5GB,Solaris 大约3GB。64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
Map的key和value都可以是任何类型。但要注意的是,一定要重写它的equals和hashCode方法,否则容易发生内存泄漏。
标记清除(缺点是碎片化) 复制算法(缺点是浪费空间) 标记整理算法(效率比前两者差) 分代收集算法(老年代一般使用“标记-清除”、“标记-整理”算法,年轻代一般用复制算法)
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器, JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器
-Xss可以设置线程栈的大小,当线程方法递归调用层次太深或者栈帧中的局部变量过多时,会出现栈溢出错误 java.lang.StackOverflowError