目录
6、equals与hashcode的区别,存入集合时的判断过程
8、垃圾回收算法,新生代和年老代用的什么算法,为什么用这个算法?
1、自我介绍
2、介绍一下微服务架构(因为研究方向是微服务架构)
2.1、什么事为服务架构
形象一点来说,微服务架构就像搭积木,每个微服务都是一个零件,并使用这个零件组装出不同的形状。通俗来说,微服务架构就是把一个大系统按业务功能分解成多个职责单一的小系统,并利用简单的方法使多个小系统相互协作,组成一个大系统。
如果学科派一点,微服务架构就是把因相同原因而变化的功能聚合到一起,而把因不同原因而变化的功能分离开,并利用轻量化机制(通常为HTTP RESTful API)实现通信。
追本溯源,Martin Folwer对微服务架构的定义是:
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于HTTP协议的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,对具体的服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建。
对于我个人,我更喜欢一种延续性的解释,微服务架构≈ 模块化开发+分布式计算。不管微服务架构的定义怎么样,都是在描述一个核心思想:把大系统拆分成小型系统,把大事化小,以降低系统的复杂性,从而大幅降低系统建设、升级、运维的风险和成本。
顺带提一下,亚马逊创始人Jeff Bezos在2002年就说过:所有团队的模块都要以Service Interface的方式将数据和功能开放出来。不这样做的人会被炒鱿鱼。这才是思路超前的大牛。
需要注意的是“微服务”与“微服务架构”是有本质区别的。“微服务”强调的是服务的大小,它关注的是某一个点。而“微服务架构”则是一种架构思想,需要从整体上对软件系统进行通盘的考虑。
Chris Richardson说:“微服务”是一个很糟糕的名字,它导致开发人员创建了许多粒度很小的服务,每个服务拥有一个单独的REST端点。不仅如此,这个名字还暗示了微服务在开发者心目中的重要位置。例如,人们会说“我们可以用微服务来解决这个问题”;我也看到了越来越多的“某某微服务框架”,而实际上,这些框架跟微服务架构不一定有太多联系,它们只是简单的Web框架(如:spring-boot)。使用“微服务架构”这个名字会更恰当些。它是一种架构风格,它把一系列协作的服务组织成一个系统来支撑业务。
1.2、常见的微服务组件
1.服务注册:服务提供方将自己调用地址注册到服务注册中心,让服务调用方能够方便地找到自己。
2.服务发现:服务调用方从服务注册中心找到自己需要调用的服务的地址。
3.负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,节点选择的工作对服务调用方来说是透明的。
4.服务网关:服务网关是服务调用的唯一入口,可以在这个组件是实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。
5.配置中心:将本地化的配置信息(properties,xml, yaml等)注册到配置中心,实现程序包在开发、测试、生产环境的无差别性,方便程序包的迁移。
6.API管理:以方便的形式编写及更新API文档,并以方便的形式供调用者查看和测试。
7.集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。
8.分布式事务:对于重要的业务,需要通过分布式事务技术(TCC、高可用消息服务、最大努力通知)保证数据的一致性。具有代表性的有spring transaction
9.调用链:记录完成一个业务逻辑时调用到的微服务,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。具有代表性的有pinpoint.
10.支撑平台:系统微服务化后,系统变得更加碎片化,系统的部署、运维、监控等都比单体架构更加复杂,那么,就需要将大部分的工作自动化。现在,可以通过Docker等工具来中和这些微服务架构带来的弊端。 例如:持续集成、蓝绿发布、健康检查、性能健康等等。严重点,以我们两年的实践经验,可以这么说,如果没有合适的支撑平台或工具,就不要使用微服务架构。
一般情况下,如果希望快速地体会微服务架构带来的好处,使用Spring Cloud提供的服务注册(Eureka)、服务发现(Ribbon)、服务网关(Zuul) 三个组件即可以快速入门。其它组件则需要根据自身的业务选择性使用。
1.3微服务架构有哪些优势劣势?
要谈优势,就一定要有对比,我们可以尝试着从两个维度来对比:
1.3.1单体架构和微服务架构的对比
【结论】:
n对于简单项目来说,单体架构5胜8败。
(优势项:开发效率、上手难度、运维效率、硬件需求、项目成本;劣势项:系统设计(高内聚低耦合)、系统设计(扩展性)、需求变更响应速度、系统升级效率、知识积累、非功能需求、职责、成就感、风险)
n对于复杂项目来说,单体架构2胜11败。
(优势项:上手难度、运维效率;劣势项:硬件需求、项目成本、开发效率、系统设计(高内聚低耦合)、系统设计(扩展性)、需求变更响应速度、系统升级效率、知识积累、非功能需求、职责、成就感、风险;)
1.3.2微服务与共享库的对比.,
1.3.3什么场景需要用微服务架构?
看了上面的比较,微服务架构可以说是以压倒性的优势胜过单体架构和共享库,会让人产生一种错觉,微服务架构就是软件开发中的银弹。
但是,正如大家所了解的,软件研发是一个系统工程,它没有银弹,不能够一招鲜吃遍天。正如当年的CMMI和敏捷方法一样,敏捷虽好,但它不一定能适用于所有的场景,它对组织环境、团队氛围、沟通方式、技术能力这些都是有一些要求的,如果用不好,反而会带来一些负面影响。
那么,我们什么时候需要采用微服务呢?从我个人的经验来看,我认为有三种场景可以考虑使用微服务。
1.规模大(团队超过10人)
2.业务复杂度高(系统超过5个子模块)
3.需要长期演进(项目开发和维护周期超过半年)
3、微服务架构与SOA有什么区别?
首先SOA和微服务架构一个层面的东西,而对于ESB和微服务网关是一个层面的东西,一个谈的是架构风格和方法,一个谈的是实现工具或组件。
1.SOA(Service Oriented Architecture)“面向服务的架构”:他是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间 通过网络调用。
2.微服务架构:其实和 SOA 架构类似,微服务是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + 80%的领域建模思想
--------------------
4、微服务架构的服务粒度怎么确定,服务怎么通信?
5、java中的乐观锁与悲观锁
乐观锁:获得锁后一直持有锁以防本线程再次申请该锁造成无畏的解锁再加锁开销,或者假设没有冲突而去完成同步代码块,如果冲突再循环重试,或者采取申请锁失败后不立刻挂起而是稍微等待再次尝试获取的等待策略减少线程因为挂起、阻塞、唤醒(发生CPU的调度切换)而造成的开销。
偏向锁、轻量级锁(CAS轮询)、自旋锁就是基于上述思路的乐观锁。
悲观锁:悲观锁就是不管是否发生多线程冲突,只要存在这种可能,就每次访问都加锁,加锁就会导致锁之间的争夺,有争夺就会有输赢,输者等待。
syncrhoized是一种独占锁,即:占用该锁的线程才可以执行,申请该锁的线程只能挂起等待,直到占用锁的线程释放锁才唤醒,拿到锁并执行。所以syncrhoized是一种悲观锁,凡是用syncrhoized加了锁的多线程之间都会因锁的争夺而导致挂起、唤醒等开销。
在多线程的加锁机制中,JVM会首先尝试乐观锁,失败后才调用悲观锁。
6、equals与hashcode的区别,存入集合时的判断过程
先来试想一个场景,如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?通常的做法是逐一取出每个元素与要查找的对象一一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是当集合中的元素很多时,譬如有一万个元素,那么逐一的比较效率势必下降很快。于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不同的存储区域(不同的桶中)。如下图所示:
实际的使用中,一个对象一般有key和value,可以根据key来计算它的hashCode。假设现在全部的对象都已经根据自己的hashCode值存储在不同的存储区域中了,那么现在查找某个对象(根据对象的key来查找),不需要遍历整个集合了,现在只需要计算要查找对象的key的hashCode,然后找到该hashCode对应的存储区域,在该存储区域中来查找就可以了,这样效率也就提升了很多。说了这么多相信你对hashCode的作用有了一定的了解,下面就来看看hashCode和equals的区别和联系。
在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashCode()这两个方法的定义和规范:在Java中任何一个对象都具备equals(Object obj)和hashCode()这两个方法,因为他们是在Object类中定义的。 equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。 hashCode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。
下面是官方文档给出的一些说明:
- <span style="font-size:18px;">hashCode 的常规协定是:
- 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
- 以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
- 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
- 当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。</span>
下面是我查阅了相关资料之后对以上的说明做的归纳总结:
1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
6.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
想要弄清楚以上六点,先要知道什么时候需要重写equals和hashCode。一般来说涉及到对象之间的比较大小就需要重写equals方法,但是为什么第一点说重写了equals就需要重写hashCode呢?实际上这只是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode,否则会导致存储数据的不唯一性(存储了两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。但是个人觉得还是重写比较好一点,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以重写了也不要紧,也为后期的修改打了补丁。
下面来看一张对象放入散列集合的流程图:
从上面的图中可以清晰地看到在存储一个对象时,先进行hashCode值的比较,然后进行equals的比较。可能现在你已经对上面的6点归纳有了一些认识。我们还可以通过JDK中得源码来认识一下具体hashCode和equals在代码中是如何调用的。
HashSet.java
- <span style="font-size:18px;"> public boolean add(E e) {
- return map.put(e, PRESENT)==null;
- }</span>
HashMap.java
- <span style="font-size:18px;"> public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }</span>
最后再来看几个测试的例子吧:
测试一:覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性
- <span style="font-size:18px;">public class HashCodeTest {
- public static void main(String[] args) {
- Collection set = new HashSet();
- Point p1 = new Point(1, 1);
- Point p2 = new Point(1, 1);
- System.out.println(p1.equals(p2));
- set.add(p1); //(1)
- set.add(p2); //(2)
- set.add(p1); //(3)
- Iterator iterator = set.iterator();
- while (iterator.hasNext()) {
- Object object = iterator.next();
- System.out.println(object);
- }
- }
- }
- class Point {
- private int x;
- private int y;
- public Point(int x, int y) {
- super();
- this.x = x;
- this.y = y;
- }
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- Point other = (Point) obj;
- if (x != other.x)
- return false;
- if (y != other.y)
- return false;
- return true;
- }
- @Override
- public String toString() {
- return "x:" + x + ",y:" + y;
- }
- }
- </span>
输出结果:
[java] view plain copy
- <span style="font-size:18px;">true
- x:1,y:1
- x:1,y:1
- </span>
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,因此jdk默认不同hashCode值,equals一定返回false,所以直接存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。
测试二:覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性
修改Point类:
[java] view plain copy
- <span style="font-size:18px;">class Point {
- private int x;
- private int y;
- public Point(int x, int y) {
- super();
- this.x = x;
- this.y = y;
- }
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + x;
- result = prime * result + y;
- return result;
- }
- @Override
- public String toString() {
- return "x:" + x + ",y:" + y;
- }
- }
- </span>
输出结果:
[java] view plain copy
- <span style="font-size:18px;">false
- x:1,y:1
- x:1,y:1</span>
原因分析:
(1)当执行set.add(p1)时(1),集合为空,直接存入集合;
(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,默认使用'=='来判断,所以这里equals返回false,jdk认为是不同的对象,所以将p2存入集合。
(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。
综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
(注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,会被舍弃,而在HashMap中插入同一个Key(Value 不同)时,原来的元素会被覆盖。)
测试三:在内存泄露问题
[java] view plain copy
- <span style="font-size:18px;">public class HashCodeTest {
- public static void main(String[] args) {
- Collection set = new HashSet();
- Point p1 = new Point(1, 1);
- Point p2 = new Point(1, 2);
- set.add(p1);
- set.add(p2);
- p2.setX(10);
- p2.setY(10);
- set.remove(p2);
- Iterator iterator = set.iterator();
- while (iterator.hasNext()) {
- Object object = iterator.next();
- System.out.println(object);
- }
- }
- }
- class Point {
- private int x;
- private int y;
- public Point(int x, int y) {
- super();
- this.x = x;
- this.y = y;
- }
- public int getX() {
- return x;
- }
- public void setX(int x) {
- this.x = x;
- }
- public int getY() {
- return y;
- }
- public void setY(int y) {
- this.y = y;
- }
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + x;
- result = prime * result + y;
- return result;
- }
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- Point other = (Point) obj;
- if (x != other.x)
- return false;
- if (y != other.y)
- return false;
- return true;
- }
- @Override
- public String toString() {
- return "x:" + x + ",y:" + y;
- }
- }
- </span>
运行结果:
[java] view plain copy
- <span style="font-size:18px;">x:1,y:1
- x:10,y:10</span>
原因分析:
假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号筒中。这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值得对象是否在集合中。假设修改后的hashCode值为10(仍存在2号桶中),这时查找结果空,jdk认为该对象不在集合中,所以不会进行删除操作。然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决该问题的办法是不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。
总结:
1.hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用。
2.equals和hashCode需要同时覆盖。
3.若两个对象equals返回true,则hashCode有必要也返回相同的int数。
4.若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高哈希表的性能。
5.若两个对象hashCode返回相同int数,则equals不一定返回true。
6.若两个对象hashCode返回不同int数,则equals一定返回false。
7.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
7、java的内存模型,什么数据放在什么区域
程序计数器(线程私有):
是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是Native方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机栈(线程私有):
也是线程私有的。
每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈,动态连接,方法返回地址等。
每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
通常所说的栈,一般是指虚拟机栈中的局部变量表部分。
局部变量表所需的内存在编译期间完成分配。
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。
本地方法栈(线程私有):
和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。
也会抛出StackOverflowError和OutOfMemoryError。
Java堆(线程共享):
被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。
堆可以按照可扩展来实现(通过-Xmx和-Xms来控制)
当堆中没有内存可以分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
方法区(线程共享):
被所有线程共享的一块内存区域。
用于存储已被虚拟机加载的类信息,常量,静态变量等。
这个区域的内存回收目标主要针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,则抛出OutOfMemoryError异常。
在HotSpot虚拟机中,用永久代来实现方法区,将GC分代收集扩展至方法区,但是这样容易遇到内存溢出的问题。
JDK1.7中,已经把放在永久代的字符串常量池移到堆中。
JDK1.8撤销永久代,引入元空间。
8、垃圾回收算法,新生代和年老代用的什么算法,为什么用这个算法?
1. 标记清除算法
标记清除算法是最基础的回收算法,分为标记和清除两个部分:首先标记出所有需要回收的对象,这一过程在可达性分析过程中进行。在标记完之后统一回收所有被标记的对象。
标记清除算法有如下不足:
效率问题:
标记和清除这两个过程的效率不高
空间问题
清除之后会产生大量不连续的内存碎片,内存碎片太多会导致以后的程序运行中无法分配出较大的内存,从内不得不触发另外的垃圾回收。
如上图中,经过标记清除之后,假设有了100M空间,但是这100M是不连续的,最大的一块连续空间可能才10M,所以导致之后程序需要一块20M内存空间时就不得不再进行一次GC来继续清理空间,效率极低。
鉴于标记清除算法有如上的缺陷,所以现在一般是用的是其的变种算法。
2. 复制算法(新生代算法)
2.1 复制算法概念
复制算法是针对Java堆中的新生代内存垃圾回收所使用的回收策略,解决了”标记-清理”的效率问题。
复制算法将堆中可用的新生代内存按容量划分成大小相等的两块内存区域,每次只使用其中的一块区域。当其中一块内存区域需要进行垃圾回收时,会将此区域内还存活着的对象复制到另一块上面,然后再把此内存区域一次性清理掉。
这样做的好处是每次都是对整个新生代一半的内存区域进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :
现在主流的虚拟机,包括HotSpot都是采用的这种回收策略进行新生代内存的回收。
2.2 新生代内存划分
新生代中98%的对象都是”朝生夕死”的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。
当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,则需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
发生在新生代的垃圾回收成为Minor GC,Minor GC又称为新生代GC,因为新生代对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
3. 标记整理算法(老年代回收算法)
复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以在栈的老年代不适用复制算法。
针对老年代对象存活率高的特点,提出了一种称之为”标记-整理算法”。标记过程仍与”标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
发生在老年代的GC称为Full GC,又称为Major GC,其经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
9、数据库事务的隔离级别?
如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:
⑴ 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
⑵ 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
⑶ 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
⑷ 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
以上介绍完事务的四大特性(简称ACID),现在重点来说明下事务的隔离性,当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题:
1,脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下
update account set money=money+100 where name=’B’; (此时A通知B)
update account set money=money - 100 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
2,不可重复读
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……
3,虚读(幻读)
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
现在来看看MySQL数据库为我们提供的四种隔离级别:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
在MySQL数据库中查看当前事务的隔离级别:
select @@tx_isolation;
在MySQL数据库中设置事务的隔离 级别:
set [glogal | session] transaction isolation level 隔离级别名称;
set tx_isolation=’隔离级别名称;’
例1:查看当前事务的隔离级别:
例2:将事务的隔离级别设置为Read uncommitted级别:
或:
记住:设置数据库的隔离级别一定要是在开启事务之前!
如果是使用JDBC对数据库的事务设置隔离级别的话,也应该是在调用Connection对象的setAutoCommit(false)方法之前。调用Connection对象的setTransactionIsolation(level)即可设置当前链接的隔离级别,至于参数level,可以使用Connection对象的字段:
在JDBC中设置隔离级别的部分代码:
后记:隔离级别的设置只对当前链接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效;对于JDBC操作数据库来说,一个Connection对象相当于一个链接,而对于Connection对象设置的隔离级别只对该Connection对象有效,与其他链接Connection对象无关。