什么是事务:事务是程序中一系列严密的操做,全部操做执行必须成功完成,不然在每一个操做所作的更改将会被撤销,这也是事务的原子性(要么成功,要么失败)。java
事务特性分为四个:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持续性(Durability)简称ACID。mysql
一、原子性:事务是数据库的逻辑工做单位,事务中包含的各操做要么都作,要么都不作。程序员
二、一致性:事务执行的结果必须是使数据库从一个一致性状态变到另外一个一致性状态。所以当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。若是数据库系统运行中发生故障,有些事务还没有完成就被迫中断,这些未完成事务对数据库所作的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。算法
三、隔离性:一个事务的执行不能其它事务干扰。即一个事务内部的操做及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。sql
四、持久性:也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操做或故障不该该对其执行结果有任何影响。数据库
并发过程当中会出现的问题:json
丢失更新:是不可重复读的特殊状况。若是两个事物都读取同一行,而后两个都进行写操做,并提交,第一个事物所作的改变就会丢失。数组
脏读:一个事务读取到另外一个事务未提交的更新数据。缓存
幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,形成两次结果不一致,只是另外一个事务在这两次查询中间插入或删除了数据形成的。tomcat
不可重复读:一个事务两次读取同一行的数据,结果获得不一样状态的结果,中间正好另外一个事务更新了该数据,两次结果相异,不可被信任。
事务的隔离级别有4种:
一、未提交读(Read uncommitted)
定义:就是一个事务读取到其余事务未提交的数据,是级别最低的隔离机制。
缺点:会产生脏读、不可重复读、幻读。
二、提交读(Read committed)
定义:就是一个事务读取到其余事务提交后的数据。Oracle默认隔离级别。
缺点:会产生不可重复读、幻读。
三、可重复读(Repeatable read)
定义:就是一个事务对同一份数据读取到的相同,不在意其余事务对数据的修改。MySQL默认的隔离级别。
缺点:会产生幻读。
四、串行化(Serializable)
定义:事务串行化执行,隔离级别最高,牺牲了系统的并发性。
缺点:能够解决并发事务的全部问题。可是效率地下,消耗数据库性能,通常不使用。
页面缓存,用来缓存Web页面的内容片断,包括HTML、CSS 和图片等,多应用于社交网站等。
应用对象缓存,缓存系统做为ORM框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问。
状态缓存,缓存包括Session会话状态及应用横向扩展时的状态数据等,这类数据通常是难以恢复的,对可用性要求较高,多应用于高可用集群。
并行处理,一般涉及大量中间计算结果须要共享。
事件处理,分布式缓存提供了针对事件流的连续查询(continuous query)处理技术,知足实时性需求。
极限事务处理,分布式缓存为事务型应用提供高吞吐率、低延时的解决方案,支持高并发事务请求处理,多应用于铁路、金融服务和电信等领域。
两种数据库的区别:
传统的关系型数据库,数据是以表单为媒介进行存储的。
相比较Mysql,Mongodb以一种直观文档的方式来完成数据的存储。
Mongodb的鲜明特征:
自带GirdFS的分布式文件系统,这也为Mongodb的部署提供了很大便利。
Mongodb内自建了对map-reduce运算框架的支持,虽然这种支持从功能上看还算是比较简单的,至关于MySQL里GroupBy功能的扩展版,不过也为数据的统计带来了方便。
Mongodb在启动后将数据库中得数据以文件映射的方式加载到内存中,若是内存资源至关丰富的话,这将极大的提升数据库的查询速度。
Mongodb的优点:
Mongodb适合那些对数据库具体格式不明确或者数据库数据格式常常变化的需求模型,并且对开发者十分友好。
Mongodb官方就自带一个分布式文件系统,Mongodb官方就自带一个分布式文件系统,能够很方便的部署到服务器机群上。
Mongodb的缺陷:
事务关系支持薄弱。这也是全部NoSQL数据库共同的缺陷,不过NoSQL并非为了事务关系而设计的,具体应用仍是很需求。
稳定性有些欠缺
方便开发者的同时,对运维人员提出了更高的要求。
Mongodb的应用场景:
表结构不明确且数据不断变大:MongoDB是非结构化文档数据库,扩展字段很容易且不会影响原有数据。内容管理或者博客平台等,例如圈子系统,存储用户评论之类的。
更高的写入负载:MongoDB侧重高数据写入的性能,而非事务安全,适合业务系统中有大量“低价值”数据的场景。自己存的就是json格式数据。例如作日志系统。
数据量很大或者未来会变得很大:Mysql单表数据量达到5-10G时会出现明细的性能降级,须要作数据的水平和垂直拆分、库的拆分完成扩展,MongoDB内建了sharding、不少数据分片的特性,容易水平扩展,比较好的适应大数据量增加的需求。
高可用性:自带高可用,自动主从切换(副本集):
不适用的场景:
MongoDB不支持事务操做,须要用到事务的应用建议不用MongoDB。
MongoDB目前不支持join操做,须要复杂查询的应用也不建议使用MongoDB。
在带“_id”插入数据的时候,MongoDB的插入效率其实并不高。若是想充分利用MongoDB性能的话,推荐采起不带“_id”的插入方式,而后对相关字段做索引来查询。
关系型数据库和非关系型数据库的应用场景对比:
关系型数据库适合存储结构化数据,如用户的账号、地址:
这些数据一般须要作结构化查询,好比join,这时候,关系型数据库就要胜出一筹。
这些数据的规模、增加的速度一般是能够预期的。
事务性、一致性。
NoSQL适合存储非结构化数据,如文章、评论:
这些数据一般用于模糊处理,如全文搜索、机器学习。
这些数据是海量的,并且增加的速度是难以预期的。
根据数据的特色,NoSQL数据库一般具备无限(至少接近)伸缩性。
按key获取数据效率很高,可是对join或其余结构化查询的支持就比较差。
1)什么是索引?
索引实际上是一种数据结构,可以帮助咱们快速的检索数据库中的数据。
2)索引具体采用的哪一种数据结构呢?
常见的MySQL主要有两种结构:Hash索引和B+ Tree索引,一般使用的是InnoDB引擎,默认的是B+树。
3)InnoDb内存使用机制?
Innodb体系结构如图所示:
Innodb关于查询效率有影响的两个比较重要的参数分别是innodb_buffer_pool_size,innodb_read_ahead_threshold:
innodb_buffer_pool_size指的是Innodb缓冲池的大小,该参数的大小可经过命令指定innodb_buffer_pool_size 20G。缓冲池使用改进的LRU算法进行管理,维护一个LRU列表、一个FREE列表,FREE列表存放空闲页,数据库启动时LRU列表是空的,当须要从缓冲池分页时,首先从FREE列表查找空闲页,有则放入LRU列表,不然LRU执行淘汰,淘汰尾部的页分配给新页。
innodb_read_ahead_threshold相对应的是数据预加载机制,innodb_read_ahead_threshold 30表示的是若是一个extent中的被顺序读取的page超过或者等于该参数变量的,Innodb将会异步的将下一个extent读取到buffer pool中,好比该参数的值为30,那么当该extent中有30个pages被sequentially的读取,则会触发innodb linear预读,将下一个extent读到内存中;在没有该变量以前,当访问到extent的最后一个page的时候,Innodb会决定是否将下一个extent放入到buffer pool中;能够在Mysql服务端经过show innodb status中的Pages read ahead和evicted without access两个值来观察预读的状况:Innodb_buffer_pool_read_ahead:表示经过预读请求到buffer pool的pages;Innodb_buffer_pool_read_ahead_evicted:表示因为请求到buffer pool中没有被访问,而驱逐出内存的页数。
能够看出来,Mysql的缓冲池机制是能充分利用内存且有预加载机制,在某些条件下目标数据彻底在内存中,也可以具有很是好的查询性能。
4)B+ Tree索引和Hash索引区别?
哈希索引适合等值查询,可是没法进行范围查询。
哈希索引没办法利用索引完成排序。
哈希索引不支持多列联合索引的最左匹配规则。
若是有大量重复键值的状况下,哈希索引的效率会很低,由于存在哈希碰撞问题。
5)B+ Tree的叶子节点均可以存哪些东西吗?
InnoDB的B+ Tree可能存储的是整行数据,也有多是主键的值。
6)这二者有什么区别吗?
在 InnoDB 里,索引B+ Tree的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引。而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引。
7)聚簇索引和非聚簇索引,在查询数据的时候有区别吗?
聚簇索引查询会更快,由于主键索引树的叶子节点直接就是咱们要查询的整行数据了。而非主键索引的叶子节点是主键的值,查到主键的值之后,还须要再经过主键的值再进行一次查询。
8)主键索引查询只会查一次,而非主键索引须要回表查询屡次(这个过程叫作回表)。是全部状况都是这样的吗?非主键索引必定会查询屡次吗?
覆盖索引(covering index)指一个查询语句的执行只用从索引中就可以取得,没必要从数据表中读取。也能够称之为实现了索引覆盖。当一条查询语句符合覆盖索引条件时,MySQL只须要经过索引就能够返回查询所须要的数据,这样避免了查到索引后再返回表操做,减小I/O提升效率。
如,表covering_index_sample中有一个普通索引 idx_key1_key2(key1,key2)。当咱们经过SQL语句:select key2 from covering_index_sample where key1 = 'keytest';的时候,就能够经过覆盖索引查询,无需回表。
9)在建立索引的时候都会考虑哪些因素呢?
通常对于查询几率比较高,常常做为where条件的字段设置索引。
10)在建立联合索引的时候,须要作联合索引多个字段之间顺序,这是如何选择的呢?
在建立多列索引时,咱们根据业务需求,where子句中使用最频繁的一列放在最左边,由于MySQL索引查询会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
因此当咱们建立一个联合索引的时候,如(key1,key2,key3),至关于建立了(key1)、(key1,key2)和(key1,key2,key3)三个索引,这就是最左匹配原则。
11)你知道在MySQL 5.6中,对索引作了哪些优化吗?
索引条件下推:“索引条件下推”,称为 Index Condition Pushdown (ICP),这是MySQL提供的用某一个索引对一个特定的表从表中获取元组”,注意咱们这里特地强调了“一个”,这是由于这样的索引优化不是用于多表链接而是用于单表扫描,确切地说,是单表利用索引进行扫描以获取数据的一种方式。
例若有索引(key1,key2),SQL语句中where key1 = 'XXX' and key2 like '%XXX%'
:
若是没有使用索引下推技术,MySQL会经过key1 = 'XXX'从存储引擎返回对应的数据至MySQL服务端,服务端再基于key2 like 判断是否符合条件。
若是使用了索引下推技术,MySQL首先返回key1='XXX'的索引,再根据key2 like 判断索引是否符合条件,若是符合则经过索引定位数据,若是不符合则直接reject掉。有了索引下推优化,能够在有like条件查询的状况下,减小回表次数。
12)如何知道索引是否生效?
explain显示了MySQL如何使用索引来处理select语句以及链接表。能够帮助选择更好的索引和写出更优化的查询语句。使用方法,在select语句前加上explain就能够了。
13)那什么状况下会发生明明建立了索引,可是执行的时候并无经过索引呢?
在一条单表查询语句真正执行以前,MySQL的查询优化器会找出执行该语句全部可能使用的方案,对比以后找出成本最低的方案。这个成本最低的方案就是所谓的执行计划。优化过程大体以下:
根据搜索条件,找出全部可能使用的索引。
计算全表扫描的代价。
计算使用不一样索引执行查询的代价。
对比各类执行方案的代价,找出成本最低的那一个。
14)为何索引结构默认使用B+Tree,而不是Hash,二叉树,红黑树?
B+tree是一种多路平衡查询树,节点是自然有序的,非叶子节点包含多个元素,不保存数据,只用来索引,叶子节点包含完整数据和带有指向下一个节点的指针,造成一个有序链表,有助于范围和顺序查找。由于非叶子节点不保存数据,因此一样大小的磁盘页能够容纳更多的元素,一样能数据量的状况下,B+tree相比B-tree高度更低,所以查询时IO会更少。
B-tree无论叶子节点仍是非叶子节点,都会保存数据,这样致使在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的状况下要保存大量数据,只能增长树的高度,致使IO操做变多,查询性能变低;
Hash索引底层是基于哈希表,就是以key-value存储数据的结构,多个数据在存储关系上是没有任何顺序关系的。只适合等值查询,不适合范围查询,并且也没法利用索引完成排序,不支持联合索引的最左匹配原则,若是有大量重复键值的状况下,哈希索引效率会很低,由于存在哈希碰撞。
二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),而且IO代价高。
红黑树:树的高度随着数据量增长而增长,IO代价高。
MySQL优化大体能够分为三部分:索引的优化、SQL语句优化和表的优化
索引优化能够遵循如下几个原则:
联合索引最左前缀匹配原则
尽可能把字段长度小的列放在联合索引的最左侧(由于字段越小,一页存储的数据量越大,IO性能也就越好)
order by 有多个列排序的,应该创建联合索引
对于频繁的查询优先考虑使用覆盖索引
前导模糊查询不会使用索引,好比说Like '%aaa%'这种
负向条件不会使用索引,如!=,<>,not like,not in,not exists
索引应该创建在区分度比较高的字段上 通常区分度在80%以上的时候就能够创建索引,区分度能够使用 count(distinct(列名))/count(*)
对于where子句中常用的列,最好设置索引
SQL语句优化,能够经过explain查看SQL的执行计划,优化语句原则能够有:
在where和order by涉及的列上创建合适的索引,避免全表扫描
任何查询都不要使用select * ,而是用具体的字段列表代替
多表链接时,尽可能小表驱动大表,即小表join大表
用exists代替in
尽可能避免在where字句中对字段进行函数操做
数据库表优化
表字段尽量用not null
字段长度固定表查询会更快
将数据库大表按照时间或者一些标志拆分红小表
水平拆分:将记录散列到不一样的表中,每次从分表查询
垂直拆分:将表中的大字段单独拆分到另外一张表,造成一对一的关系
多出一些不用的列,这些列可能正好不在索引的范围以内(索引的好处很少说)select * 杜绝了索引覆盖的可能性,而索引覆盖又是速度极快,效率极高,业界极为推荐的查询方式。(索引覆盖)
数据库须要知道 * 等于什么 = 查数据字典会增大开销(记录数据库和应用程序元数据的目录)。
不须要的字段会增长数据传输的时间,即便 mysql 服务器和客户端是在同一台机器上,使用的协议仍是 tcp,通讯也是须要额外的时间。
大字段,例如很长的 varchar,blob,text。准确来讲,长度超过 728 字节的时候,会把超出的数据放到另一个地方,所以读取这条记录会增长一次 io 操做。(mysql innodb)
影响数据库自动重写优化SQL(相似 Java 中编译 class 时的编译器自动优化) 。(Oracle)
select * 数据库须要解析更多的 对象,字段,权限,属性相关,在 SQL 语句复杂,硬解析较多的状况下,会对数据库形成沉重的负担。
额外的 io,内存和 cpu 的消耗,由于多取了没必要要的列。
用 SELECT * 需谨慎,由于一旦列的个数或顺序更改,就有可能程序执行失败。
Java实现多线程有几种方式?
有三种方式:
继承Thread类,并重写run方法。
实现Runnable接口,并重写run方法。
实现Callable接口,并重写run方法,并使用FutureTask包装器。
线程的生命周期
一、新建状态(New):新建立了一个线程对象。
二、就绪状态(Runnable):线程对象建立后,其余线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
三、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
四、阻塞状态(Blocked):阻塞状态是线程由于某种缘由放弃CPU使用权,暂时中止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的状况分三种:
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
其余阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程从新转入就绪状态。(注意,sleep是不会释放持有的锁)
五、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
start()方法和run()方法的区别?
start()方法会使得该线程开始执行,java虚拟机会去调用该线程的run()方法。
经过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并无运行,一旦获得cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。
run()方法只是类的一个普通方法而已,若是直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径仍是只有一条,仍是要顺序执行,仍是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
Runnable接口和Callable接口的区别?
Runnable接口中的run()方法的返回值是void,它作的事情只是纯粹地去执行run()方法中的代码而已。
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合能够用来获取异步执行的结果。
这实际上是颇有用的一个特性,由于多线程相比单线程更难、更复杂的一个重要缘由就是由于多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候咱们指望的数据是否已经赋值完毕?没法得知,咱们能作的只是等待这条多线程的任务执行完毕而已。而Callable + Future/FutureTask却能够获取多线程运行的结果,能够在等待时间太长没获取到须要的数据的状况下取消该线程的任务,真的是很是有用。
volatile关键字
volatile基本介绍:volatile能够当作是synchronized的一种轻量级的实现,但volatile并不能彻底代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。可见性即用volatile关键字修饰的成员变量代表该变量不存在工做线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其余线程的可见性。另外,使用volatile还能确保变量不能被重排序,保证了有序性。
当一个变量定义为volatile以后,它将具有两种特性:
①保证此变量对全部线程的可见性:当一条线程修改了这个变量的值,新值对于其余线程能够说是能够当即得知的。Java内存模型规定了全部的变量都存储在主内存,每条线程还有本身的工做内存,线程的工做内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读取主内存中的变量。
②禁止指令重排序优化:
volatile boolean isOK = false;
//假设如下代码在线程A执行
A.init();
isOK=true;
//假设如下代码在线程B执行
while(!isOK){
sleep();
}
B.init();
A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才可以进行本身的初始化。这里的前后关系依赖于isOK这个变量。若是没有volatile修饰isOK这个变量,那么isOK的赋值就可能出如今A.init()以前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们以前造成的那种依赖关系,可能就会出错。
volatile使用场景:
若是正确使用volatile的话,必须依赖下如下种条件:
对变量的写操做不依赖当前变量的值。
该变量没有包含在其余变量的不变式中。
在如下两种状况下都必须使用volatile:
状态的改变。
读多写少的状况。
什么是线程安全?
若是你的代码在多线程下执行和在单线程下执行永远都能得到同样的结果,那么你的代码就是线程安全的。
线程安全的级别:
1)不可变:像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新建立一个,所以这些不可变对象不须要任何同步手段就能够直接在多线程环境下使用。
2)绝对线程安全:无论运行时环境如何,调用者都不须要额外的同步措施。要作到这一点一般须要付出许多额外的代价,Java中标注本身是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet。
3)相对线程安全:相对线程安全也就是咱们一般意义上所说的线程安全,像Vector这种,add、remove方法都是原子操做,不会被打断,但也仅限于此,若是有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的状况下都会出现ConcurrentModificationException,也就是fail-fast机制。
4)线程非安全:ArrayList、LinkedList、HashMap等都是线程非安全的类。
sleep方法和wait方法有什么区别?
原理不一样:sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其余线程,等到计时时间一到,此线程会自动苏醒。而wait()方法是Object类的方法,用于线程间的通讯,这个方法会使当前拥有该对象锁的进程等待,直到其余线程用调用notify()或notifyAll()时才苏醒过来,开发人员也能够给它指定一个时间使其自动醒来。
对锁的处理机制不一样:因为sleep()方法的主要做用是让线程暂停一段时间,时间一到则自动恢复,不涉及线程间的通讯,所以调用sleep()方法并不会释放锁。而wait()方法则不一样,当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其余synchronized数据可被别的线程使用。
使用区域不一样:wait()方法必须放在同步控制方法或者同步语句块中使用,而sleep方法则能够放在任何地方使用。
sleep()方法必须捕获异常,而wait()、notify()、notifyAll()不须要捕获异常。在sleep的过程当中,有可能被其余对象调用它的interrupt(),产生InterruptedException异常。
因为sleep不会释放锁标志,容易致使死锁问题的发生,通常状况下,不推荐使用sleep()方法,而推荐使用wait()方法。
写一个会致使死锁的程序。
public class MyThread{
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (lock1){
System.out.println("thread1 get lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("thread1 get lock2");
}
System.out.println("thread1 end");
}
}).start();
new Thread(()->{
synchronized (lock2){
System.out.println("thread2 get lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println("thread2 get lock1");
}
System.out.println("thread2 end");
}
}).start();
}
}
一、类加载过程:加载->连接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
具体过程以下:
1)加载:首先经过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个表明这个类的Class对象,做为方法区这些数据的访问入口。总的来讲就是查找并加载类的二进制数据。
2)连接:
验证:确保被加载类的正确性。
准备:为类的静态变量分配内存,并将其初始化为默认值。
解析:把类中的符号引用转换为直接引用。
符号引用即用字符串符号的形式来表示引用,其实被引用的类、方法或者变量尚未被加载到内存中。
直接引用则是有具体引用地址的指针,被引用的类、方法或者变量已经被加载到内存中。
直接引用能够是:
直接指向目标的指针。(我的理解为:指向对象,类变量和类方法的指针)
相对偏移量。(指向实例的变量,方法的指针)
一个间接定位到对象的句柄。
为何要使用符号引用?
符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高。那为何要用符号引用呢?这是由于类加载以前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,因此只能用符号引用来表示,固然,符号引用是要遵循java虚拟机规范的。
还有一种状况须要用符号引用,就例如前文举得变量的符号引用的例子,是为了逻辑清晰和代码的可读性。
3)为类的静态变量赋予正确的初始值。
二、类的初始化
1)类何时才被初始化:
建立类的实例,也就是new一个对象。
访问某个类或接口的静态变量,或者对该静态变量赋值。
调用类的静态方法。
反射(Class.forName(“com.lyj.load”))。
初始化一个类的子类(会首先初始化子类的父类)。
JVM启动时标明的启动类,即文件名和类名相同的那个类。
2)类的初始化顺序
若是这个类尚未被加载和连接,那先进行加载和连接
假如这个类存在直接父类,而且这个类尚未被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
总的来讲,初始化顺序依次是:
(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;
若是有父类,则顺序是:父类的静态变量 –> 父类的静态代码块 –> 子类的静态变量 –> 子类的静态代码块 –> 父类的非静态变量 –> 父类的非静态代码块 –> 父类的构造方法 –> 子类的非静态变量 –> 子类的非静态代码块 –> 子类的构造方法。
三、类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,而后在堆区建立一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:
类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。加载类的方式有如下几种:
从本地系统直接加载。
经过网络下载.class文件。
从zip,jar等归档文件中加载.class文件。
从专有数据库中提取.class文件。
将Java源文件动态编译为.class文件(服务器)。
四、加载器
JVM的类加载是经过ClassLoader及其子类来完成的,类的层次关系和加载顺序能够由下图来描述:
加载器介绍:
1)BootstrapClassLoader(启动类加载器):
负责加载JAVA_HOME中jre/lib/rt.jar里全部的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。
2)ExtensionClassLoader(标准扩展类加载器):
负责加载java平台中扩展功能的一些jar包,包括JAVAHOME中jre/lib/rt.jar里全部的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。2)ExtensionClassLoader(标准扩展类加载器):负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。载System.getProperty(“java.ext.dirs”)所指定的路径或jar。
3)AppClassLoader(系统类加载器):
负责加载classpath中指定的jar包及目录中class。
4)CustomClassLoader(自定义加载器):
属于应用程序根据自身须要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。
类加载器的顺序
加载过程当中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只全部ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
在加载类时,每一个类加载器会将加载任务上交给其父,若是其父找不到,再由本身去加载。
Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null。
五、类加载器之双亲委派模型
所谓的双亲委派模型指除了启动类加载器之外,其他的加载器都有本身的父类加载器,而在工做的时候,若是一个类加载器收到加载请求,他不会立刻加载类,而是将这个请求向上传递给他的父加载器,看父加载器能不能加载这个类,加载的原则就是优先父加载器加载,若是父加载器加载不了,本身才能加载。
由于有了双亲委派模型的存在,相似Object类重复屡次的问题就不会存在了,由于通过层层传递,加载请求最终都会被Bootstrap ClassLoader所响应。加载的Object对象也会只有一个。而且面对同一JVM进程多版本共存的问题,只要自定义一个不向上传递加载请求的加载器就好啦。
Java内存区域划分
咱们先来看看Java的内存区域划分状况,以下图所示:
私有内存区的区域名和相应的特性以下表所示:
虚拟机栈中的局部变量表里面存放了三个信息:
各类基本数据类型(boolean、byte、char、short、int、float、long、double)。
对象引用(reference)。
returnAddress地址。
这个returnAddress和程序计数器有什么区别?前者是指示JVM的指令执行到了哪一行,后者是指你的代码执行到哪一行。
共享内存区(接下来主要讲jdk1.7)的区域名和相应的特性以下表所示:
哪些内存须要回收?
私有内存区伴随着线程的产生而产生,一旦线程停止,私有内存区也会自动消除,所以咱们在本文中讨论的内存回收主要是针对共享内存区。
Java堆
新生代GC(Minor GC):指发生在新生代的垃圾收集动做,由于Java对象大都具有朝生夕灭的特性,因此Minor GC很是频繁,通常回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,常常会伴随至少一次的Minor GC (但非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度通常会比Minor GC慢10倍以上。
新生代:刚刚新建的对象在Eden中,经历一次Minor GC, Eden中的存活对象就被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC, Eden和S0中的存活对象会被复制送入第二块survivor space S1。S0和Eden被清空,而后下一轮S0与S1交换角色,如此循环往复。若是对象的复制次数达到16次,该对象就被送到老年代中。
为何新生代内存须要有两个Sruvivor区:
先不去想为何有两个Survivor区,第一个问题是,设置Survivor区的意义在哪里?
若是没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(由于Major GC通常伴随着Minor GC,也能够看作触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是很是可观的,这一点会影响大型程序的执行和响应速度,更不要说某些链接会由于超时发生链接错误了。那咱们来想一想在没有Survivor的状况下,有没有什么解决办法,能够避免上述状况:
显而易见,没有Survivor的话,上述两种解决方案都不能从根本上解决问题。咱们能够获得第一条结论:Survivor的存在乎义,就是减小被送到老年代的对象,进而减小Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,下面咱们来分析一下。为何一个Survivor区不行?
第一部分中,咱们知道了必须设置Survivor区。假设如今只有一个survivor区,咱们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,若是此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就致使了内存碎片化。
那么,瓜熟蒂落的,应该创建两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程很是重要,由于这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,而后下一轮S0与S1交换角色,如此循环往复。若是对象的复制次数达到16次,该对象就会被送到老年代中。
参考文章:https://blog.csdn.net/antony9118/article/details/51425581
老年代:若是某个对象经历了几回垃圾回收以后还存活,就会被存放到老年代中。老年代的空间通常比新生代大。
这个流程以下图所示:
何时回收?
Java并无给咱们提供明确的代码来标注一块内存并将其回收。或许你会说,咱们能够将相关对象设为null或者用System.gc()。然而,后者将会严重影响代码的性能,由于每一次显示调用system.gc()都会中止全部响应,去检查内存中是否有可回收的对象,这会对程序的正常运行形成极大威胁。
另外,调用该方法并不能保障JVM当即进行垃圾回收,仅仅是通知JVM要进行垃圾回收了,具体回收与否彻底由JVM决定。
生存仍是死亡
可达性算法:这个算法的基本思路是经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的。
二次标记:在可达性分析算法中被判断是对象不可达时不必定会被垃圾回收机制回收,由于要真正宣告一个对象的死亡,必须经历两次标记的过程。
若是发现对象不可达时,将会进行第一次标记,此时若是该对象调用了finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,若是在此队列中该对象没有成功拯救本身(拯救本身的方法是该对象有没有被从新引用),
那么GC就会对F-Queue队列中的对象进行小规模的第二次标记,一旦被第二次标记的对象,将会被移除队列并等待被GC回收,因此finalize()方法是对象逃脱死亡命运的最后一次机会。
在Java语言中,可做为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即通常说的Native方法)引用的对象。
GC的算法
引用计数法(Reference Counting):
给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,可是不能解决互相循环引用的问题。
优势:
及时回收无效内存,实时性高。
垃圾回收过程当中无需挂起。
没有全局扫描,性能高。
缺点:
对象建立时须要更新引用计数器,耗费一部分时间。
浪费CPU资源,计数器统计须要实时进行。
没法解决循环引用问题,即便对象无效仍不会被回收。
标记-清除(Mark-Sweep)算法:
分为两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象(后续的垃圾回收算法都是基于此算法进行改进的)。
缺点:效率问题,标记和清除两个过程的效率都不高;空间问题,会产生不少碎片。
复制算法:
将可用内存按容量划分为大小相等的两块,每次只用其中一块。当这一块用完了,就将还存活的对象复制到另一块上面,而后把原始空间所有回收。高效、简单。
缺点:将内存缩小为原来的一半。
标记-整理(Mark-Compat)算法
标记过程与标记-清除算法过程同样,但后面不是简单的清除,而是让全部存活的对象都向一端移动,而后直接清除掉端边界之外的内存。
分代收集(Generational Collection)算法
新生代中,每次垃圾收集时都有大批对象死去,只有少许存活,就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
老年代中,其存活率较高、没有额外空间对它进行分配担保,就应该使用“标记-整理”或“标记-清除”算法进行回收。
增量回收GC和并行回收GC这里就不作具体介绍了,有兴趣的朋友能够自行了解一下。
垃圾收集器
Serial收集器:单线程收集器,表示在它进行垃圾收集时,必须暂停其余全部的工做线程,直到它收集结束。"Stop The World"。
ParNew收集器:实际就是Serial收集器的多线程版本。
并发(Parallel):指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。
并行(Concurrent):指用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。
Parallel Scavenge收集器:该收集器比较关注吞吐量(Throughout)(CPU用于用户代码的时间与CPU总消耗时间的比值),保证吞吐量在一个可控的范围内。
CMS(Concurrent Mark Sweep)收集器:CMS收集器是一种以获取最短回收停顿时间为目标的垃圾收集器,是基于“标记——清除”算法实现的。
其回收过程主要分为四个步骤:
初始标记:标记一下GC Roots能直接关联到的对象,速度很快。
并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时。
从新标记:修正并发标记期间因用户程序继续运做致使的标记变更,速度比较快。
并发清除:对标记的对象进行统一回收处理,比较耗时。
因为初始标记和从新标记速度比较快,其它工做线程停顿的时间几乎能够忽略不计,因此CMS的内存回收过程是与用户线程一块儿并发执行的。初始标记和从新标记两个步骤须要Stop the world;并发标记和并发清除两个步骤可与用户线程并发执行。“Stop the world”意思是垃圾收集器在进行垃圾回收时,会暂停其它全部工做线程,直到垃圾收集结束为止。
CMS的缺点:
对CPU资源很是敏感;也就是说当CMS开启垃圾收集线程进行垃圾回收时,会占用部分用户线程,若是在CPU资源紧张的状况下,会致使用户程序的工做效率降低。
没法处理浮动垃圾致使又一次FULL GC的产生;因为CMS并发回收垃圾时用户线程同时也在运行,伴随用户线程的运行天然会有新的垃圾产生,这部分垃圾出如今标记过程以后,CMS没法在当次收集过程当中进行回收,只能在下一次GC时在进行清除。因此在CMS运行期间要确保内存中有足够的预留空间用来存放用户线程的产生的浮动垃圾,不容许像其它收集器同样等到老年代区彻底填满了以后再进行收集;那么当内存预留的空间不足时就会产生又一次的FULL GC来释放内存空间,因为是经过Serial Old收集器进行老年代的垃圾收集,因此致使停顿的时间变长了(系统有一个阈值来触发CMS收集器的启动,这个阈值不容许过高,过高反而致使性能下降)。
标记——清除算法会产生内存碎片;若是产生过多的内存碎片时,当系统虚拟机想要再分配大对象时,会找不到一块足够大的连续内存空间进行存储,不得不又一次触发FULL GC。
G1(Garbage First)收集器:G1收集器是一款成熟的商用的垃圾收集器,是基于“标记——整理”算法实现的。
其回收过程主要分为四个步骤:
初始标记:标记一下GC Roots能直接关联到的对象,速度很快。
并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时。
最终标记:修正并发标记期间因用户程序继续运做致使的标记变更,速度比较快。
筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。
G1收集器的特色:
并发与并行:机型垃圾收集时能够与用户线程并发运行。
分代收集:能根据对象的存活时间采起不一样的收集算法进行垃圾回收。
不会产生内存碎片:基于标记——整理算法和复制算法保证不会产生内存空间碎片。
可预测的停顿:G1除了追求低停顿时间外,还能创建可预测的停顿时间模型,便于用户的实时监控。
CMS收集器与G1收集器的区别:
CMS采用标记——清除算法会产生空间碎片,G1采用标记——整理算法不会产生空间碎片。
G1能够创建可预测的停顿时间模型,而CMS则不能。
JDK 1.8 JVM的变化
一、为何取消方法区
它在启动时固定大小,很难进行调优,而且FullGC时会移动类元信息。
类及方法的信息等比较难肯定大小,所以对永久代的大小指定比较困难。
在某些场景下,若是动态加载类过多,容易形成Perm区的OOM。
字符串存在方法区中,容易出现性能问题和内存溢出。
永久代GC垃圾回收效率偏低。
二、JDK 1.8里Perm区中的全部内容中字符串常量移至堆内存,其余内容如类元信息、字段、静态属性、方法、常量等都移动到元空间内。
三、元空间
元空间(MetaSpace)不在堆内存上,而是直接占用的本地内存。所以元空间的大小仅受本地内存限制
也可经过参数来设定元空间的大小:
-XX:MetaSpaceSize 初始元空间大小
-XX:MaxMetaSpaceSize 最大元空间大小
除了上面两个指定大小的选项之外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC以后,最小的Metaspace剩余空间容量的百分比,减小为分配空间所致使的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC以后,最大的Metaspace剩余空间容量的百分比,减小为释放空间所致使的垃圾收集。
元空间的特色:
每一个加载器有专门的存储空间。
不会单独回收某个类。
元空间里的对象的位置是固定的。
若是发现某个加载器再也不存货了,会把相关的空间整个回收。
性能优化:
减小new对象。每次new对象以后,都要开辟新的内存空间。这些对象不被引用以后,还要回收掉。所以,若是最大限度地合理重用对象,或者使用基本数据类型替代对象,都有助于节省内存。
多使用局部变量,减小使用静态变量。局部变量被建立在栈中,存取速度快。静态变量则是存储在堆内存中。
避免使用finalize,该方法会给GC增添很大的负担。
若是是单线程,尽可能使用非多线程安全的,由于线程安全来自于同步机制,同步机制会下降性能。例如,单线程程序,能使用HashMap,就不要使用HashTabl。同理,尽可能减小使用synchronized。
用移位符号替代乘除号。好比:a*8应该写做a<<3。
对于常常反复使用的对象使用缓存。
尽可能使用基本类型而不是包装类型,尽可能使用一维数组而不是二维数组。
尽可能使用final修饰符,final表示不可修改,访问效率高。
单线程下(或者是针对于局部变量),字符串尽可能使用StringBuilder,比StringBuffer要快。
尽可能使用StringBuffer来链接字符串。这里须要注意的是,StringBuffer的默认缓存容量是16个字符,若是超过16,append方法调用私有的expandCapacity()方法,来保证足够的缓存容量。所以,若是能够预设StringBuffer的容量,避免append再去扩展容量。
当基本类型包装类与基本类型值进行==运算时,包装类会自动拆箱。即比较的是基本类型值。
具体实现上,是调用了Integer.intValue()方法实现拆箱。
int a = 1;Integer b = 1;Integer c = new Integer(1);System.out.println(a == b); //trueSystem.out.println(a == c); //trueSystem.out.println(c == b); //falseInteger a = 1;会调用这个 Integer a = Integer.valueOf(1);Integer已经默认建立了数值【-128到127】的Integer常量池Integer a = -128;Integer b = -128;System.out.println(a == b); //trueInteger a = 128;Integer b = 128;System.out.println(a == b); //falseJava的数学计算是在内存栈里操做的c1 + c2 会进行拆箱,比较仍是基本类型int a = 0;Integer b1 = 1000;Integer c1 = new Integer(1000);Integer b2 = 0;Integer c2 = new Integer(0);System.out.println(b1 == b1 + b2); //trueSystem.out.println(c1 == c1 + c2); //trueSystem.out.println(b1 == b1 + a); //trueSystem.out.println(c1 == c1 + a); //true