1.关于HashCode
不能根据hashCode值判断两个对象是否相等,但可以直接根据hashCode值判断两个对象不相等。
如果两个对象的hashCode值不等,一定是不同的对象,要判断两个对象是否真正相等,必须通过equals()方法
如果调用equals()方法得到的结果为true,则两个对象的hashCode值一定相等
如果equals()方法得到的结果为false,则两个对象的hashCode值不一定不同
如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
如果两个对象的hashcode值相等,则equals方法得到的结果未知。
在重写equals()方法时,必须重写hashCode方法,让equals()方法和hashCode()方法始终在逻辑上保持一致性
hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable
Java里静态语句块是优先对象存在,也就是优先于构造方法存在,我们通常用来做只创建一次对象使用,类似于单列模式而且执行的顺序是:
父类静态语句块 -> 子类静态语句块 -> 父类非静态代码块、父类构造方法 -> 子类非静态代码块构造方法
静态语句块可以看作在类加载的时候执行,且只执行一次
Java对象初始化顺序:
静态代码块内容先执行(父>子),接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法
首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,
父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法
2.JVM内存模型
程序计数器
用来指示 执行哪条指令的
如果线程执行的是非native方法,则程序计数器保持的是当前需要执行的指令的地址
如果线程执行的是native方法,程序计数器中的值是undefined
Java栈
存放一个个的栈帧,每个栈帧对应一个被调用的方法。
栈帧中包括:局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址和一些额外的附加信息
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压入栈,当方法执行完后,便会将栈帧出栈
线程当前执行的方法所对应的栈帧位于Java栈的顶部
栈帧
局部变量表
操作数栈
指向运行时常量池的引用
方法返回地址
额外的附加信息
堆
存储对象本身以及数组
方法区 --- 线程共享区域
1、存储了每个类的信息(包括:类名称、方法信息、字段信息)、静态变量、常量以及编译后的代码等
2、常量池
本地方法栈
3.Hash表
散列表,能快速定位到想要查找的记录,而不是与表中存在的记录的关键字比较来进行查找。
Hash表的设计,采用了函数映射思想,将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找
eg:
张三 13980593357
李四 15828662334
王五 13409821234
张帅 13890583472
Hash表能通过"李四"这个信息直接获取到该记录在表中的位置,复杂度为O(1)
原理:
Hash表采用一个映射函数
f:key --> address
将关键字key映射到该记录在表中的存储位置
说明:
1.映射关系 称为 Hash函数
2.通过Hash函数和关键字计算出来的存储位置(hash表中的存储位置,不是实际的物理地址) 称为 Hash地址
联系人信息采用Hash表存储时,当想找 "李四" 的信息时,直接根据 "李四" 和 Hash函数,计算出Hash地址即可
4.Java线程安全的本质:线程中并没有存放任何对象数据,而是在执行时,去主内存(堆)中去同步数据,所有的对象数据都存在JVM的堆中,因此需要对资源进行共享锁!!!
堆 --- JVM的核心数据存储区 --- 线程共享的主内存
堆中为JVM的所有对象分配了内存空间用以存储和维护变量值等
栈 --- 线程私有的内存区,由栈帧(线程栈)组成,存放8中基本数据类型和对象引用
每个线程都会生成一个自有的线程栈,线程栈中用存储了该线程的基本数据常量,变量值,以及对象长变量的引用
每个线程执行时,根据代码顺序,压栈 栈帧(栈内存)
对象变量在线程执行时的过程:!!! --- 由JVM内存模型决定
1.线程根据栈中的引用去堆上同步该对象数据下来,然后在线程自己的内存中进行操作
2.操作之后再将线程栈撒花姑娘的运算结果同步到堆(主内存)中
3.多线程时,因为每个线程都操作自己从主内存(JVM堆)中同步过来的数据,如果不加锁,会导致线程安全问题(数据提交到主内存时不一致)
5.堆 --- JVM中所有对象的内存空间 分为: Young Gen, Old Gen
Young Gen 又分为:Eden区和两个大小相同的Survivor区(from 和 to)
Eden和Survivor默认比例 8:1 由 -XX:SurvivorRation设置
堆大小 -Xmx -Xms 设置
Young Gen -Xmn 设置
-XX:NewSize和-XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
Minor GC --- 发生在新生代的垃圾回收,Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快,
年轻代的GC使用复制算法(将内存分为两块,每次只用其中一块,当这块内存用完,就将还活着的对象复制到另外一块上面,复制算法不会产生内存碎片)
Full GC --- 发生在年老代的GC, Full GC比较慢,尽量避免
新创建对象都会被分配到Eden区(一些大对象特殊处理),当Eden区满则进行Minor GC,
这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区, 对象在Survivor区中每熬过一次Minor GC,年龄增加1岁,
当年龄增加到一定程度后(默认15岁),会被移动到年老代中,
当年老代满时,经常Full GC
线程Stack 每个线程独有的操作数栈局部变量表方法入口 -Xss 设置
方法区 -XX:PermSize和-XX:MaxPermSize设置
6.JVM class类加载加载机制 --- JVM把描述类的数据从class加载到内存,并对数据进行校验,转化解析和初始化,最终得到可被虚拟机直接使用的Java类型
类装载器: 寻找类的字节码文件, 并构造出类在JVM内部表示的对象组件
JVM类加载器把一个类装入JVM过程:
(1) 装载:查找和导入Class文件;
(2) 链接:把类的二进制数据合并到JRE中;
(a)校验:检查载入Class文件数据的正确性,确保被加载类信息符合JVM规范;
(b)准备:给类的静态变量分配存储空间,并将其初始化为默认值;
(c)解析:将虚拟机常量池中符号引用转成直接引用;
(3) 初始化:对类的静态变量,静态代码块执行初始化操作,为类的静态变量赋初始值
说明:
1.类加载的双亲委托机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务交给父类加载器,父类加载器又将加载任务向上委托,直到最高层的父类加载器,
如果最高层的父类加载器可以完成类加载任务,就成功返回,否则向下传递加载任务,由其子类加载器进行加载
2.初始化步骤
1.如果类没有被加载和链接,则先进行加载和链接
2.如果类存在直接父类,并且父类未被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类
3.如果存在static块,一次执行这些初始化语句
4.static块,会在类第一次被使用时执行(调用类的静态变量,初始化对象,Class.forName等)
7.正则表达式总计:
^ 和$表示以字符串开始和以字符串结尾。例:^abc 表示必须以abc开始(如:abcd,abcefd),abc$ 表示必须以abc结尾(如:);^abc$ 只能是abc(abc是个整体,abcabc不匹配) ;abc 表示包含abc的字符串
* 和 + 和 ? 分别表示出现0次或多次,1次或多次,0次或1次。例:abc*表示有0个或多个abc,其他两个同理
上面的*+?完全可以用范围代替,abc{2}表示ab后面有至少两个c,如abcc,dfdabccccc都是符合的;abc{2}$ 只有以abcc结尾的符合,如343abcc
abc{1,2} 表示ab后面跟着1或2个c;
abc{3,} 表示ab后面跟着至少3个c; {,3}这种是不正确的
| 或运算 ab|cd 表示字符串里有ab或者cd;
. 可以替换任意字符
下面是几种是需要记住的
"[ab]":表示一个字符串有一个"a"或"b"(相当于"a|b");
"[a-d]":表示一个字符串包含小写的'a'到'd'中的一个(相当于"a|b|c|d"或者"[abcd]");
"^[a-zA-Z]":表示一个以字母开头的字符串;
"[0-9]%":表示一个百分号前有一位的数字;
",[a-zA-Z0-9]$":表示一个字符串以一个逗号后面跟着一个字母或数字结束。
下面看看具体的实例,比如我今天做的:一个输入框,可以输入数字,也可以输入多个数字用逗号隔开,或者两个数字用~分隔。
我写的正则表达式 : ((^[0-9]+[~]?)?|^([0-9]+[,])+)[0-9]+$
8.JVM中的GC垃圾回收和内存分配策略 ---- JVM高级 参考:
1.判断回收对象
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值+1,当引用失效,计数器值-1,任何时刻计数器为0的对象就不可能再被使用
缺点:
无法解决对象的循环依赖问题!!!
2.可达性算法 --- JVM使用的GC判断算法
通过一系列称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链,
当一个对象到 GC Roots 没有任何引用链相连时(即:GC Roots到这个对象不可达),则此对象是不可用的
Java中可作为 GC Roots的对象包括以下几种:
1.虚拟机栈(栈帧的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(Native方法)引用的对象
2.JVM的垃圾回收机制
1.几个概念:
内存分代 新生代(Eden + 2 Survivor )、老年代、永久代
新创建的对象分配在Eden区,经历一次Minor GC后被移到 Survivor1区,再经历一次Minor GC被移到Survivor2区,直到升至老年代
大对象(长字符串或大数组)可能直接存放到老年代
对象创建都在堆上,类信息、常量、静态变量存储在方法区,堆和方法区是线性共享的
GC由守护线程执行,在从内存回收一个对象之前,会调用对象的finalize()方法
GC的触发由JVM堆内存大小决定,System.gc()和Runtime.gc()会向JVM发送执行GC的请求,但JVM不保证一定会执行GC
堆中没有内存创建新对象时,会抛出 OutOfMemoryError
2.GC回收什么对象?
可达性算法 --- 根搜索法
--- 通过一系列称为GC Roots的对象作为起始点,从GC Roots开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用,可以被回收了
程序把所有的引用关系看做一张图,从一个节点 GC Roots开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点被认为是没有被引用的节点,可被回收
--- 可被作为GC Roots的对象
1.虚拟机栈中引用的对象(本地变量表)
2.方法静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈用引用的对象(Native对象)
引用计数法
--- 可能有循环依赖问题,导致计数器不为0,对象永远无法被回收
多次扫描,而都不可达的对象,经历几次扫描后会被回收
3.垃圾回收算法: 参考:
任何垃圾回收算法,只做2件基本事情:
1.发现无用可被回收对象 (可达性算法)
2.回收被无用对象占用的空间,使得该空间可被再次使用
1.标记-清除算法:
从GC Roots根集合进行扫描,对存活的对象进行标记,标记完成后,在扫描整个空间中未被标记的对象,进行回收
优点:不必移动对象,在存活对象比较多的情况下极为高效
缺点:GC时会暂停整个应用,容易造成不连续的内存碎片
2.复制算法:
为了解决内存碎片问题而产生的一种算法。它的整个过程可以描述为:标记所有的存活对象;通过重新调整存活对象位置来缩并对象图;更新指向被移动了位置的对象的指针
将内存划分为大小相等的2块,每次只是用其中一块,当一块用完后,将还存活的对象复制到另一块内存上,然后把已使用过的内存空间一次清理掉,
这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效
优点:不会造成内存碎片
缺点:内存缩小为原来的一半,需要移动对象
3.标记-整理算法:
先进行 标记-清除算法,然后在清理完无用对象后,将所有存活的对象移动到一端,并更新引用其对象的指针
4.分代算法:
基于对象生命周期分析得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同的生命周期使用不同的算法
年轻代: --- Minor GC, 对象生命周期短 使用: Serial、PraNew、Parallel Scavenge 收集器
1.所有新生成对象,目标是尽快收集掉生命周期短的对象
2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
Eden : Survivor0:Survivor1 = 8:1:1
3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
老年代: --- Full GC 对象生命周期长 使用:Serial Old、Parallel Old、CMS收集器
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
永久代: --- 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响
4.GC实现 --- 垃圾收集器
Serial --- 复制算法
新生代单线程收集器,标记和清理都是单线程,优点:简单高效
Serial Old --- 标记-整理算法
老年代单线程收集器
ParNew --- 停止复制算法
Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
Parallel Scavenge ---
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景
Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先
CMS --- 标记清理算法
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择
5.GC执行机制
Minor GC: 新对象在年轻代的Eden区申请内存失败时,触发Minor GC --- 年轻代
--- 只对年轻代的Eden去进行,不影响老年代
--- 频繁进行
--- 选用速度快、效率高的算法,使Eden尽快空闲
Full GC: 对整个堆进行整理,包括 年轻代、老年代、永久代
--- 对整个堆进行回收,速度比 Minor GC 慢
--- 尽量减少 Full GC次数
--- 对象JVM调优过程,大部分是针对Full GC的调节
可能导致Full GC的原因:
1.老年代满
2.持久代满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化
6.容易出现泄露的地方:
1.静态集合类(HashMap、Vector),这些静态变量生命周期和应用程序一样,所有的对象也不能被释放
2.各种连接、数据了连接、网络连接、IO连接没被显示调用close(),不被GC回收导致内存泄露
3.监听器,在释放对象的同时没有相应删除监听器时可能导致内存泄露
9.JVM内存模型
内存模型:描述了程序中各个变量(实例域、静态域和数组元素)直接的关系,以及在实际计算机系统中将变量存储到内存、从内存中取出变量这样的底层细节
JVM中存在一个主存,Java中所有对象成员变量都存储在主存中,对于所有线程是共享的,每条线程都有自己的工作内存,
工作内存中保存的是主存中某些对象成员变量的拷贝,线程对所有成员变量的操作都是在工作内存中进行,然后同步到主存,线程之间无法相互直接访问,变量传递都需要通过主存
JMM(Java内存模型,Java Memory Model简称)是控制Java线程之间、线程和主存之间通信的协议。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(local memory),
本地内存中存储了该线程以读/写共享变量的副本,线程在本地私有内存中操作完后,要将数据同步到主内存
Java内存模型中规定了:
所有变量都存储在主内存中,每个线程都有自己的工作内存,线程的工作内存中使用到的变量,是主内存的副本拷贝,
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量,操作完后,要将数据同步到主内存
不同的线程之间工作内存是独立的,线程间变量值的传递均需要在主内存来完成
特别注意:
(根据Java虚拟机规范的规定,volatile变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性规定——
从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所有看起来如同直接在主内存中读写访问一般,因此这里的描述对于volatile也不例外)。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成
8种内存间的交互操作:
Java内存模型定义了8种操作来完成主内存与工作内存之间交互的实现细节
1.lock(锁定)
作用于主内存的变量,它把一个变量标识为一条线程独占的状态
2.unlock(解锁)
作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3.read(读取)
作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
4.load(载入)
作用于主内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
5.use(使用)
作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值得字节码指令时,将会执行这个操作
6.assign(赋值)
作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
7.store(存储)
作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,一般随后的write操作使用
8.write(写入)
作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中
8种基本操作必须满足的规则:
1.不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行(即:read和load直接、store和write之间是可插入其他指令的)
2.不允许一个线程丢弃它的最近的assign操作,即:变量在工作内存中改变了之后,必须把该变化同步回主内存
3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中
4.一个新的变量只能从主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,
即:对一个变量实施use和store操作之前,必须先执行过了assign河load操作
5.一个变量在同一时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个线程重复执行(可重入锁),
多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
6.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
7.如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
volatile变量的特殊规则: --- 不能保证原子性
1.保证此变量对所有线程的可见性
2.禁止指令重排序优化
内存模型有哪些规则?
原子性、可见性、可排序性
10.Java反射与动态代理
Java反射,可以知道Java类的内部构造,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。
这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是性能比较差
每个类被加载进内存后,系统就会为该类生成一个对应的 java.lang.Class对象,通过该Class对象即可访问到JVM中的这个类
加载完类之后,在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类结构信息,这个Class对象就像一面镜子,可以看到类的结构
程序在运行状态中,可以动态加载一个只有名称的类(全路径),对于任意一个已经加载的类,都能够知道这个类的所有属性和方法,
对于任意一个对象,都能调用它的任意一个方法和属性
用法:
作用1:获取类的内部结构
Java反射API可以获取程序在运行时刻的内部结构,只需短短十几行代码,就可遍历出一个Java类的内部结构,包括:构造方法、声明的域和定义的方法等
java.lang.Class类的对象,可以通过其中的方法来获取到该类中的构造方法、域和方法(getConstructor、getFiled、getMethod)
这三个方法还有相应的getDeclaredXXX版本,区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。
Constructor、Field和Method这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。
作用2:运行时刻,对一个Java对象进行操作
动态创建一个Java类的对象,获取某个属性的值和调用某个方法
在Java源码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现
特别说明:
clazz.setAccessible(true) --- 可以获取到类中的private属性和private方法
Class对象的获取
1.对象的 getClass()方法
2.类.class属性 --- 最安全,性能最好
3.Class.forName(String classFullName) 动态加载类 --- classFullName是类的全限定名(包.类名) --- 最常用
从Class中获取信息
1.获取信息
构造器、包含方法、包含属性、包含的Annotation、实现的接口,所在包,类名,简称,修饰符等
获取内容
方法签名
构造器
Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法
Method getMethod(String name, Class<?>... parameterTypes)
包含的属性
Field getField(String name)
包含的Annotation
<A extends Annotation> A getAnnotation(Class<A> annotationClass)
内部类
Class<?>[] getDeclaredClasses()
外部类
Class<?> getDeclaringClass()
所实现的接口
Class<?>[] getInterfaces()
修饰符
int getModifiers()
所在包
Package getPackage()
类名
String getName()
简称
String getSimpleName()
2.判断信息
注解类型、是否是了指定注解、是否是数组、枚举、接口等
判断内容
方法签名
注解类型?
boolean isAnnotation()
使用了该Annotation修饰?
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名类?
boolean isAnonymousClass()
数组?
boolean isArray()
枚举?
boolean isEnum()
原始类型?
boolean isPrimitive()
接口?
boolean isInterface()
obj是否是该Class的实例
boolean isInstance(Object obj)
反射生成并操作对象
通过Method对象执行相应的方法
通过Constructor对象调用对应的构造器来创建实例
通过Field对象直接访问和修改对象的成员变量值
反射创建对象
2种方式:
1.使用Class对象的newInstance()创建该Class对象对应类的实例(要求改Class对应类有默认构造器)
2.先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法创建该Class对象对应类的实例
--- 该方式,可选择指定的构造器来创建实例
1.Spring根据配置文件信息(类的全限定名),使用反射创建对象 ---- 方式一,默认构造器创建实例
eg:
模拟Spring,实现一个对象池,对象池根据文件读取key-value对,然后创建这些对象,并放入Map中,
对象池可以将id作为key,将对象实例作为value,可以通过id获取对象实例
即:
ObjectPool pool = ObjectPool.init("config.json");
User user = (User) pool.getObject("id1");
System.out.println(user);
Bean bean = (Bean) pool.getObject("id2");
System.out.println(bean);
参考:
配置文件
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User"
},
{
"id": "id2",
"class": "com.fq.domain.Bean"
}
]
}
ObjectPool对象池
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
private static JSONArray getObjects(String config) throws IOException {
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
// 根据指定的JSON配置文件来初始化对象池
public static ObjectPool init(String config) {
try {
JSONArray objects = getObjects(config);
ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
if (objects != null && objects.size() != 0) {
for (int i = 0; i < objects.size(); ++i) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
pool.putObject(id, getInstance(className));
}
}
return pool;
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Object getObject(String id) {
return pool.get(id);
}
public void putObject(String id, Object object) {
pool.put(id, object);
}
public void clear() {
pool.clear();
}
}
实例类User和Bean
public class User {
private int id;
private String name;
private String password;
}
public class Bean {
private Boolean usefull;
private BigDecimal rate;
private String name;
}
反射调用方法
获取到某个类对应的Class对象后,可通过该Class对象的getMethod()来获取一个Method数组或Method对象,
每个Method对象对应一个方法,在获取Method对象后,可通过调用invoke()方法调用该Method对象对应的方法
eg:
通过动态调用对象方法 + 配置文件,来给对象设置值 --- 根据属性创建对象(调用setter方法,可设置属性和依赖的对象)
1.json格式的配置文件,用来定义对象、属性值及其依赖关系 --- config.json
注意:
其中fields代表该Bean所包含的属性, name为属性名称, value为属性值(属性类型为JSON支持的类型),
ref代表引用一个对象(也就是属性类型为Object,但是一定要引用一个已经存在了的对象)
这里定义了一个User对象,设置其3个属性
定义一个ComplexBean对象,设置其name属性为complex-bean-name,并设置其引用的对象是id2
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User",
"fields": [
{
"name": "id",
"value": 101
},
{
"name": "name",
"value": "feiqing"
},
{
"name": "password",
"value": "ICy5YqxZB1uWSwcVLSNLcA=="
}
]
},
{
"id": "id2",
"class": "com.fq.domain.Bean",
"fields": [
{
"name": "usefull",
"value": true
},
{
"name": "rate",
"value": 3.14
},
{
"name": "name",
"value": "bean-name"
}
]
},
{
"id": "id3",
"class": "com.fq.domain.ComplexBean",
"fields": [
{
"name": "name",
"value": "complex-bean-name"
},
{
"name": "refBean",
"ref": "id2"
}
]
}
]
}
2.定义对象池,用于创建对象,并调用其方法给对象赋值
參考:反射.ObjectPool 的实现
package com.jay.advanced.java.反射;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模拟Spring,创建一个对象池
* Created by hetiewei on 2017/2/17.
*/
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
//从指定文件,读取配置信息,返回解析后json数组
private static JSONArray getObjects(String config) throws IOException {
//获取输入流
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
//读取输入流内容,变成json数组返回
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
//根据类名,获取类的对象实例
private static Object getIntance(String className, JSONArray fields) throws ClassNotFoundException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//配置的Class
Class<?> clazz = Class.forName(className);
//目标Class的实例对象
Object targetObject = clazz.newInstance();
//遍历属性,赋值给实例对象 --- 注意区分,直接赋值,还是赋值引用对象
if (fields != null && fields.size() != 0) {
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
//需要设置的成员变量名
String filedName = field.getString("name");
//需要设置的成员变量的值
Object fieldValue;
//如果是8种基本类型或String类型,直接获取value,得到属性赋值
if (field.containsKey("value")) {
fieldValue = field.get("value");
} else if (field.containsKey("ref")) {
//如果是引用类型, 先获得引用对象的id,然后根据id,从对象池中得到引用对象
String refBeanId = field.getString("ref");
fieldValue = OBJECTPOOL.getObject(refBeanId);
} else {
throw new RuntimeException("没有value 或 ref 引用");
}
//构造setterXxx
String setterName = "set" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
//需要设置成员变量的setter方法
Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
//调用setter方法设置值
setterMethod.invoke(targetObject, fieldValue);
}
}
return targetObject;
}
private static ObjectPool OBJECTPOOL;
//创建一个对象池实例(保存多线程安全)
private static void initSingletonPool() {
if (OBJECTPOOL == null) {
synchronized (ObjectPool.class) {
if (OBJECTPOOL == null) {
OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
}
}
}
}
//指定根据的JSON配置文件来初始化对象池
public static ObjectPool init(String config) {
//初始化pool
initSingletonPool();
//解析Json配置文件
try {
JSONArray objects = getObjects(config);
for (int i = 0; i < objects.size(); i++) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
//初始化Bean,并放入对象池中
OBJECTPOOL.putObject(id, getIntance(className, object.getJSONArray("fields")));
}
return OBJECTPOOL;
} catch (IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void putObject(String id, Object obj) {
pool.put(id, obj);
}
public Object getObject(String id) {
return pool.get(id);
}
public void clear() {
pool.clear();
}
}
3.客户端使用对象池
//初始化对象池
ObjectPool pool = ObjectPool.init("config.json");
//从对象池中根据id获取对象实例
User user = (User) pool.getObject("id1");
Bean bean = (Bean) pool.getObject("id2");
ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
4.ComplexBean类
public class ComplexBean {
private String name;
private Bean refBean;
}
反射访问并操作成员变量
通过Class对象的getField()方法可获取该类所包含的全部或指定的成员变量Field,
getDeclaredXxx方法可以获取所有的成员变量,无论private/public;
Field提供了读取和设置成员变量值的方法
getXxx(Object obj)
获取obj对象的该成员变量的值,此处的Xxx对应8中基本类型,如果该成员变量的类型是引用类型, 则取消get后面的Xxx;
setXxx(Object obj, Xxx val)
将obj对象的该成员变量值设置成val值.此处的Xxx对应8种基本类型, 如果该成员类型是引用类型, 则取消set后面的Xxx;
eg:
通过反射,设置成员变量值
User user = new User();
//反射获取对象的指定属性
Field idField = User.class.getDeclaredFiled("id");
//设置该属性即使为private也可被访问
idField.setAccessible(true);
//将对象的常用变量,设置为指定值 --- 这里将user对象的id属性,设置为30
idField.setInt(user, 30);
private void setAccessible(AccessibleObject object) {
object.setAccessible(true);
}
反射获取注解信息
只需要获取到Class Method Filed等这些实现了AnnotatedElement接口的类实例, 就可以获取到我们想要的注解信息
eg:
获取client()方法上的注解信息
Annotation[] annotations = this.getClass().getMethod("client").getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType().getName());
}
eg:
获取某个注解中的元数据,需要强转成所需的注解类型,然通过注解对象的抽象方法来访问这些元数据
@Tag(name = "client")
public class Client {
@Test
public void client() throws NoSuchMethodException {
Annotation[] annotations = this.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Tag) {
Tag tag = (Tag) annotation;
System.out.println("name: " + tag.name());
System.out.println("description: " + tag.description());
}
}
}
}
反射获取泛型信息
为了通过反射操作泛型,Java新增了4种类型来代表不能归一到Class了下,但又和原始类型同样重要的类型
类型
含义
ParameterizedType
一种参数化类型, 比如Collection<String>
GenericArrayType
一种元素类型是参数化类型或者类型变量的数组类型
TypeVariable
各种类型变量的公共接口
WildcardType
一种通配符类型表达式, 如? ? extends Number ? super Integer
动态代理:
代理模式:
代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。
代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持,
传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成
动态代理:
JDK5引入了动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。
在运行时刻,可以动态创建出一个实现了多个接口的代理类,每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接口的实现,
当使用者调用代理对象所代理的接口中的方法时,这个调用信息被传递个InvocationHandler的invoke()方法,
在invoke()方法参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数,invoke()方法的返回值被返回给使用者。
相当于对方法调用进行了拦截 --- 这是一个不需要依赖AspectJ等AOP框架的一种AOP实现方式
11.常用的内存调节参数
-Xms 初始堆大小,默认是物理内存1/64(<1G)
-Xmx 最大堆大小
-Xmn 新生代的内存空间大小(eden+ 2 survivor space),
整个堆大小=新生代大小 + 老生代大小 + 永久代大小
在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐新生代配置为整个堆的3/8
-XX:SurvivorRation 新生代Eden区和Survivor区容量比,默认是8, 两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-Xss 每个线程的堆栈大小, 推荐 256K
-XX:PermSize 持久代(非堆内存)初始值, 默认是物理内存 1/64
-XX:MaxPermSize 持久代(非堆内存)最大值,默认物理内存1/4
-XX:+UseParallelGC 多核处理器,配置后提示GC效率
eg:
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=256M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配
说明:
Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。
JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,
方法区,JVM内存处理货优化所需的内存、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中
内存分配方法:
1.堆上分配
2.栈上分配
12.JVM内存管理
程序计数器
方法区
堆
栈(JVM栈 + 本地方法栈)
说明:
GC主要发生在堆上,方法区也会发生GC, 栈与寄存器是线程私有的,不会GC
方法区:
存放内容:类信息、类的static属性、方法、常量池
已经加载的类的信息(名称、修饰符等)
类中的static变量
类中的field信息
类中定义为final常量
类中的方法信息
运行时常量池:编译器生成的各种字面量和符号引用(编译期)存储在class文件的常量池中,这部分内容会在类加载之后进入运行时常量池
使用实例: 反射
在程序中通过Class对象调用getName等方法获取信息数据时,这些信息数据来自方法区
调节参数:
-XX:PermSize 指定方法区最小值,默认16M
-XX:MaxPermSize 指定方法区最大值,默认64M
所抛异常:
方法区使用内存超过最大值,抛出 OutOfMemoryError
GC操作:
对类的卸载
针对常量池的回收
总结:
企业开发中, -XX:PermSize==-XX:MaxPermSize,都设置为256M
eg:
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
类中的static变量会在方法区分配内存,但是类中的实例变量不会(类中的实例变量会随着对象实例的创建一起分配在堆中,当然若是基本数据类型的话,会随着对象的创建直接压入操作数栈)
关于方法区的存放内容,可以这样去想所有的通过Class对象可以反射获取的都是从方法区获取的(包括Class对象也是方法区的,Class是该类下所有其他信息的访问入口)
堆:
存放内容:
对象实例(类中的实例变量会随着对象实例的创建一起分配在堆中,当然若是基本数据类型的话,会随着对象的创建直接压入操作数栈)
数组值
使用实例:
new创建的对象都在这里分配
调节参数:
-Xmx:最大堆内存,默认为物理内存的1/4但小于1G
-Xms:最小堆内存,默认为物理内存的1/64但小于1G
-XX:MinHeapFreeRatio,默认当空余堆内存小于最小堆内存的40%时,堆内存增大到-Xmx
-XX:MaxHeapFreeRatio,当空余堆内存大于最大堆内存的70%时,堆内存减小到-Xms
注意:
在实际使用中,-Xmx与-Xms配置成相等的,这样,堆内存就不会频繁的进行调整了!!!
所抛错误:
OutOfMemoryError:Java heap space
堆内存划分:
新生代:
Eden + from + to from 与 to 大小相等
-Xmn:整个新生代大小
-XX:SurvivorRation : 调整Eden:from(to)的比例,默认是 8:1 即: eden:from:to = 8:1:1
老年代:
新建对象直接分配到老年代的2种情况:
大对象:-XX:PretenureSizeThreshold(单位:字节)参数来指定大对象的标准
大数组:数据中的元素没有引用任何外部的对象
总结:
企业开发中 -Xmx==-Xms
eg:
<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年轻代大小-Xmn==512m,这样,年老代大小就是2048-512==1536m,这个比率值得记住,
在企业开发中,年轻代:年老代==1:3,而此时,我们配置的-XX:MaxTenuringThreshold=15(这也是默认值),年轻代对象经过15次的复制后进入到年老代
年轻代分为Eden和2个Survivor(from+to),默认 Eden:from:to==8:1:1
1.新产生的对象有效分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代)
2.当Eden区满了或放不下时,其中存活的对象会复制到from区(注意:如果存活下来的对象from区放不下,则这些存活下来的对象全部进入老年代),之后Eden区的内存全部回收掉
注意:如果Eden区没有满,但来了一个小对象Eden区放不下,这时候Eden区存活对象复制到from区后,清空Eden区,之后刚才的小对象再进入Eden区
3.之后产生的对象继续分配在Eden区,当Eden区满时,会把Eden区和from区存活下来的对象复制到to(同理,如果存活下来的对象to区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和from区的所有内存;
4)如上这样,会有很多对象会被复制很多次(每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了
5)当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)
栈:
注意点:
每个线程都会分配一个栈,每个栈中有多个栈帧(每个方法对应一个栈帧) 每个方法在执行的同时都会创建一个栈帧,每个栈帧用于存储当前方法的局部变量表、操作数栈等,
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程,说的更明白一点,就是方法执行时创建栈帧,方法结束时释放栈帧所占内存
存放内容:
局部变量表: 8种基本数据类型、对象引用, 该空间在编译期已经分配好,运行期不变
参数调节:
-Xss:设置栈大小,通常设置为1m就好
eg:
<jvm-arg>-Xss1m</jvm-arg>
所抛异常:
StackOverFlowError 线程请求的栈深度大于虚拟机所允许的深度
eg:
没有终止调节的递归(递归基于栈)
每个方法的栈深度在javac编译之后就已经确定了
OutOfMemoryError: 虚拟机栈可以动态扩展,如果扩展时无法申请到足够内存,则抛该异常
注意:
栈可以动态扩展,但栈中的局部变量表不可以
C寄存器(程序计数器)
概念: 当前线程所执行的字节码的行号指示器,用于字节码解释器对字节码指令的执行。
多线程:通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器(也就是一个核)只能执行一条线程中的指令,
为了线程切换后能恢复到正确的执行位置,每条线程都要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
内存分配概念:
在类加载完成后,一个对象所需的内存大小就可以完全确定了,具体的情况查看对象的内存布局。
为对象分配空间,即把一块儿确定大小(上述确定下来的对象内存大小)的内存从Java堆中划分出来
Java的对象内存布局 3 大块
对象头
存储对象自身的运行时数据:Mark Word(在32位和64位JVM上长度分别为32bit和64bit),包含信息如下:
对象hashCode
对象GC分代年龄
锁状态标识
线程持有的锁
偏向锁相关:偏向锁、自旋锁、轻量级锁以及其他的一些锁优化策略是JDK1.6加入的,这些优化使得Synchronized的性能与ReentrantLock的性能持平,
在Synchronized可以满足要求的情况下,优先使用Synchronized,除非是使用一些ReentrantLock独有的功能,例如指定时间等待等。
类型指针: 对象指向类元数据的指针
JVM通过这个指针来确定这个对象是哪个类的实例(根据对象确定其Class的指针)
实例数据
对象真正存储的有效信息
对齐填充
JVM要求对象的大小必须是8的整数倍,若不够,需要补位对齐
注意:
1.Mark Word是非固定的数据结构,以便在极小空间内存储尽量多的信息
2.如果对象是一个数组,对象头必须有一块用来记录数组长度的数据,JVM可以通过Java对象的元数据确定对象长度,但对于数组不行
3.基本数据类型和对象包装类所在的内存大小(字节)
boolean 1字节
byte 1字节
short 2字节
char 2字节
int 4字节
float 4字节
long 8字节
double 8字节
引用类型 在32位和64位系统上长度分别为4bit和8bit
内存分配的 2 种方式:
1.指针碰撞
适用场合:
堆内存规整(即:没有内存碎片,有大块完整内存)的情况下
原理:
用过的内存全部整合到一边,没用过的内存放在另一边,中间有个分界值指针,只需要向着没有用过的内存方向将该指针移动新创建的对象内存大小位置即可
GC收集器:
Serial、ParNew
2.空闲列表
适用场合:
堆内存不规整情况
原理:
JVM虚拟机会维护一个列表,该列表会记录哪些内存块是可用的,在分配时,找一块足够大的内存来划分给new的对象实例,最后更新列表记录
GC收集器:
CMS
注意:
1.2种内存分配方式,取决于Java堆内存是否规整
2.Java堆内存是否规整,取决于GC收集器的算法是 "标记-清除" 还是 "标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
创建一个真正对象的基本过程:5步
1.类加载机制检查
JVM首先检查一个new指定的参数是否能在常量池中定位到一个符号引用,并且检查该符号应用代表的类是否已被加载、解析和初始化过
实际就是在检查new的对象所属的类是否已经执行过类加载机制,如果没有,则先进行加载机制加载类
2.分配内存
把一块确定大小的内存从堆中划分出来
3.初始化零值
对象的实例字段不需要赋初始值也可以直接通过其默认零值
每种类型的对象都有不同的默认零值
4.设置对象头
5.执行<init>
为对象字符赋值(第3步只是初始化了零值,这里会根据参数,给实例赋值)
13.JVM 内存回收GC
1.内存回收区域
堆:GC主要区域
方法区: 回收无用的类和废气的常量
注意:
栈和PC寄存器是线程私有,不会发生GC
2.判断对象是否存活
1.引用计数法
原理:
给对象添加一个引用计数器,每当有一个地方使用它,计数器值+1,引用失效时,计数器值-1
缺点:
1.每次为对象赋值时,都要进行计数器值得加减,消耗较大
2.对于循环依赖无法处理
2.可达性分析(跟踪收集)
原理:
从根集合(GC Root)开始向下扫描,根集合中的节点可以到达的节点就是存活节点,根集合中的节点到达不了的节点就是要被回收的的节点
GC Root节点: 全局性的引用(常量和静态属性)和栈引用
1.Java栈中的对象引用
2.方法区中, 常量+静态变量
3. 3 种引用类型
强引用: A a = new A();//a是常引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
软引用: 内存不足时,是否弱引用所引用的对象,当内存足够时,就是一个普通对象(强引用)
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a); //软引用
弱引用: 弱引用对象只能存活到下一次垃圾会话之前,一旦发生垃圾回收,立刻被回收掉
4.GC回收算法
1.标记-清楚算法 --- 年老代
2.标记-整理算法(标记-压缩) --- 年老代
3.复制算法 --- 年轻代
标记-清楚算法 --- 年老代
原理:
从根集合点扫描,标记处所有的存活对象,最后扫描整个内存空间,并清除没有标记的对象(即:死亡对象)
使用场合:
存活对象较多的情况下比较高效
适用于年老代
缺点:
容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
注意:
在该情况下,内存不规整,对象的内存分配采用"空闲列表法"
标记-整理算法(标记-压缩) --- 年老代
原理:
从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理),清除完之后,将所有的存活对象左移到一起。
适用场合:
用于年老代(即旧生代)
缺点:
需要移动对象,若对象非常多而且标记回收后的内存非常不完整,可能移动这个动作也会耗费一定时间
扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
优点:
不会产生内存碎片
注意:
在该情况下,内存规整,对象的内存分配采用"指针碰撞法"
复制算法 --- 年轻代
原理:
从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉
适用场合:
存活对象较少的情况下比较高效
扫描了整个空间一次(标记存活对象并复制移动)
适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少
缺点:
需要一块儿空的内存空间
需要复制移动对象
注意:
在该情况下,内存规整,对象的内存分配采用"指针碰撞法",见《第二章 JVM内存分配》
以空间换时间:通过一块儿空内存的使用,减少了一次扫描
14.关于Set --- HashSet、TreeSet、LinkedHashSet -- 都是去重的,都可用Iterator或foreach进行遍历
参考:
HashSet --- 去重,无序, add()时会调用hashcode和equals,所以存储在HashSet中的对象需要重写这两个方法,非同步的,元素只能放一个null
即:
HashSet:数据结构式哈希表,线程非同步。保证元素唯一性的原理,判断hashCode是否相同,如果相同,判断元素的equals方法
TreeSet --- 去重,可按照某种顺序排序, add()会将对象转为Comparable,然后调用compareTo()方法,所以存储在TreeSet中的对象必须实现Comparable,重写compareTo()方法
底层数据结构是 二叉树,保证元素唯一性的依据
支持2种排序:自然排序、定制排序
TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。
如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0
定制排序
自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法
2种排序方式比较:
方式一:让集合中的元素自身具有比较性,这就让加入到TreeSet集合中的对象必须实现comparable接口重写compareTo(Object obj)方法
这种方式也成为元素的自然排序或默认排序。(但是如果排序的元素不是本人写的,别人写的没有实现comparable接口时想排序使用第二种方式)
方式二:让集合容器具有比较性,自定义一个比较器实现comparator接口,重写compare(Object o1,Object o2)方法,在初始化TreeSet容器对象将这个
自定义的比较器作参数传给容器的构造函数,使得集合容器具有比较性,使用这种方式的优先级高于方式一,
LinkedHashSet --- HashSet的子类,去重,并保留存储顺序
HashSet 工作原理: 每次存储对象时,调用对象的hashCode(),计算一个hash值,在集合中查找是否包含hash值相同的元素
如果没有hash值相同的元素,根据hashCode值,来决定该对象的存储位置,直接存入,
如果有hash值相同的元素,逐个使用equals()方法比较,
比较结果全为false就存入.
如果比较结果有true则不存.
如何将自定义类对象存入HashSet进行去重复
* 类中必须重写hashCode()方法和equals()方法
* equals()方法中比较所有属性
* hashCode()方法要保证属性相同的对象返回值相同, 属性不同的对象尽量不同
TreeSet 工作原理:存储对象时,add()内部会自动调用compareTo()方法进行比较,根据比较结果使用二叉树形式进行存储 --- 二叉树实现存储
参考:
TreeSet使用二叉树原理,对新add()的对象安装指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入二叉树指定的位置
Integer和String都是按默认的TreeSet排序,自定义的对象,必须实现Comparable接口,重写compareTo(),指定比较规则
在重写compare()方法时,要返回相应的值才能使TreeSet按照一定规则来排序,升序是:比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
如果想把自定义类的对象存入TreeSet进行排序, 那么必须实现Comparable接口
* 在类上implement Comparable
* 重写compareTo()方法
* 在方法内定义比较算法, 根据大小关系, 返回正数负数或零
TreeSet实现排序的2种比较方式:
1.自定义类类 实现 Comparable接口,重写其 compareTo()方法 ---- 类排序
2.给TreeSet定义一个实现Comparator接口的比较器,重写其 compare()方法 ---- 比较器排序
* a.自然顺序(Comparable)
* TreeSet类的add()方法中会把存入的对象提升为Comparable类型
* 调用对象的compareTo()方法和集合中的对象比较
* 根据compareTo()方法返回的结果进行存储
* b.比较器顺序(Comparator)
* 创建TreeSet的时候可以制定 一个Comparator
* 如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
* add()方法内部会自动调用Comparator接口中compare()方法排序
* 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
* c.两种方式的区别
* TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)
* TreeSet如果传入Comparator, 就优先按照Comparator
15.关于Map --- HashMap、TreeMap
原理:
HashMap
按照key的hashCode实现,无序
TreeMap
基于红黑树实现,映射根据其key的自然顺序进行排序,或根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法
TreeMap只能依据key来排序,不能根据value排序
如果想对value排序,可以把TreeMap的EntrySet转换成list,然后使用Collections.sort排序 -- 参考:
eg:value是String或引用类型的值,按照指定规则对value进行排序
public static Map sortTreeMapByValue(Map map){
List<Map.Entry> list = new ArrayList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry>() {
//升序排
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().toString().compareTo(o2.getValue().toString());
}
});
for (Map.Entry<String, String> e: list) {
System.out.println(e.getKey()+":"+e.getValue());
}
return map;
}
16.关于ThreadLocal
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
3个重要方法:
void set(T value)、T get()以及T initialValue()
使用场景:
多线程中,每个线程需要独享这个变量,且每个线程用的变量最初都是一样的,可以通过ThreadLocal处理该变量
原理:
ThreadLocal如何为每个线程维护变量的副本?
ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中key为线程对象,value为线程的变量副本
eg:
public class JavaTest {
// 创建一个Integer型的线程本地变量, 并通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];// 创建5个线程
for (int j = 0; j < 5; j++) {
threads[j] = new Thread(new Runnable() {
@Override
public void run() {
// 获取当前线程的本地变量,然后累加5次
int num = local.get();// 返回当前线程的线程本地变量值,若对应的thread不存在,则会调用initialValue初始化
for (int i = 0; i < 5; i++) {
num++;
}
// 重新设置累加后的本地变量
local.set(num);
System.out.println(Thread.currentThread().getName() + " : "
+ local.get());
}
}, "Thread-" + j);
}
for (Thread thread : threads) {// 启动线程
thread.start();
}
}
}
运行后结果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我们看到,每个线程累加后的结果都是5,各个线程处理自己的本地变量值,线程之间互不影响
17.自定义注解
元注解
Java提供了4种元注解,专门负责注解其他注解使用
@Retention 表示需要在什么级别保存该注释信息(生命周期)
可选参数:
RetentionPolicy.SOURCE: 停留在java源文件,编译器被丢掉
RetentionPolicy.CLASS:停留在class文件中,但会被VM丢弃(默认)
RetentionPolicy.RUNTIME:内存中的字节码,VM将在运行时也保留注解,因此可以通过反射机制读取注解的信息 --- 最常用
@Target 表示该注解用于什么地方
可选参数:
ElementType.CONSTRUCTOR: 构造器声明
ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
ElementType.LOCAL_VARIABLE: 局部变量声明
ElementType.METHOD: 方法声明
ElementType.PACKAGE: 包声明
ElementType.PARAMETER: 参数声明
ElementType.TYPE: 类、接口(包括注解类型)或enum声明
@Documented 将注解包含在JavaDoc中
@Inheried 运行子类型继承父类中的注解
自定义注解:
eg:
自定义注解 --- MyAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
* 作用于方法和类
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
//为注解添加属性
String color();
String value() default "我是xxx";//为注解属性提供默认值
int[] array() default {1,2,3};
Gender gender() default Gender.MAN; // 添加一个枚举
// 添加枚举属性
MetaAnnotation metaAnnotation() default @MetaAnnotation(birthday = "我的出身日期为1988-2-18");
}
定义一个枚举类 --- Gender
public enum Gender{
MAN{
public String getName(){
return "男";
}
},
WOMEN{
public String getName(){
return "女";
}
};
}
定义注解类 --- MetaAnnotation
public @interface MetaAnnotation{
String birthday();
}
解析注解:
/**
* 调用注解并赋值
* Created by hetiewei on 2016/10/12.
*/
@MyAnnotation(metaAnnotation = @MetaAnnotation(birthday = "我的出身日期为1991-2-27"),
color = "red", array = {23, 26 })
public class Test {
public static void main(String args[]){
//检查类Test中是否包含@MyAnnotation注解
if (Test.class.isAnnotationPresent(MyAnnotation.class)){
//若存在则获取注解,并解析
MyAnnotation annotation = Test.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
//解析注解中的内容
//1.获取注解属性
System.out.println(annotation.color());
System.out.println(annotation.value());
//2.获取属性数组
int[] arrs = annotation.array();
System.out.println(arrs.toString());
//3.获取枚举
Gender gender = annotation.gender();
System.out.println("性别:"+gender);
//4.获取注解属性
MetaAnnotation meta = annotation.metaAnnotation();
System.out.println(meta.birthday());
}
}
}
18.关于枚举 枚举类是一种特殊的类,它一样有自己的Field,方法,可以实现一个或者多个接口,也可以定义自己的构造器
枚举与普通类有如下简单区别:
(1). 枚举类可以实现一个或者多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable接口。
(2). 使用enum定义,非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。
(3). 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
(4). 枚举类的所有实例必须在枚举类的第一行显示列出,否则这个枚举类永远不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显示添加。
所有的枚举类都提供了一个values方法,该方法可以很方便的遍历所有的枚举值
(5) 枚举常用方法
name() ,toString() --- 返回此枚举实例名称,优先使用 toString()
ordinal() --- 返回枚举实例的索引位置,第一个枚举值索引为0
public static T valueOf(Class enumType, String name)
--- 返回指定枚举类中指定名称的枚举值,名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符
eg:
public enum SeasonEnum{
//列出4个枚举实例
SPRING,SUMMER,FALL,WINTER;
}
解析:
1.枚举值之间以英文逗号(,)隔开,枚举值列举结束后以英文分号作为结束
2.枚举值代表了该枚举类的所有可能实例
3.使用枚举值 EnumClass.variable eg: SeasonEnum.SPRING
4.枚举可作为switch条件表达式
5.获取所有枚举值 EnumClass[] enums = EnumClass.values();
枚举类的属性Field、方法和构造器
1.枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可以定义Field,方法
19.几种集合类解析
1.HashMap 底层 数组+链表,计算出hash值后,得到元素在数组中存放的位置索引,
若不同元素hash值相同,即:有相同的存放位置,则在相同位置建立链表,采用头插入法依次保存元素
工作原理:
数组+链表 以 Entry[]数组实现的哈希桶数组,用Key的哈希值取模数组的大小得到数组的下标
如果多个key占有同一个下标(碰撞),则使用链表将相同的key串起来
通过hash方法,通过put和get存储和获取对象,存储对象时,将K/V传给put()方法时,它调用hashCode()计算hash值得到bucket位置,
进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过加载因子,容量扩展为2倍)。
获取对象时,通过K,调用hashCode()得到bucket位置,并进一步调用equals()方法确定键值对。
如果发生碰撞时,HashMap通过链表将产生碰撞的元素组织起来,在Java8中,如果一个bucket中碰撞的元素超过某个限制(,默认8个),
则使用红黑树来替换链表,从而提高速度
2.HashSet 底层 是HashMap实现, 优点:利用哈希表提供查询效率, 缺点:元素不能重复
由于HashSet不允许元素重复,故需要判断元素是否相同,
用hash表判断元素是否相同的方法,即需要hashCode和equals两个方法,对于hashSet,先通过hashCode判断元素在哈希表中的位置是否相同,在通过equals方法判断元素内容是否相同
哈希表如何判断元素是否相同?
1> 哈希值是否相同 :判断的其实是对象的hashCode()方法是否相同(看对象是否在哈希表的同一个位置)
2>内容是否相同:用equals方法判断对象是否相同。
规则:若hash值不同,不必判断对象内容,返回false;若hash值相同,有必要判断对象内容,若在相同,返回true,否则false。
3.TreeSet 使用元素的自然顺序,对象集合中元素进行排序,添加的元素需要自己实现Comparable接口,以便默认排序时调用其CompareTo()进行比较
2中自定义排序方式
1.元素的类,实现Comparable接口,实现compareTo()
2.给ThreeSet传递一个实现Comparator接口的参数对象
20.关于线程池
参考:
http://www.codeceo.com/article/java-thread-pool-deep-learn.html
http://www.codeceo.com/article/java-threadpoolexecutor.html
1.核心类:
ThreadPoolExecutor extends AbstractExecutorService implement ExecutorService 提供4个构造器
构造器参数:
corePoolSize: 核心池大小
默认情况,创建线程池后,池中线程数为0,当有任务来时,创建一个线程去执行任务,当线程池中线程数目达到corePoolSize后,会把任务放入缓存队列、
maxPoolSize: 线程池最大线程数
keepAliveTime:表示线程没有任务执行时最多保持多久会终止
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize
unit: 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue: 阻塞队列,用来存储等待执行的任务
可选的阻塞队列:
ArrayBlockingQueue
LinkedBlockingQueue --- 默认,用的最多
SynchronousQueue
PriorityBlockingQueue
threadFactory: 线程工厂,主要用来创建线程
handler: 表示当拒绝处理任务时的策略,
4种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
2.线程池原理:
1.线程池状态:
ThreadPoolExecutor中定义了一个Volatile变量runState表示当前线程池的状态,使用volatile来保证线程之间的可见性
线程池的4种状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
解析:
1.创建线程池后,初始时,线程池处于RUNNING状态
2.如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能接受新任务,但会等待所有任务执行完毕
3.如果调用了shutdownNow()方法,线程池处于STOP状态,此时线程池不能接受新任务,并且会尝试终止正在执行的任务
4.当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
2.任务的执行:
线程任务保存在BlockingQueue中,通过execute(Runnable )来调用执行,
21.Java的类加载器
类加载器 --- 一个用来加载类文件的类
Java源码通过javac编译成类文件,然后JVM来执行类文件中的字节码,类加载器负责加载文件系统、网络或其他来源的类文件
JVM中类加载器的树状层次结构
Bootstrap ClassLoader 引导类加载器, 加载Java的核心库(jre/lib/rt.jar),用C++代码实现,不继承子java.lang.ClassLoader
Extension ClassLoader 扩展类加载器, 加载Java的扩展库(jre/ext/*.jar), Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
System ClassLoader 系统类加载器, 根据Java应用的类路径(classpath)来加载Java类,
Java应用的类都是由它来完成加载的,可通过ClassLoader.getSystemClassLoader()获取系统类加载器
自定义类加载器 通过继承java.lang.ClassLoader类,实现自己的类加载器
Java类加载器的3个机制:
委托机制:
将加载器的请求交给父加载器,如果父类加载器找不到或不能加载这个类,则当前类加载器再加载它
可见性机制:
子类的加载器可以看见所有父类加载器加载的类,而父类加载器看不到子类加载器加载的类
单一性机制:
类仅被加载一次, 由 委托机制 确保子类加载器不会再次加载父类加载器加载过的类
类加载过程 3个步骤:
装载:
链接:(验证、准备、解析)
初始化:
装载:
查找并加载类的二进制数据
链接:
验证:确保被加载类信息符合JVM规范、没有安全方面问题
准备:为类的静态变量分配内存,并将其初始化为默认值
解析:把虚拟机常量池中的符号引用转换为直接引用
初始化:
为类的静态变量赋予正确的初始值
说明:
1.JVM会为每个加载的类维护一个常量池
2.类的初始化步骤:
1.如果这个类没被加载和链接,则先进行加载和链接
2.如果这个类存在父类,如果类未初始化,则先初始化其父类
3.如果类中存在static块,一次执行这些初始化语句
java.lang.ClassLoader类
根据一个指定的类名称,找到或生成其对于的字节代码,然后从这些字节码中定义出一个Java类,即:java.lang.Class类的一个实例
ClassLoader中的方法:
getParent() 返回该类加载器的父类加载器
loadClass(name) 加载名称为name的类,返回结果是java.lang.Class的实例
findClass(name) 查找名称为name的类,返回结果是java.lang.Class的实例
findLoadedClass(name) 查找名称为name的已被加载过的类,返回结果是java.lang.Class的实例
resolveClass(Class<?> c) 链接指定的Java类
Java中的类加载过程
加载(可自定义类加载器) 连接 ( 验证 准备 解析 ) 初始化
加载:
获取二进制字节流 --> 将字节流静态存储结构转换为方法区的运行时数据结构 --> 在堆中生成Class对象
连接:
验证:
文件格式验证: 1.参照Class文件格式规范验证
2.此阶段基于字节流经过此验证后,字节流才会进入方法区,后面的验证都依赖与方法区的验证
元数据验证: Java语法验证,eg:该类是否继承了不该继承的类
字节码验证: 运行时安全性检查
符号引用验证: 确保类中引用的类,字段,方法都是可访问的
准备:
设置类变量初始值 --- static类变量 初始值 , 注意:final比较特别!!!
1.设置类变量 --- static变量
2.设置变量初始值 (注意:非代码中定义的值,8种基本数据类型都有初始值 eg: static int a = 10, 准备阶段会把a初始值赋值为0,初始化时,再赋值为10 )
3.对于final的值,设为代码中的值(eg:final static int a = 10 , 准备阶段直接把 a 赋值为10)
解析:
将符号引用转换为直接引用
1.符号引用: 用符号来表示所引用的目标
2.直接引用: 一个指向内存中目标对象地址的句柄
初始化:
1.根据代码实现初始化类变量及其他资源 (准备阶段,static类变量还是初始值,这里赋值为代码中指定的值)
2.执行子类初始化方法时,先执行父类的初始化方法(static变量赋值,static代码段执行,先父类后子类)
22.Java反射 增加 装饰模式 的适用性
装饰模式:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能,它是通过创建一个包装对象来包裹真实的对象,比生产子类更加灵活,
使用Java的动态代理实现装饰模式,会具有更强的灵活性和适用性
装饰模式有什么特点呢?
1、装饰对象和真实对象有相同的接口。这样调用者就能以和真实对象相同的方式和装饰对象交互。
2、装饰对象包含一个真实对象的引用(即上面例子中的Ability接口)。
3、装饰对象接受所有来调用者的请求,并把这些请求转发给真实的对象。
4、装饰对象可以在调用者的方法以前或以后增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加的功能。
什么样的地方使用装饰模式呢?
1、需要动态扩展一个类的功能,或给一个类添加附加职责。
2、需要动态的给一个对象添加功能,这些功能可以再动态的撤销。
3、需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。
4、 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
23.JVM的运行时栈帧 --- JVM运行程序的过程!!! --- 方法的运行过程 !!!
1.每个方法的执行,在JVM中都是对应的栈帧在JVM栈中的入栈到出栈的过程!!!
2.每个在JVM中运行的程序,都是由许多的帧切换产生的结果
参考:
栈帧: --- 线程安全!!!每个线程的栈帧相互独立 ---> 局部变量在多线程环境下线程安全的原因!!!
存放方法的局部变量表、操作数栈、动态链接,方法返回值和一些额外的附加信息
当前栈:
一个方法的调用链可能很长,当调用一个方法时,可能会有很多方法处于执行状态,但对于执行引擎,置于JVM栈顶的栈帧才是有效的,这个栈帧称为 当前栈
当前栈所关联的方法称为当前方法,执行引擎的所有指令都是针对当前栈帧进行操作的
局部变量表:
内容: 存放方法的局部变量
eg:方法参数,方法内定义的局部变量,对象引用,returnAddress类型
在Java程序被编译为class文件时,这个表的容量最大值已经确定
访问:
虚拟机利用索引编号的递增来对局部变量表中定义的变量进行一次访问(从0开始),而对于实例方法(非static方法),其局部变量表的第0个索引是this,
这是可以在实例方法中使用this.name ......的原因
动态连接:
参考:
方法的调用过程:
在虚拟机运行时,运行时常量池会保存大量符号引用,这些符号引用可以看做每个方法的间接引用,
如果代表栈帧A的方法要调用代表栈帧B的方法,则这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,
但因为符号引用并不是直接指向代表B方法的内存位置,所有在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用访问到真正的方法!
注意:
静态解析:
如果符号引用在类加载阶段或第一次使用时转化为直接引用,则这种转换成为静态解析
动态连接:
如果在运行期间转换为直接引用,这种转换称为动态连接
栈帧A 常量池 栈帧B
局部变量表 局部变量表
A方法的符号引用
操作数栈 操作数栈
B方法的符号引用
动态连接 动态连接
字符串常量等
返回地址 返回地址
方法返回地址
1.正常退出:根据方法定义决定是否要返回值给上层调用者
2.异常退出:不会传递返回值给上层调用者
注意:
1. 不管那种方式结束,在退出当前方法时,都会跳转到当前方法被调用的位置!!!
如果正常退出,则调用者的PC计数器的值可作为返回地址,
如果异常退出,则需要通过异常处理表来确定
2. 方法的一次调用对应着栈帧在虚拟机中的一次入栈出栈操作!!!
方法退出时做的事情:
恢复上层方法的局部变量表以及操作数栈,如果有返回值,就把返回值压入到调用者栈帧的操作数栈中,
还会把PC计数器的值调整为方法调用入口的下一条指令
24.JVM的内存溢出分析和参数调优
1.JVM调优
1.JVM内存参数调优
-Xms 设置初始化堆的内存
-Xmx 设置堆最大使用内存
-Xss 设置每个线程的栈大小
-Xmn 设置年轻代大小
eg:
java -Xmx4096m -Xms4096m -Xmn2g -Xss128k
或
java -Xmx4g -Xms4g -Xmn2g -Xss128k
设置JVM堆最大可以内存为4096M,初始内存为4096M(-Xms设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存)
设置JVM年轻代大小为2G JVM堆内存 = 年轻代大小 + 年老代大小 + 持久代大小
持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,推荐配置为整个堆的3/8
设置每个线程的栈大小为128k
JDK5+ 线程栈默认1M, 相同物理内存下,减小该值能生成更多线程,但操作系统对一个进程内的线程数有限制,最好不超过5000
如果方法递归太深,则可能耗尽线程栈,报出 StackOverflow !!! 线程栈内存溢出 <--- 方法调用太深
eg:设置堆内存中的内存分配比例
java -Xmx4g -Xms4g -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=64m -XX:MaxTenuringThreshold=0
-Xmn2g 设置年轻代大小为2G
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆的1/5
-XX:SurvivorRatio=4 设置年轻代中from和to区的比例,Eden区(from)与Survivor区(to)的大小比值为4:1:1,即 一个 Survivor 占 年轻代的1/6
特别注意:
上面的比值 4 <=等价=> 1:4
-XX:MaxPermSize=64m 设置持久代大小为64M
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄
小结:
1,整个堆包括年轻代,老年代和持久代。其中年轻代又包括一个Eden区和两个Survivor区。
2,年轻代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
2,持久代:
-XX:PermSize
-XX:MaxPermSize
3,年轻代和老年代的比例:
-XX:NewRatio(年轻代和老年代的比值,年轻代多,除去持久代)
当设置了-XX:+UseConcMarkSweepGC后,会使-XX:NewRatio=4失效,此时需要使用-Xmn设置年轻代大小
4,Eden与Survivor的比例
-XX:SurvivorRatio(Eden区与两个Survivor区的比值,Eden区多)
2.GC参数设置
并行收集器 --- 吞吐量优先,适合后台处理
eg:
-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
解析:
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集,JDK6.0支持对年老代并行收集
并发收集器 --- 响应时间优先,保证系统响应时间,减少垃圾收集时的停顿时间,适合应用服务器和典型领域等
eg:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
3.常见配置汇总
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
4.调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。
如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;
如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息
持久代并发收集次数
传统GC信息
花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,
减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。
但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,
然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
2.JVM内存溢出分析 --- OutOfMemoryError 不属于Exception,继承自Throwable
1.堆内存溢出 --- 不断创建对象,并且不释放,导致GC无法回收,堆内存移除 Java heap space
eg: 制造堆内存溢出的程序
1.降低修改虚拟机堆内存大小 -Xms20m -Xmx20m
2.不断创建强引用对象,方式GC回收
public static void main(String[] args) {
headOutOfMemory();
}
/*
* -verbose:gc -XX:+PrintGCDetails -verbose:gc
* -XX:+HeapDumpOnOutOfMemoryError
*
* -Xms20m -Xms20m
*
*/
static void headOutOfMemory() {
long count = 0;
try {
List<Object> objects = new ArrayList<Object>();
while (true) {
count++;
objects.add(new Object());
}
} catch (Throwable ex) {
System.out.println(count);
ex.printStackTrace();
}
}
}
异常信息:
java.lang.OutOfMemoryError: Java heap space
2.栈内存溢出 --- 栈主要存放栈帧(局部变量表(基本数据类型,对象引用,returnAddress类型),操作数栈,动态链接,方法出口信息),
1.StackOverflowError ---- 当线程栈的空间大于虚拟机所允许时,抛出 StackOverflowError
线程栈,因递归或方法调用太深,导致超过线程栈设定时,抛 StackOverflowError
eg:
自定义线程栈溢出
1.降低线程栈大小 -Xss4k
2.方法循环递归
public class JVMStackSOF {
/**
* (1) 在hotspot虚拟机中不区分虚拟机栈(-Xss)和本地方法栈(-Xoss),且只有对Xss参数的设置,才对栈的分配有影响
*
* (2)
* 由于StackOverflowError->VirtualMachineError->Error
* ->Throwable,所以catch的时候如果用Exception的话将捕获不到异常 Stack length 会随着-Xss的减少而相应的变小
*/
private int stackNumber1 = 1;
public void stackLeck1() {
stackNumber1++;
stackLeck1();
}
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeck1();
} catch (Throwable ex) {
System.out.println("Stack length:" + jvmStackSOF.stackNumber1);
ex.printStackTrace();
}
}
}
异常信息:
java.lang.StackOverflowError
2.OutOfMemoryError ---- 栈空间不足,抛出OutOfMemoryError
JVM栈,整体内存不足时,抛OutOfMemoryError
eg:
自定义JVM栈溢出
1.JVM中除了堆和方法区,剩余的内存基本都由栈占用
2.每个线程都有独立的栈空间(堆、方法区是线程公用)
3.如果-Xss调大每个线程的栈空间,可建立的线程数量必然减少
public class JVMStackOOM {
/**
* (1)不停的创建线程,因为OS提供给每个进程的内存是有限的,且虚拟机栈+本地方法栈=(总内存-最大堆容量(X模型)-最大方法区容量(
* MaxPermSize)),于是可以推断出,当每个线程的栈越大时,那么可以分配的线程数量的就越少,当没有足够的内存来分配线程所需要的栈空间时,
* 就会抛出OutOfMemoryException
* (2)由于在window平台的虚拟机中,java的线程是隐射到操作系统的内核线程上的,所以运行一下代码时,会导致操作系统假死(我就尝到了血的代价)
*/
private static volatile int threadNumber = 0;
public void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
threadNumber++;
while (true) {
System.out.println(Thread.currentThread());
}
}
}.start();
}
}
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
try {
jvmStackOOM.stackLeakByThread();
} catch (Throwable ex) {
System.out.println(JVMStackOOM.threadNumber);
ex.printStackTrace();
}
}
}
异常信息如下:
java.lang.OutOfMemoryError:unable to create new native thread
3.方法区溢出
方法区:存放JVM加载的类信息,常量,运行时常量池,静态变量,编译期编译后的代码等
异常信息:
java.lang.OutOfMemoryError: PermGen space
eg:
自定义方法区溢出代码
1.通过不断产生类信息来占用方法区内存
2.调整 -XX:PermSize=10m --XX:MaxPermSize=10m,来降低方法区内存大小
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 利用CGLib技术不断的生成动态Class,这些Class的信息会被存放在方法区中,如果方法区不是很大会造成方法去的OOM
*
*
* -XX:PermSize=10m -XX:MaxPermSize=10m
* */
public class MethodAreaOOM {
static class Test {}
public static void main(String[] args) {
try{
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(arg0, arg2);
}
});
System.out.println(enhancer.create());
}
}catch(Throwable th){
th.printStackTrace();
}
}
}
异常信息:java.lang.OutOfMemoryError: PermGen space
25.JVM内存分配策略与回收
1.分配策略
1.对象优先在Eden区上分配
2.大对象直接分配在老年区
-XX:PretenureSizeThreshold 参数设置直接放入老年区的对象大小
3.长期存活的对象直接进入老年区
-XX:MaxTenuringThreshold 参数设置对象年龄,经历几次gc可以进入老年区
JVM为每个对象定义了年龄计数器,
如果对象在Eden出生并经过第一次Minor GC后任然存活,并能被Survivor容纳,将被移到Survivor空间中,
并将对象年龄设为1,对象在Survivor区每熬过一次Minor GC,年龄+1,
当年龄达到一定程度(默认15,可参数-XX:MaxTenuringThreshold设置)时,进入到老年代中
2.内存回收
1.Minor GC 发生在年轻代的GC,当JVM无法为一个新对象分配空间时,触发Minor GC,清理年轻代内存,大多数对象生命周期短,所以Minor GC 非常频繁,而且速度较快
触发条件:
Eden区满时,触发Minor GC
2.Full GC 发生在年老代的GC
触发条件:
1.调用System.gc(),系统建议执行Full GC,但不一定执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的对象平均大小,大于老年代可用内存空间
5.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
26.AOP应用和实现
AOP优化改造:
1.关于Aspectj
获取目标方法信息 --- JoinPoint Spring只支持方法执行的JoinPoint
JoinPoint里包含了如下几个常用的方法:
Object[] getArgs:返回目标方法的参数
Signature getSignature:返回目标方法的签名
Object getTarget:返回被织入增强处理的目标对象
Object getThis:返回AOP框架为目标对象生成的代理对象
注意:当使用@Around处理时,我们需要将第一个参数定义为ProceedingJoinPoint类型,该类是JoinPoint的子类
2.5种增强处理
Before、 在某个连接点JoinPoint之前执行, 不能阻止连接点前的执行
Around、 包围一个连接点的通知,可以在方法调用前后完成自定义的行为
AfterReturning、 在某连接点正常完成后执行的通知,不包括抛出异常的情况
After、 某连接点退出时执行(不同正常返回还是异常退出,都会执行的通知,类似finally功能,可以用于释放连接和资源)
AfterThrowing 在方法抛出异常退出时执行的通知
--- 5种增强处理中,织入增强处理的目标方法、目标方法的参数和被织入增强处理的目标对象等
任何一种织入的增强处理中,都可以获取目标方法的信息
切点表达式:
1.execution()表达式
eg:
execution (* com.sample.service.impl..*. *(..))
1.execution() 表达式主体
2.第一个* : 表示返回类型, * 表示所有类型
3.包名: 要拦截的包,后面2个句点表示当前包和当前包的所有子包,即:com.sample.service.impl包、及其子孙包下所有类的方法
4.第二个* : 表示类名, * 表示所有类
5.第三个* : 表示方法名, * 表示所有方法, *(..) 中的2个句点表示方法参数, 2个句点表示任何参数
2.自定义注解
1.定义切点方法
//自定义注解方式
@Pointcut("annotation(com.jay.annotation.MyAnnotation)")
//@Pointcut("execution (* com.gcx.controller..*.*(..))") --- execution()切点表达式方式
public void controllerAspect(){}
2.在增强方法上,使用切点方法
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint){
}
eg:
使用5种增强处理
1.类上添加 @Aspect注解
2.方法上添加 4中增强处理注解
3.特别注意:
@Around中参数是 ProceedJoinPoint, 其他4类增强用 JoinPoint
@Aspect
public class AdviceTest {
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模拟权限检查...");
System.out.println("@Before:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:参数为:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被织入的目标对象为:" + point.getTarget());
}
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:执行目标方法之前...");
//访问目标方法的参数:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改变后的参数1";
}
//用改变后的参数执行目标方法
Object returnValue = point.proceed(args);
System.out.println("@Around:执行目标方法之后...");
System.out.println("@Around:被织入的目标对象为:" + point.getTarget());
return "原返回值:" + returnValue + ",这是返回结果的后缀";
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模拟日志记录功能...");
System.out.println("@AfterReturning:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:参数为:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值为:" + returnValue);
System.out.println("@AfterReturning:被织入的目标对象为:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模拟释放资源...");
System.out.println("@After:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:参数为:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被织入的目标对象为:" + point.getTarget());
}
//标注该方法体为异常通知,当目标方法出现异常时,执行该方法体
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")
public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){
...
}
}
3.动态代理实现
1.静态代理 --- 代理模式
两个类实现同一个接口,在代理类的接口方法中,调用被代理对象的接口方法,同时在代理类的接口方法中,调用被代理类方法前后添加逻辑
2.动态代理 --- JDK动态代理 和 cglib 动态代理(字节码增强技术,效率高)
参考:
2种动态代理区别:
1.JDK动态代理要求被代理类要实现接口,而cglib不需要
2.cglib能根据内存中为其创建子类(代理对象)
1.JDK的动态代理 --- 通过创建一个实现InvocationHandler接口的中间对象,实现动态代理
优点:不必要求代理者和被代理者实现相同接口
缺点:仅支持接口代理, JDK动态代理需要实现类通过接口定义业务方法,对于没有接口的类,无法代理
eg:
自定义的基于JDK的动态代理类
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客户端使用JDK动态代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy()
greeting.sayHello("Jack");
2.cglib动态代理 --- 字节码增强技术,效率高
原理:
通过字节码技术,为一个类创建子类,并在子类中采用方法拦截的技术,拦截所有父类方法的调用,顺势织入横切逻辑
优点:字节码增强技术,CGLib创建的动态代理对象性能比JDK创建的动态代理对象的性能高不少,
但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,
反之,使用JDK方式要更为合适一些
缺点:CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客户端使用cglib动态代理
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
27.关于JVM架构
参考:http://www.ityouknow.com/java/2017/03/01/jvm-overview.html
1.Java内存模型
内存模型:
描述程序中各个变量(实例域、静态域、数组元素)之间的关系,以及在计算机系统中将变量存储到内存,从内存取出变量这样的底层细节
内存模型规则:
原子性:
约定了:访问存储单元内任何类型字段的值以及对其进行更新操作时,必须保证其是原子的
即:获得或初始化某一些值时(),该值的原子性在JVM内部是必须得到保证的
可见性:
一个线程修改的状态,对另一个线程是可见的,
即:一个线程修改的结果,另一个线程马上就能看到
eg:
volatile修饰的变量具有可见性,不允许线程内部缓存和重排序,但不保证原子性
可见性规则约束下,定义了一个线程在哪种情况下可以访问或影响另外一个线程,以及从另外一个线程的可见区域读取相关数据、将数据写入到另外一个线程内
可排序性:
为了提高性能,编译器和处理器可能会对指令做重排序
volatile修饰的变量不允许线程内部缓存和重排序
Java内存模型: --- JMM Java Memory Model
JMM: 是控制Java线程之间、线程和主存直接通向的协议
JMM定义了线程和主存之间的抽象关系:
线程之间的共享变量存储在主存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存存储了该线程以读/写共享变量的副本
2.JVM实例内部结构
子系统:
类加载器作为JVM的子系统,针对Class文件进行检测来加载对应的类
执行引擎:
负责代码的解释和执行
内存区域:
存储字节码、类信息、对象、参数、变量等
方法区: --- 线程共享
存储类的装载信息(类信息),
静态变量(类变量),
运行时常量池
堆: --- 线程共享
对象、数组内存对分配:
当一个Java程序创建一个对象或者一个数组时,JVM实例会针对该对象和数组分配一个新的内存堆空间。在JVM实例内部,只存在一个内存堆的实例,所有的依赖该JVM的Java程序都共享该实例
进程内存堆分配:
多个Java进程启动时,会得到JVM分配给自己的对空间,多个Java进程的堆空间时相互独立的
JVM栈: --- 线程私有
对于线程内存栈分配:
当一个新线程启动时,JVM为其创建独立内存栈,
内存栈由栈帧构成,栈帧有2中操作:出栈和入栈
当前线程方法 --- 正在被线程执行的方法, 该方法的栈帧称为 当前帧
对于方法:
当一个线程调用某个方法时,JVM创建并将一个新帧压入到内存栈中,这个帧称为当前栈帧,
当该方法执行时,JVM使用内存栈来存储 参数引用、局部引用变量、基本类型数值、返回值等相关数据
无论方法正常结束还是异常结束,JVM都弹出或丢弃该栈帧,上一帧方法成为当前帧
本地方法栈:
保存了本地Java方法调用状态,其状态包括局部变量、被调用的参数、它的返回值、以及中间计算结果
程序计数器: --- 线程私有
每个线程都有自己的PC寄存器,通过计数器来指示下一条指令执行
3.JVM内存分配策略
静态存储:
编译时,能确定每个数据在运行时需要的存储空间,因而在编译时就给它们分配固定的内存空间
此分配策略,要求代码中不允许有可变数据结构存在,也不允许嵌套或递归结构出现(无法计算需要内存空间)
eg:
static final 全局常量
栈式存储:
动态存储分配,由一个类似堆栈的运行栈来实现,按先进后出原则分配
程序对数据所需内存空间未知,只有到运行时才能知道占用空间
堆式存储:
专门负责在编译时或运行时,无法确定存储要求的数据结构的内存分配
eg:
可变字符串和对象实例
4.对象分配规则:
1.对象优先分配在年轻代的Eden区,如果Eden区没有足够空间,JVM执行一次 Minor GC
2.大对象直接进入老年代(大对象:需要大量连续内存空间的对象, 可参数设置大对象大小),目的:避免在Eden区和2个Survivor区直接进行大量的内存拷贝 <--- 新生代采用复制算法收集内存
3.长期存活的对象进入老年代, JVM为每个对象定义了一个年龄计数器,如果对象经过1次Minor GC,对象进入Survivor区,之后没经过一次Minor GC,对象年龄+1, 直到达到阈值,才进入老年代 (可参数设置年龄计数器大小)
4.动态判断对象年龄。 如果Survivor区中相同年龄的所有对象大小总和,大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
5.空间分配担保。 每次进行Minor GC时, JVM会计算Survivor区移至老年代的对象的平均大小,如果这个值大于老年区剩余值,则进行一个Full GC
5.GC算法 垃圾回收
对象存活判断:
引用计数 --- 每个对象有应用计数属性,新增一个引用,计数+1, 应用释放,计数-1,计数为0,可回收 ---- 无法解决对象循环引用问题!
可达性算法(根搜索算法) --- 从GC Roots开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链可达时, 证明此对象不再用,可回收
可作为GC Roots的对象:
虚拟机栈中引用的对象
方法区中的静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
特别注意:
GC管理的主要是 Java 堆,一般情况下只针对堆,进行垃圾回收,
方法区、栈、本地方法区不被GC锁管理,因而选择这些区域内的对象作为GC Roots,
被GC Roots引用的对象不被GC回收
GC算法: 3种基础算法
参考:
标记-清除算法
分"标记" 和 "清除" 两个阶段
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
缺点:
标记和清除过程效率低
标记清除后,产生大量不连续的内存碎片
复制算法
将可用内存按容量分为相等的2块,每次只使用其中的一块,
当这块内存用完了,就将还存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉
缺点:
内存缩小为原来的一半
优点:
不会有内存碎片,
只需要移动堆的内存指针,按顺序分配内存即可,实现简单,运行高效
标记-压缩(整理)算法
标记与第一种算法一样类似,但后续操作不是直接清理对象,而是让所有存活对象都向一端移动,
并更新引用其对象的指针,然后直接清理掉端边界意外的内存
缺点:
在标记-清除的基础上,需要进行对象的移动,成本相对较高
优点:
不会产生内存碎片
JVM分代收集算法
JVM把堆内存分为新生代和老年代, 根据各个代的特点,采用合适的收集算法
新生代 --- 复制算法 !!!
每次垃圾收集都有大批对象死去,只有少量存活,适合复制算法,只需付出少量存活对象的复制成本就可完成收集
老年代 --- 标记-整理算法 !!!
老年代中对象存活率高,没有额外空间对它进行分配担保,必须使用 "标记-压缩" 算法进行回收
垃圾回收器:
Serial收集器 --- 串行收集器,最古老,最稳定,效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收
ParNew收集器 --- Serial收集器的多线程版本
Parallel收集器 --- 类似ParNew,更关注系统的吞吐量
Parallel Old收集器 --- 使用多线程 和 标记-整理 算法
CMS收集器 --- 是一种以获取最短回收停顿时间为目标的收集器
G1收集器 --- 面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
28.哈希表 和 HashMap原理
哈希表和哈希算法原理:
一种根据关键字直接访问内存存储位置的数据结构
哈希函数: 通过哈希表,数据元素的存放位置和数据元素关键字之间建立起某种对应关系, 关键字到内存的映射
hash表以及HashMap原理:
http://www.code123.cc/258.html
HashMap原理:
HashMap基于哈希函数,使用put(key,value)存储对象,使用get(key)获取对象,
put传递键值对,先对键调用hashCode()方法,返回hashCode()用于找到bucket位置来存储Entry对象,
HashMap是在bucket中存储键对象和值对象,作为 Map.Entry
bucket存储LinkedList,而LinkedList中存储的是key-value对象 --> Map.Entry
HashMap在Map.Entry静态内部类中实现存储key-value对,
HashMap使用哈希算法,在put和get方法中,使用hashCode()和equals()方法,
通过传递key-value对,调用put方法时,HashMap调用键对象(key)的hashCode()计算hashCode,然后bucket位置来存储对象(根据key查找value存放地址),
如果多个key的hashCode相同,则是发生碰撞,对象将存储在同一个bucket位置的LinkedList的下一个节点中(键值对对象 <==> Map.Entry,Entry存储在LinkedList中)
两个对象的hashCode相同会发生什么?
1.两个对象的hashCode相同,但它们可能并不相等 hashCode() 和 equals()
2.HashMap的key-value抽象成Map.Entry, Entry存储在LinkedList中, 每个bucket位置对应一个LinkedList,用于解决哈希碰撞
3.如果hashCode相同,则它们的bucket位置相同, 存储时会发生碰撞,
如果发送碰撞,后一个添加的对象,会存放在bucket位置对应的LinkedList的下个节点中
如果两个键的hashCode相同,如何获取值对象?
1.get()根据key的hashCode去查找bucket位置,然后获取对应的value对象
2.如果两个key的hashCode相同,则两个key对应的Entry都存储在同一个bucket位置的LinkedList中
3.HashMap的LinkedList中存储的是键值对Map.Entry对象
4.根据key的hashCode找到bucket位置后,调用key.equals()方法找到LinkedList中正确的节点,最终找到要查的value值对象
5.注意:
hashCode()用于直接定位bucket位置
equals() 用于获取值对象时使用
如果HashMap大小超过了负载因子定义的容量,怎么办?
1.默认负载因子是0.75,即:一个map填满75%的bucket时,将会创建原来HashMap大小的2倍的bucket数组,来重新调整map的大小,
并将原来的对象放入新的的bucket数组中 --- 这个过程称为 rehashing --- 因为它调用hash方法找到新的bucket位置
重新调整HashMap大小时,存在什么问题?
多线程下,可能产生条件竞争
eg:
如果2个线程都发现HashMap需要重新调整大小,它们会同时尝试调整大小。
在调整大小过程中,存储在LinkedList中的元素的次序将会反过来。
因为移动到新的bucket位置时,HashMap并不会将元素放到LinkedList的尾部,而是放在头部,这是为了避免尾部遍历,
如果条件竞争发生了,就死循环了
多线程环境下,使用HashTable或ConcurrentHashMap替代HashMap
注意:
1.String、Integer适合作为HashMap的key
原因:
String是不可变的,final的,已经重写了equals()和hashCode()方法
如果两个不相等的对象返回不同的hashCode,碰撞的几率就会小,进而提高HashMap的性能
2.只要遵循equals()和hashCode()的定义规则,并且当对象插入到Map中之后不会再改变了,可以自定义对象作为key,
3.使用ConcurrentHashMap替代HashTable
HashTable是同步的,线程安全的,但ConcurrentHashMap同步性能更好,其采用了分段锁
4.HashTable和ConcurrentHashMap的区别
1.HashTable每次同步执行是,都要锁住整个结构
2.ConcurrentHashMap将hash表分为16个bucket桶(默认值),采用分段锁,get、put、remove等操作只锁当前需要用到的桶
eg:
原先只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,读线程不受限制),提升了并发性
总结:
HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对
HashTable实现原理:
实现原理与HashMap类似,但为了线程安全,HashTable中几乎所有的public方法都用synchronized做了同步处理,有些方法也是在内部通过 synchronized 代码块来实现
Hashtable 与 HashMap 的简单比较
HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap则不是。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。
所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。
不考虑性能问题的时候,我们的解决方案有 Hashtable 或者Collections.synchronizedMap(hashMap)来替换HashMap,这两种方式基本都是对整个 hash 表结构做锁定操作的
ConcurrentHashMap实现原理: --- 依赖于Java内存模型
参考:http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
ConcurrentHashMap结果中包含Segment的数组,默认并发基本,创建包含16个Segment对象的数组,
每个Segment又包含若干个散列表的桶,每个桶是由HashEntry连接起来的一个链表,
如果key能够均匀散列,每个Segment大约守护整个散列表桶总数的1/16
并发读些操作:
执行 put 方法的时候,会需要加锁来完成,但加锁操作是针对的hash值对应的Segment,而不是整个ConcurrentHashMap,因为put操作只是在某个Segment中完成,并不需要对整个ConcurrentHashMap加锁,
此时,其他线程可以对另外的Segment进行put操作,虽然该 Segment 被锁住了,但其他的 Segment 并没有加锁
同时,读线程并不会因为本线程的加锁而阻塞
在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作
总结:
散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。
ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。
在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
ConcurrentHashMap 的高并发性主要来自于三个方面:
用分离锁实现多个线程间的更深层次的共享访问。
用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
使用分离锁,减小了请求 同一个锁的频率。
通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。
通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高
29.关于Comparable和Comparator接口
1.Comparable和Comparator接口被用来对对象集合或者数组进行排序
2.内部排序,对象类实现Comparable接口,重写 CompareTo()方法
2.外部集合排序,调用Collections.sort(collection, Comparator<T>)对集合元素排序
eg:
Collections.sort(list,new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
if (user1.getName().equals(user2.getName())) {
return user1.getAge() - user2.getAge();
} else {
return user1.getName().compareTo(user2.getName());
}
}
});
30.OAuth认证流程
参考:http://www.code123.cc/1671.html
31.Java NIO
参考:
概念:
专门为提高I/O吞吐量而设计,NIO通过Reactor模式的事件驱动机制来达到 Non Blocking
Reactor -- 反应器
将事件注册到Reactor中,当有相应的事件发生时,Reactor告诉我们哪些事情发生了,我们根据具体的事件去做相应的处理
通道和缓冲区: 标准IO基于字节流和字符流进行操作,而NIO基于通道Channel和缓冲区Buffer进行操作,数据总是从通道读取到缓冲区,或从缓冲区写入到通道中
异步IO:NIO可以异步的使用IO,当线程从通道读取数据到缓冲区时,线程还可进行其他事情,当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似
Selectors选择器:选择器用于监听多个通道的事件(eg:连接打开,数据到达等),因此,单线程可以监听多个数据通道
eg:
Selector运行单线程处理多个Channel,如果应用打开了多个连接(通道),但每个连接流量都很低,使用Selector就会很方便, 如:在一个聊天服务器中
优点:
旧IO对文件操作只能一个字节一个字节或一行一行的读,对Socket IO会阻塞,可以为每个Socket创建一个Thread,但开销太大
NIO 对Socket IO可以实现非阻塞,可用单线程管理多个通道,
NIO使用缓冲区,File IO和Socket IO都是和Buffer缓冲区交互读取
NIO将通道数据读到缓冲区中再进行操作,避免逐字节或逐行读取的性能开销
从Channel读取到Buffer -->
Channel Buffer
<--从Buffer写入到Channel
NIO和IO如何影响程序的设计?
对NIO和IO类的API调用
数据处理
用来处理数据的线程数
使用场景:
1.聊天服务器 需要管理同时打开的成千上万个连接,但这些连接每次只发送少量的数据
2.P2P网络中, 需要维持许多打开的连接到其他计算机上,使用一个单独的线程来管理所有出战连接
3.其他流量小, 连接多的场景
不适合场景:
1.少量连接占用高带宽,一次发送大量数据 --- 文件服务器(适合IO实现,一个连接通过一个线程处理)
NIO核心模块:
Selector(选择器):
1.Selector允许单线程处理多个Channel pk 旧IO的多线程处理, 性能更高
一个单线程选择器可监控多个通道的多个事件(eg:连接打开,数据到达等事件)
2.使用Selector,需要向它注册一个Channel,然后轮询调用它的select()方法,该方法将阻塞,
当注册的某个通道准备好要进行IO操作时,返回已选择键的个数,
此时通过selectedKeys获得已选择的键,就可进行相关的IO操作,
选择键SelectionKey 是用来连接 Selector 和 Channel
直到这里注册中的Channels中有一个准备好的事件,
一旦这个方法返回,这个线程将会执行这些事件
事件的实例是进来的连接,接收到的数据等等
3. 要使用Selector,得向Selector注册Channel,然后调用它的select()方法,
该方法会一直阻塞到某个注册的通道有事件就绪,一旦这个方法返回,线程就可以处理这些事件(新连接进来,数据接收等)
---> Channel
Thread ---> Selector ---> Channel
---> Channel
4.NIO的选择器允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个单独的线程来 "选择" 通道,
这些通道里已经有可以处理的输入,或选择已准备写入的通道, 这种机制,使得一个单独线程很容易来管理多个通道
Channel(通道):
1.4种Channel
FileChannel 文件IO
ServerSocketChannel TCP IO
SocketChannel TCP IO
DatagramChannel UDP IO
Buffer(缓冲区):
缓冲区 --- 内存中预留指定字节数的内存空间,用来对输入、输出的数据作临时存储, IO操作中数据的中转站
缓冲区直接为通道Channel服务,写入数据到通道,或从通道读取数据
8种缓冲区类:
对应boolean之外的7基本数据类型 + MappedByteBuffer(专门用于内存映射的ByteBuffer)
ByteBuffer <--继承 -- MappedByteBuffer 用于表示内存映射文件
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer读写数据的4个步骤:
写入数据到Buffer
调用flip()方法
从Buffer中读取数据
调用clear()或 compact()方法
说明:
1.当向Buffer写入数据时,buffer会记录下写了多少数据,
一旦要读取数据,调用flip()方法,将Buffer从写模式切换到读模式,在读模式下,可以读取之前写入到Buffer的所有数据
2.一旦读完所有数据,就要清空缓冲区,让它可再次被写入,2种清空缓冲区方式:
clear() --- 清空整个缓冲区
compact() --- 只清除已读过的数据,任何未读的数据都将被移到缓冲区的起始处,新写入的数据,将放到缓冲区未读数据的后面
缓冲区的 4 个属性: capacity>=limit>=position>=mark>=0
capacity
可容纳的最大数量,缓冲区创建时被设定,不能改变
limit
上界,缓冲区中当前数据量
读模式下:
表示最多能读到多少数据,当切换Buffer到读模式时,limit被设置为写模式下的position值,
即:能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
写模式下:
表示最多能往Buffer里写多少数据,limit等于Buffer的capacity
position
位置,下一个要被读、写的元素的索引
写数据到Buffer时,position表示当前的位置,初始位置为0,当数据写入到Buffer后,position向前移动到下一个可插入数据的Buffer单元
读数据时,同某个特定位置读,当将Buffer从写模式切换到读模式,position会被重置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置
初始:为 0,最大:capacity-1
mark
标记,调用mark()来设置mark=position,再调用reset()可以让position回复到标记的位置,即:position=mark
初始:为 -1
创建缓冲区
缓冲区类都是抽象的,不能new方式实例化,每个缓冲区类都有一个静态工厂方法,用于创建相应缓冲区对象
格式:
XxBuffer buf = XxBuffer.allocate(capacity)
eg:
//创建一个容量为10的byte缓冲区和char缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(10);
CharBuffer buf2 = CharBuffer.allocate(10);
如果想用一个指定大小的数组,作为缓冲区的数据存储器,可用wrap()方法创建缓冲区
eg:
//使用一个指定数组,作为缓冲区存储器
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes);
解析:
1.缓冲区数据会存在bytes数组中,bytes数组或buf缓冲区任何一方中数据的改动都会影响另一方
2.还可创建指定初始位置(position)和上界(limit)的缓冲区
//使用指定数组作为缓冲区存储器,并创建一个position=3,limit=8,capacity=10的缓冲区
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes, 3, 8);
操作缓冲区
1.存取
get() 从缓冲区取数据
put(xx) 向缓冲区存数据
channel.read(buf) 从Channel写到Buffer
channel.write(buf) 从Buffer读取数据到Channel
Buffer.rewind() 将position设回0, 可以重读Buffer中的所有数据,limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)
eg:
ByteBuffer buf = ByteBuffer.allocate(10);
//存3个数据
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'D');
//反转缓冲区,即从写模式切换到读模式,从头开始读,最多读取已写入个数的数据
buf.flip();
//读取2次数据
System.out.println((char)buf.get());
System.out.println((char)buf.get());
//返回当前位置到上界的数据元素数量
System.out.println(buf.remaining());
//从当前位置到上界是否还有数据元素
System.out.println(buf.hasRemaining());
解析:
1.调用put()或get()时,每调用一次,position值 +1,指示下次存或取开始的位置
2.buf.flip() 从写模式切换到读模式,可以从头开始读取最多已存入个数的数据
3.Buffer.remaining():返回从当前位置到上界的数据元素数量;
Buffer.hasRemaining():告诉我们从当前位置到上界是否有数据元素;
2.反转 flip()
将一个处于存数据状态的缓冲区变为一个处于准备读取数据的状态
反转缓冲区,
即:将缓冲字节数组的指针设置为数组的开始序列,即:数组下标0,这样才能从buffer开头,对buffer进行遍历(读取)
即:调用flip()后,读写指针知道缓冲区头部,并且设置了最多只能读取已已写入的数据长度
如果不调用flip(),就会从文件最好开始读取
特别注意:
buffer.flip() 作用:将Buffer从写模式切换到读模式,调用flip()方法设置这个position的值为0,以及设置这个limit的值为刚才position的值,
换句话说,position现在标记了读的位置,limit标记了有多少个Buffer的字节,字符等等被写入到buffer。限制有多少个字节,字符可以去读取的。
flip源码:
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;
}
3.清空数据
1.清空缓冲区内所有数据 --- clear()
2.清空已读取的数据 --- compact()
在buffer中仍然有未读取的数据,并且你想稍后读取,调用compact(),
compact()方法拷贝所有未读取的数据到buffer的开头,然后设置position值为最后未读取数据元素的后面,
再写数据时,从已有数据后面继续写
4.标记 --- mark()
记住当前位置,之后可以将位置恢复到标记处(使用reset()方法恢复)
通过调用Buffer.mark(),可以标记Buffer中的一个特定的position,
之后可通过调用Buffer.reset() 恢复到这个position
eg:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
5.比较2个缓冲区是否相等
6.批量移动缓冲区的数据
public static void batchMove(){
byte[] bytes = "hello nio".getBytes();
/**这里,可以直接把数据交给数组来保存
ByteBuffer buf = ByteBuffer.wrap(bytes);
*/
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
//将byte数据写入缓冲区 <=等价=> buf.put(bytes);
buf.put(bytes, 0, bytes.length);
//反转缓冲区,变为读模式
buf.flip();
//轮询判断是否有数据,有则将缓冲区数据批量读到array中
byte[] array = new byte[bytes.length];
while (buf.hasRemaining()){
buf.get(array, 0, buf.remaining());
}
//输出缓冲区读出的数据
System.out.println(new String(array));
}
7.复制缓冲区
通道之间的数据传输:
NIO中,如果2个通道中有一个是FileChannel, 则可以直接将数据从一个Channel传输到另外一个Channel
transferFrom() --- 对目标Channel调用
FileChannel的transferFrom()方法可将数据从源通道传入到FileChannel中
两种方式:
toChannel.transferFrom(fromChannel, position, count)
toChannel.transferFrom(position, count, fromChannel)
transferTo() --- 对源Channel调用
将数据从FileChannel传输到其他Channel中
eg:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Selector选择器:
NIO能检测到一到多个通道,并能知道通道的各类事件,一个单独的线程可以管理多个Channel,从而管理多个网络连接
1.Selector创建
Selector selector = Selector.open();
2.向Selector中注册通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ)
注意:
1.与Selector一起使用时,Channel必须处于非阻塞模式下,所以:不能将FileChannel于Selector一起使用,因为FileChannel不能切换到非阻塞模式
2.Channel通过register()方法注册到Selector,并标明它感兴趣的事件,之后通过Selector的select()判断是否有感兴趣的事件发生,如果有,通过selectedKeys()获得兴趣事件的集合
3.register(selector, Key)中,Key 表示通过Selector监听Channel时,对什么事件感兴趣
4种不同类型事件:
Connect
Accept
Read
Write
3.SelectionKey
向Selector注册Channel时,register()方法返回一个SelectionKey对象,这个对象包含感兴趣的属性:
interest集合
ready集合
Channel
Selector
附加的对象(可选)
4.通过Selector选择通道
select() 方法 返回 "感兴趣事件(连接,接受,读,写)" 已经就绪的那些通道
NIO聊天室参考:
32.Java中线程池原理和实现
JDK 线程池
Executors -创建-> 4种线程池 -实现-> ExecutorService(接口) -继承-> Executor(接口)
4种线程池
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
--- 单线程执行,按提交的线程顺序依次执行
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
--- 推荐方式,超过线程池最大容量,线程等待被调用,
--- 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行,延迟执行
eg:
/**
* 使用线程池定时延迟调度
*/
private static void test2(){
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延迟3秒后执行
scheduledExecutorService.schedule(getThread(),3, TimeUnit.SECONDS);
//延迟1秒后,每3秒执行一次
scheduledExecutorService.scheduleAtFixedRate(getThread(),1,3, TimeUnit.SECONDS);
}
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程(空闲线程默认60s会被回收),若无可回收,则新建线程
--- 不推荐,如果一次提交过多线程,而且每个线程比较耗时耗内存,可能瞬间挤爆JVM内存!!!
1.创建
ExecutorService executorService = Executors.newXxThreadExecutor()
2.调用
1.不要返回值
void executorService(Runnable)
2.需要返回值
Future<T> submit(Callable<T>)
3.如果需要 定时或 延迟 执行线程, 使用 ScheduledExecutorService的schedule()和scheduleAtFixedRate() 调用线程
3.ThreadPoolExecutor是Executors类的底层实现,
ThreadPoolExecutor的构造器:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize-池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
4.关于线程等待队列
1.newFixedThreadPool和newSingleThreadExecutor 线程等待队列是 LinkedBlockingQueue --- 无界阻塞队列
2.newCachedThreadPool 线程等待队列是 SynchronousQueue --- 同步阻塞队列 --- 每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其 实没有任何一个元素,或者说容量是0
3.ScheduledThreadPoolExecutor 线程等待队列是 DelayedWorkQueue --- 延迟阻塞队列
如何选择线程阻塞队列?
1.直接提交的无界队列 SynchronousQueue
2.无界队列 LinkedBlockingQueue
3.有界队列 ArrayBlockingQueue
关于SynchronousQueue?
1.该Queue,在某次添加元素后,必须等待其他线程取走后才能继续添加 (类似,进栈的元素,必须出栈,才能有新的元素被放入)
2.可避免在处理具有内部依赖性的请求集时出现锁
eg:
如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,
A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中
5.线程池的4个组成:
1.线程池管理器(ThreadPool) --- 创建并管理线程池,包括:创建线程池,销毁线程池,添加新任务
2.工作线程(PoolWorker) --- 线程池中线程,没有任务时处于等待状态,可以循环执行任务
3.任务接口(Task) --- 每个任务必须实现的接口,以便工作线程调度 --- Runnable
4.任务队列(taskQueue) --- 存放没有处理的任务,提供一种缓冲机制
6.JDK线程池源码分析:
33.JDK动态代理和cglib字节码技术代理的区别?
1.JDK动态代理:
1.静态代理 --- 代理对象和目标对象实现了相同的接口,目标对象作为代理对象的一个属性,
具体接口实现中,可以调用目标对象相应方法前后加上其他业务处理逻辑
2.JDK动态代理只能针对实现了接口的类生成代理
2.CGLIB代理 --- 通过字节码技术,为目标对象生成一个与其功能一样的子类
1.针对类实现代理
2.主要是对指定的类生产一个子类,覆盖其中的所有方法
3.被代理类或方法不能声明为final
3.区别:
1.JDK动态代理只能对实现了接口的类生成代理, 动态代理只能对于接口进行代理
2.cglib针对类实现代理,主要是对指定的类生成一个子类,覆盖中的方法,因为是继承,所以该类或方法最好不要声明成final ,final可以阻止继承和多态
3.Spring实现中,如果有接口,默认使用JDK动态代理,如果目标对象没有实现接口,使用cglib代理,
如果目标对象实现了接口,可以强制使用CGLIB实现代理(添加CGLIB库,并在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
4.动态代理的应用
AOP(Aspect-OrientedProgramming,面向切面编程),AOP包括切面(aspect)、通知(advice)、连接点(joinpoint),实现方式就是通过对目标对象的代理在连接点前后加入通知,完成统一的切面操作。
实现AOP的技术,主要分为两大类:
一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。
默认的策略是如果目标类是接口,则使用JDK动态代理技术,如果目标对象没有实现接口,则默认会采用CGLIB代理。
如果目标对象实现了接口,可以强制使用CGLIB实现代理(添加CGLIB库,并在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
5.参考:
34.Spring的事务传播级别
Spring中定义了7种传播行为:
参考:https://yq.aliyun.com/articles/71303?spm=5176.8067842.tagmain.14.VE7RJr
35.关于Spring 声明式事务的原理
参考:http://yemengying.com/2016/11/14/something-about-spring-transaction/
Spring的声明式事务:
1.JavaConfig方法 --- 在需要管理事务的类或方法上添加 @Transactional注解,然后在配置类上添加 @EnableTransactionManagement注解
2.Xml方式 --- 添加 <tx:annotation-driven />
Spring会利用Aop在相关方法调用的前后进行事务管理
问题:
public class JayServiceImpl implements JayService {
public void A(List<Giraffe> giraffes) {
for (Giraffe giraffe : giraffes) {
B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
说明:
Service中A方法调用B方法,方法A没有事务管理,方法B采用声明式事务,通过在方法上声明 @Transactional注解来做事务管理
问题:
Junit 测试方法 A 的时候发现方法 B 的事务并没有开启, 而直接调用方法 B 事务是正常开启的???
// 没有开启事务
@Test
public void testA() {
giraffeService.A();
}
// 正常开启事务
@Test
public void testB() {
giraffeService.B();
}
}
原理分析:
Spring在加载目标Bean时,会为声明了@Transactional的Bean创建一个代理类,而目标类本身并不能感知到代理类的存在,
调用通过Spring上下文注入的Bean的方法,而不是直接调用目标类的方法
即:
先调用代理类的方法,代理类再调用目标类的方法
Calling Code
--call--> Proxy --->foo()
---> Pojo --> pojo.foo()
对于加了@Transactional注解的方法,在调用代理类方法时,会先通过拦截器 TransactionInterceptor开启事务,
然后再调用目标类的方法,最后在调用结束后, TransactionInterceptor会提交或回滚事务
问题解析:
对于第一段的代码,我在方法 A 中调用方法 B,实际上是通过“this”的引用,也就是直接调用了目标类的方法,而非通过 Spring 上下文获得的代理类,所以。。。事务是不会开启滴
解决方法:
通过实现ApplicationContextAware接口获得 Spring 的上下文,(或自动注入Context对象),然后获得目标类的代理类,通过代理类的对象,调用方法 B,即可
public class GiraffeServiceImpl implements GiraffeService,ApplicationContextAware{
@Setter
private ApplicationContext applicationContext;
public void A(List<Giraffe> giraffes) {
GiraffeService service = applicationContext.getBean(GiraffeService.class);
for (Giraffe giraffe : giraffes) {
service.B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
36.Java类加载机制
装载 ---> 链接(验证 --> 准备 --> 解析) ---> 初始化
1.JVM类加载机制:
装载:
1.找到该类型的class文件,产生一个该类型的class文件二进制数据流(ClassLoader需要实现的loadClassData()方法)
2.解析该二进制数据流为方法区内的数据结构
3.创建一个该类型的java.lang.Class实例
最终:通过defineClass()创建一个Java类型对象(Class对象)
找到二进制字节码,并加载到JVM中
JVM通过类全限定名(包名.类名) + 类加载器 完成类的加载,生成类对应的Class对象
链接:
验证:
负责对二进制字节码进行校验、类信息是否符合JVM规范,有没有安全问题、对class文件长度和类型进行检查
参考:http://www.importnew.com/17105.html
准备:
初始化类中静态变量、并将其初始化为默认值 --- 只初始化静态变量默认值 !!!,给其类变量赋值发生在初始化阶段!!!
对于final类型的变量,准备阶段直接赋初始值
该内存分配发生在方法区
解析:
解析类中调用的接口、类、字段、方法的符号引用,把虚拟机常量池中的符号引用转换为直接引用
初始化:
1.对static类变量指定初始值!!!(2种方式:一种是通过类变量的初始化语句,一种是静态初始化语句)
2.一个类的初始化需要先初始化其父类,并递归初始化其祖先类
2.JVM必须在每个类或接口主动使用时进行初始化:
主动使用的情况:
1.创建类的实例(无论是new、还是反射、克隆、序列化创建的)
2.使用某个类的静态方法
3.访问某个类或即可的静态字段
4.调用Java API中的某些反射方法
5.初始化某个类的子类(先初始化其父类)
6.启动某个标明为启动类的类(含main()方法)
主动使用会导致类的初始化,其超类均将在该类的初始化之前被初始化,但通过子类访问父类的静态字段或方法时,对于子类(或子接口、接口的实现类)来说,这种访问就是被动访问,或者说访问了该类(接口)中的不在该类(接口)中声明的静态成员
3.创建对象时,类中各成员的执行顺序:
父静态块 <-- 子静态块 <-- 父普通代码块 <-- 父构造器 <-- 子普通代码块 <-- 子构造器
1.父类静态成员和静态初始化快,按在代码中出现的顺序依次执行。
2.子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
3. 父类的实例成员和实例初始化块,按在代码中出现的顺序依次执行。
4.执行父类的构造方法。
5.子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
6.执行子类的构造方法。
eg:
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}
class Parent{
{
System.out.println("parent中的初始化块");
}
static{
System.out.println("parent中static初始化块");
}
public Parent(){
System.out.println("parent构造方法");
}
}
class Son extends Parent{
{
System.out.println("son中的初始化块");
}
static{
System.out.println("son中的static初始化块");
}
public Son(){
System.out.println("son构造方法");
}
}
结果:
parent中static初始化块
son中的static初始化块
parent中的初始化块
parent构造方法
son中的初始化块
son构造方法
37.MySQL性能优化
参考:
MySQL性能优化总结:
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存储引擎选择
参考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事务处理,为每个表创建3个文件,分别存储不同内容
支持表级锁,表的写操作会阻塞其他用户对同一个表的读和写操作,并发度低
1.myISAM表的读操作,不会阻塞其他用户对同一个表的读请求,但会阻塞对同一个表的写请求。
2.myISAM表的写操作,会阻塞其他用户对同一个表的读和写操作。
3.myISAM表的读、写操作之间、以及写操作之间是串行的
eg:
tb_Demo表,那么就会生成以下三个文件:
1.tb_demo.frm,存储表定义;
2.tb_demo.MYD,存储数据;
3.tb_demo.MYI,存储索引
适合场景:
1.选择密集型表 --- MyISAM引擎在筛选大量数据时非常迅速 --- 查询快
2.插入密集型表 --- 并发插入特性允许同时选择和插入数据,适合管理:邮件或Web服务器日志数据
总结:
1.适合做count的计算 (注意:不含where条件的统计,因为MyISAM会记录表的行数)
2.插入不频繁,查询非常频繁
3.没有事务需求
InnoDB: 默认引擎
支持事务处理
引入了行级锁(并发高)和外键约束
不支持全文索引
适合场景:
1.更新密集型表 --- 特别适合处理多重并发的更新请求
2.事务
3.自动灾难恢复 --- InnoDB表能够自动从灾难中恢复
4.外键约束 --- MySQL支持外键的存储引擎只有InnoDB
5.支持自动增加列 Auto_INCREMNET属性
6.InnoDB是为处理巨大数据量时的最大性能设计
总结:
可靠性要求高,需要事务支持,并有较高的并发读取频率,适合InnoDB
行锁机制必然决定了写入时的更多性能开销,而它的强项在于多线程的并发处理
表更新和查询都相当的频繁,并且表锁定的机会比较大的情况指定数据引擎的创建
细节和具体实现的差别:
1.InnoDB不支持FULLTEXT类型的索引。
2.InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的。
3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。
4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。
另外,InnoDB表的行锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”
任何一种表都不是万能的,只用恰当的针对业务类型来选择合适的表类型,才能最大的发挥MySQL的性能优势。
存储引擎选择依据?
是否需要支持事务;
是否需要使用热备;
崩溃恢复:能否接受崩溃;
是否需要外键支持;
是否需要全文索引
经常使用什么样的查询模式
数据量大小
eg:
需要事务和外键约束 -- InnoDB
需要全文索引 -- MyISAM
数据量大,倾向于InnoDB,因为它支持事务处理和故障恢复,InnoDB可以利用事务日志进行数据恢复,这会比较快。而MyISAM可能会需要几个小时甚至几天来干这些事,InnoDB只需要几分钟
操作数据表的习惯,也会影响性能
eg:
COUNT() 在 MyISAM 表中会非常快,而在InnoDB 表下可能会很痛苦(
因为InnoDB不保存表的行数,即:执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可,
注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的)
主键查询在InnoDB下非常快,但如果主键太长也会导致性能问题
大批的inserts语句在MyISAM下回快一些,但updates语句在InnoDB下更快(尤其在并发量大的时候)
提示InnoDB性能的方法:
InnoDB支持事务,存储过程,视图和行级锁,在高并发下,表现比MyISAM强很多
影响性能的配置:
innodb_flush_log_at_trx_commit 这个选项,如果设置为1的话,那么每次插入数据的时候都会自动提交,导致性能急剧下降,应该是跟刷新日志有关系,设置为0效率能够看到明显提升
当然,同 样你可以SQL中提交“SET AUTOCOMMIT = 0”来设置达到好的性能
设置innodb_buffer_pool_size能够提升InnoDB的性能
设置查询缓存
2.配置文件my.ini参数优化
1.max_connections --- 最大并发连接数,允许的同时客户连接数, 默认100, 建议根据需求设定,eg:1024
2.query_cache_size=0 --- 查询缓存,用于缓存select 查询结果,如果有许多返回相同查询结果的SELECT查询,并且很少改变表,可以设置query_cache_size大于0,可以极大改善查询效率。而如果表数据频繁变化,就不要使用这个,会适得其反
3.table_cache=256
4.thread_cache_size --- 缓存的最大线程数
5.sort_buffer --- 每个需要进行排序的线程分配该大小的一个缓冲区
6.wait_timeout --- 默认是28800秒,也就是说一个connection空闲超过8个小时,Mysql将自动断开该connection,通俗的讲就是一个连接在8小时内没有活动,就会自动断开该连接。 不要设置太长,建议 7200
7.default-storage-engine=INNODB # 创建新表时将使用的默认存储引擎
配置示例,2G内存,针对站多,抗压型的设置,最佳:
table_cache=1024 物理内存越大,设置就越大.默认为2402,调到512-1024最佳
innodb_additional_mem_pool_size=4M 默认为2M
innodb_flush_log_at_trx_commit=1
(设置为0就是等到innodb_log_buffer_size列队满后再统一储存,默认为1)
innodb_log_buffer_size=2M 默认为1M
innodb_thread_concurrency=8 你的服务器CPU有几个就设置为几,建议用默认一般为8
key_buffer_size=256M 默认为218 调到128最佳
tmp_table_size=64M 默认为16M 调到64-256最挂
read_buffer_size=4M 默认为64K
read_rnd_buffer_size=16M 默认为256K
sort_buffer_size=32M 默认为256K
max_connections=1024 默认为1210
thread_cache_size=120 默认为60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多参考:
3.Query查询优化
1.explain sql 查看执行效率,定位优化对象的性能瓶颈
2.永远用小结果驱动大的结果集
3.尽可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的过滤条件
6.用表连接代替子查询
7.当只要一行数据时,使用limit 1
8.为搜索字段建立索引
9.千万不要ORDER BY RAND(),避免select *
10.尽可能使用NOT NULL
11.开启查询缓存,并为查询缓存优化查询语句
eg:
select username from user where add_time >= now()
注意:
1.这样的语句不会使用查询缓存,
2.像NOW()和RAND()或是其它的诸如此类的SQL函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替MySQL的函数,从而开启缓存
3.修改, 对now()进行处理,只取年月日 yyyy-MM-dd,变为一个不衣变的值
38.JVM性能优化
1.
2.Java代码性能优化
1.没必要尽量不要使用静态变量
2.充分利用单例机制减少对资源的加载,缩短运行的时间,提高系统效率
单例适用场景:
1. 控制资源的使用,通过线程同步来控制资源的并发访问;
2. 控制实例的产生,以达到节约资源的目的
3.减少对象创建,最大限度的重用对象
尽量避免在经常调用的方法中循环使用new对象 --- 享元模式(可减少对象多次创建)
4.使用final修饰符
5.尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在分配给改方法的栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢
6.学会用StringBuilder和StringBuffer,并尽量确定其容量
单线程使用StringBuilder,多线程情况下使用StringBuffer,这样性能会有很大提升
7.尽量使用基本数据类型代替对象 eg:字符串创建
8.使用HashMa、ArrayList,HashTable、Vector等使用在多线程的场合,内部使用了同步机制,这个会降低程序的性能
9.深入理解HashMap原理
当你要创建一个比较大的hashMap时,充分利用另一个构造函数
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认initialCapacity只有16,
而 loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理
10.尽量在finally块中释放资源
11.尽早释放无用对象的引用
12.尽量避免使用split,split由于支持正则表达式,所以效率比较低,考虑使用apache的 StringUtils.split(string,char),频繁split的可以缓存结果
13.尽量使用System.arraycopy ()代替通过来循环复制数组
System.arraycopy()要比通过循环来复制数组快的多
14..尽量缓存经常使用的对象 推荐:redis缓存
15.尽量避免非常大的内存分配
参考:http://developer.51cto.com/art/201511/496263.htm
61.MySQL性能优化
参考:
MySQL性能优化总结:
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存储引擎选择
参考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事务处理,为每个表创建3个文件,分别存储不同内容
支持表级锁,表的写操作会阻塞其他用户对同一个表的读和写操作,并发度低
1.myISAM表的读操作,不会阻塞其他用户对同一个表的读请求,但会阻塞对同一个表的写请求。
2.myISAM表的写操作,会阻塞其他用户对同一个表的读和写操作。
3.myISAM表的读、写操作之间、以及写操作之间是串行的
eg:
tb_Demo表,那么就会生成以下三个文件:
1.tb_demo.frm,存储表定义;
2.tb_demo.MYD,存储数据;
3.tb_demo.MYI,存储索引
适合场景:
1.选择密集型表 --- MyISAM引擎在筛选大量数据时非常迅速 --- 查询快
2.插入密集型表 --- 并发插入特性允许同时选择和插入数据,适合管理:邮件或Web服务器日志数据
总结:
1.适合做count的计算 (注意:不含where条件的统计,因为MyISAM会记录表的行数)
2.插入不频繁,查询非常频繁
3.没有事务需求
InnoDB: 默认引擎
支持事务处理
引入了行级锁(并发高)和外键约束
不支持全文索引
适合场景:
1.更新密集型表 --- 特别适合处理多重并发的更新请求
2.事务
3.自动灾难恢复 --- InnoDB表能够自动从灾难中恢复
4.外键约束 --- MySQL支持外键的存储引擎只有InnoDB
5.支持自动增加列 Auto_INCREMNET属性
6.InnoDB是为处理巨大数据量时的最大性能设计
总结:
可靠性要求高,需要事务支持,并有较高的并发读取频率,适合InnoDB
行锁机制必然决定了写入时的更多性能开销,而它的强项在于多线程的并发处理
表更新和查询都相当的频繁,并且表锁定的机会比较大的情况指定数据引擎的创建
细节和具体实现的差别:
1.InnoDB不支持FULLTEXT类型的索引。
2.InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的。
3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。
4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。
另外,InnoDB表的行锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”
任何一种表都不是万能的,只用恰当的针对业务类型来选择合适的表类型,才能最大的发挥MySQL的性能优势。
存储引擎选择依据?
是否需要支持事务;
是否需要使用热备;
崩溃恢复:能否接受崩溃;
是否需要外键支持;
是否需要全文索引
经常使用什么样的查询模式
数据量大小
eg:
需要事务和外键约束 -- InnoDB
需要全文索引 -- MyISAM
数据量大,倾向于InnoDB,因为它支持事务处理和故障恢复,InnoDB可以利用事务日志进行数据恢复,这会比较快。而MyISAM可能会需要几个小时甚至几天来干这些事,InnoDB只需要几分钟
操作数据表的习惯,也会影响性能
eg:
COUNT() 在 MyISAM 表中会非常快,而在InnoDB 表下可能会很痛苦(
因为InnoDB不保存表的行数,即:执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可,
注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的)
主键查询在InnoDB下非常快,但如果主键太长也会导致性能问题
大批的inserts语句在MyISAM下回快一些,但updates语句在InnoDB下更快(尤其在并发量大的时候)
提示InnoDB性能的方法:
InnoDB支持事务,存储过程,视图和行级锁,在高并发下,表现比MyISAM强很多
影响性能的配置:
innodb_flush_log_at_trx_commit 这个选项,如果设置为1的话,那么每次插入数据的时候都会自动提交,导致性能急剧下降,应该是跟刷新日志有关系,设置为0效率能够看到明显提升
当然,同 样你可以SQL中提交“SET AUTOCOMMIT = 0”来设置达到好的性能
设置innodb_buffer_pool_size能够提升InnoDB的性能
设置查询缓存
2.配置文件my.ini参数优化
1.max_connections --- 最大并发连接数,允许的同时客户连接数, 默认100, 建议根据需求设定,eg:1024
2.query_cache_size=0 --- 查询缓存,用于缓存select 查询结果,如果有许多返回相同查询结果的SELECT查询,并且很少改变表,可以设置query_cache_size大于0,可以极大改善查询效率。而如果表数据频繁变化,就不要使用这个,会适得其反
3.table_cache=256
4.thread_cache_size --- 缓存的最大线程数
5.sort_buffer --- 每个需要进行排序的线程分配该大小的一个缓冲区
6.wait_timeout --- 默认是28800秒,也就是说一个connection空闲超过8个小时,Mysql将自动断开该connection,通俗的讲就是一个连接在8小时内没有活动,就会自动断开该连接。 不要设置太长,建议 7200
7.default-storage-engine=INNODB # 创建新表时将使用的默认存储引擎
配置示例,2G内存,针对站多,抗压型的设置,最佳:
table_cache=1024 物理内存越大,设置就越大.默认为2402,调到512-1024最佳
innodb_additional_mem_pool_size=4M 默认为2M
innodb_flush_log_at_trx_commit=1
(设置为0就是等到innodb_log_buffer_size列队满后再统一储存,默认为1)
innodb_log_buffer_size=2M 默认为1M
innodb_thread_concurrency=8 你的服务器CPU有几个就设置为几,建议用默认一般为8
key_buffer_size=256M 默认为218 调到128最佳
tmp_table_size=64M 默认为16M 调到64-256最挂
read_buffer_size=4M 默认为64K
read_rnd_buffer_size=16M 默认为256K
sort_buffer_size=32M 默认为256K
max_connections=1024 默认为1210
thread_cache_size=120 默认为60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多参考:
3.Query查询优化
1.explain sql 查看执行效率,定位优化对象的性能瓶颈
2.永远用小结果驱动大的结果集
3.尽可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的过滤条件
6.用表连接代替子查询
7.当只要一行数据时,使用limit 1
8.为搜索字段建立索引
9.千万不要ORDER BY RAND(),避免select *
10.尽可能使用NOT NULL
11.开启查询缓存,并为查询缓存优化查询语句
eg:
select username from user where add_time >= now()
注意:
1.这样的语句不会使用查询缓存,
2.像NOW()和RAND()或是其它的诸如此类的SQL函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替MySQL的函数,从而开启缓存
3.修改, 对now()进行处理,只取年月日 yyyy-MM-dd,变为一个不衣变的值
62.Java常见的锁类型有哪些?请简述其特点。
1、synchronized对象同步锁:synchronized是对对象加锁,可作用于对象、方法(相当于对this对象加锁)、静态方法(相当于对Class实例对象加锁,锁住的该类的所有对象)以保证并发环境的线程安全。同一时刻只有一个线程可以获得锁。
其底层实现是通过使用对象监视器Monitor,每个对象都有一个监视器,当线程试图获取Synchronized锁定的对象时,就会去请求对象监视器(Monitor.Enter()方法),如果监视器空闲,则请求成功,会获取执行锁定代码的权利;如果监视器已被其他线程持有,线程进入同步队列等待。
2、Lock同步锁:与synchronized功能类似,可从Lock与synchronized区别进行分析:
1、Lock可以通过tryLock()方法非阻塞地获取锁而。如果获取了锁即立刻返回true,否则立刻返回false。这个方法还有加上定时等待的重载方法tryLock(long time, TimeUnit unit)方法,在定时期间内,如果获取了锁立刻返回true,否则在定时结束后返回false。在定时等待期间可以被中断,抛出InterruptException异常。而Synchronized在获得锁的过程中是不可被中断的。
2、Lock可以通过lockInterrupt()方法可中断的获取锁,与lock()方法不同的是等待时可以响应中断,抛出InterruptException异常。
3、Synchronized是隐式的加锁解锁,而Lock必须显示的加锁解锁,而且解锁应放到finnally中,保证一定会被解锁,而Synchronized在出现异常时也会自动解锁。但也因为这样,Lock更加灵活。
4、Synchronized是JVM层面上的设计,对对象加锁,基于对象监视器。Lock是代码实现的。
3、可重入锁:ReentrantLock与Synchronized都是可重入锁。可重入意味着,获得锁的线程可递归的再次获取锁。当所有锁释放后,其他线程才可以获取锁。
4、公平锁与非公平锁:“公平性”是指是否等待最久的线程就会获得资源。如果获得锁的顺序是顺序的,那么就是公平的。不公平锁一般效率高于公平锁。ReentrantLock可以通过构造函数参数控制锁是否公平。
5、ReentrantReadWriteLock读写锁:是一种非排它锁, 一般的锁都是排他锁,就是同一时刻只有一个线程可以访问,比如Synchronized和Lock。读写锁就多个线程可以同时获取读锁读资源,当有写操作的时候,获取写锁,写操作之后的读写操作都将被阻塞,直到写锁释放。读写锁适合写操作较多的场景,效率较高。
6、乐观锁与悲观锁:在Java中的实际应用类并不多,大多用在数据库锁上,可参看:
7、死锁:是当两个线程互相等待获取对方的对象监视器时就会发生死锁。一旦出现死锁,整个程序既不会出现异常也不会有提示,但所有线程都处于阻塞状态。死锁一般出现于多个同步监视器的情况。
63.volatile与automicInteger是什么?如何使用?
在并发环境中有三个因素需要慎重考量,原子性、可见性、有序性。
voatile 保证了有序性(防止指令冲排序)和变量的内存可见性(每次都强制取主存数据),每次取到volatile变量一定是最新的
volatile主要用于解决可见性,它修饰变量,相当于对当前语句前后加上了“内存栅栏”。使当前代码之前的代码不会被重排到当前代码之后,当前代码之后的指令不会被重排到当前代码之前,一定程度保证了有序性。而volatile最主要的作用是使修改volatile修饰的变量值时会使所有线程中的缓存失效,并强制写入公共主存,保证了各个线程的一致。可以看做是轻量级的Synchronized。详情可参看:。
automicXXX主要用于解决原子性,有一个很经典的问题:i++是原子性的操作码?答案是不是,它其实是两步操作,一步是取i的值,一步是++。在取值之后如果有另外的线程去修改这个值,那么当前线程的i值就是旧数据,会影响最后的运算结果。使用automicXXX就可以非阻塞、保证原子性的对数据进行增减操作。详情可参看:http://ifeve.com/java-atomic/
注:在此列举的只是Java多线程最基础的知识,也是面试官最常问到的,先打牢基础,再去探讨底层原理或者高级用法,除了这十个问题,在此再推荐一些其他的资料:
JVM底层又是如何实现synchronized的:http://www.open-open.com/lib/view/open1352431526366.html
Java线程池详解:
Java线程池深度解析:
ConcurrentHashMap原理分析:
Java阻塞队列详解:http://ifeve.com/java-blocking-queue/
64.MyBatis中timestamp时间类型,在Spring MVC出参时无法转为正确的时间类型?
在xml中配置: javaType为 java.sql.Timestamp
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" javaType="java.sql.Timestamp"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" javaType="java.sql.Timestamp"/>
65.Datatable自定义搜索
//自定义搜索,每次只能根据一个维度进行搜索(按渠道或产品类型)
$("#channel_select,#brand_select").change(function(){
var tsval = $(this).val()
table.search(tsval, false, false).draw();
});
66.synchronized和Lock的底层实现原理?
参考:http://www.open-open.com/lib/view/open1352431526366.html
锁原理:
synchronized 在软件层面依赖JVM
Lock 在硬件层面依赖特殊的CPU指令 --- CAS + JNI调用CPU指令来实现
synchronized 可以吧任何一个非 null 对象作为 "锁",
作用于方法上时,锁住的是对象实例this,
作用于静态方法,锁住的是对象对应的Class实例,因为Class数据存储在永久带,因此静态方法锁相当于该类的全局锁,
作用于某个对象实例,锁住的是对应的代码块
HotSpot JVM中,锁 --- 对象监视器(对象来监视线程的互斥) --- synchronized的实现原理
对象监视器,设置几种状态来区分请求的线程:
Contention Set: 所有请求锁的线程,被首先放置到该竞争队列 --- 先进后出的虚拟队列,会被线程并发访问
Entry Set:
等待获取锁的线程(来自Contention Set)排队队列
--- 等待获取对象锁运行
Wait Set:
获取锁后,调用wait()方法,被阻塞的线程队列
--- 等待再次获取对象锁
OnDeck: 任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner: 获得锁的线程称为Owner
!Owner: 释放锁的线程
说明:
1.Entry Set 和Contention Set 同属等待队列,
2.Contention Set会被线程并发访问,为了降低对Contention Set队尾的争用(为了减少加入与取出两个线程对于contentionList的竞争),而建立Entry Set,如果Entry Set为空,则从Contention Set队尾取出节点
3.Owner线程在unlock时,会从Contention Set中迁移线程到Entry Set,并会指定Entry Set中的某个线程(一般为Head)为Read(OnDeck)线程
4.Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁
5.OnDeck线程获得锁喉变为Owner线程,无法获得锁的线程依然留在Entry Set中
6.如果Owner线程被wait()方法阻塞,则转移后WaitSet中,如果某个时刻被notify/notifyAll唤醒,则再次转移到EntrySet
线程的互斥,其实是线程对同一个对象的监视器monitor的操作:
每个对象都有一个监视器(monitor)!,当monitor被占就会处于锁定状态,线程执行monitorentry指令时尝试获取monitor的所有权
1.如果monitor的进入数为0,则线程进入monitor,然后将进入数设置为1,线程即为monitor的所有者
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数 +1 --- 锁可重入 --- ReentrantLock 和synchronized 都是 可重入锁
3.其他线程已经占用了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再尝试获取monitor的所有权
4.线程调用一次unlock()释放锁,monitor的进入数就 -1 (只有monitor进入数为0,才能被其他线程抢占)
5.一个线程获取多少次锁,就必须释放多少次锁,对于synchronized内置锁 ,每一次进入和离开synchronized方法(代码块),就是一个完整的锁获取和释放
sleep()不会释放锁,等待指定时间后继续运行
wait()会释放锁,进入对象监视器的 Wait Set队列,等待被唤醒,被唤醒后,需要重新获取锁
wait()和notify/notifyAll必须成对出现,而且必须放在synchronized中,
yield() 不会释放锁, 只是让当前线程让出CPU占用权
Synchronized底层优化 --- 偏向锁、轻量级锁
参考:
http://www.jianshu.com/p/5dbb07c8d5d5
Synchronized效率低的原因?
Synchronized是通过对象内部的对象监视器锁(monitor)来实现的,monitor本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现,
操作系统实现线程间切换需要从用户态转到内核态(JVM转到操作系统内核),这个成本非常高,状态转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因
Synchronized底层优化:
1.锁的4种状态:
无锁状态、偏向锁、轻量级锁、重量级锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(锁升级只能从低到高升级,不会出现锁的降级)
JDK1.6默认开启偏向锁和轻量级锁,通过-XX:-UseBiasedLocking来禁用偏向锁
锁的状态保存在对象的头文件中
重量级锁 --- 依赖于操作系统Mutex Lock所实现的锁, 需要从JVM转到操作系统内核,进行互斥操作
轻量级锁 --- 并不是用来代替重量级锁,本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗
轻量级锁目的:
为了在线程交替执行同步块时提高性能 !!!
轻量级锁适用场景:
线程交替执行同步块的情况 ---- 锁竞争不激烈的情况!
如果存在同一时间访问同一个锁,就会导致轻量级锁升级为重量级锁
偏向锁 --- 为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径
一旦线程第一次获得了监视对象,之后让监视对象 "偏向"这个线程,在该线程重复获取锁时,避免CAS操作
即:
设置一个变量,如果发现是true,无需再走加锁、解锁的流程!
偏向锁目的:
解决无竞争下的锁性能问题,在只有一个线程执行同步块时,进一步提高性能
总结:
ynchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量 !!!
67.Spring如何解决Bean的循环依赖? --- 只支持Singleton作用域的, setter方式的循环依赖!!!
参考:
Spring容器循环依赖包括构造器循环依赖和setter循环依赖
如果是构造器循环依赖,Spring容器将无法启动,报循环依赖异常BeanCurrentlInCreationException
解决方式:
将构造器注入方式改为属性注入方式 --- setter
Spring 支持setter方法注入属性方式的循环依赖
Spring中将循环依赖的处理分3中情况:
1.构造器循环依赖 --- 原理, Spring 不支持构造器方式的循环依赖
通过构造器注入构成的循环依赖是无法解决的,只能在容器启动时抛出BeanCurrentlInCreationException异常 --- 表示循环依赖
Spring容器将每一个正在创建的Bean的标识符(id)放到 "当前创建bean池" 中,bean标识符在创建过程中将一直保持在这个池中,
如果在创建bean过程中,发现自己已经在 "当前创建bean池"里时,将抛出 BeanCurrentlInCreationException异常,表示循环依赖,
而对于创建完毕的bean将从 "当前创建bean池"中清除掉
eg:
如在创建TestA类时,构造器需要TestB类,那将去创建TestB,在创建TestB类时又发现需要TestC类,则又去创建TestC,
最终在创建TestC时发现又需要TestA,从而形成一个环,没办法创建 --- 循环依赖,抛出 BeanCurrentlInCreationException异常
配置文件;
<bean id="testA" class="com.bean.TestA">
<constructor-arg index="0" ref="testB"/>
</bean>
<bean id="testB" class="com.bean.TestB">
<constructor-arg index="0" ref="testC"/>
</bean>
<bean id="testC" class="com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>
测试用例:
@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test.xml");
} catch (Exception e) {
//因为要在创建testC时抛出;
Throwable ee1 = e.getCause().getCause().getCause();
throw e1;
}
}
分析:
Spring容器创建"testA"bean,首先去"当前创建bean池"查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数"testB",并将"testA"标识符放到"当前创建bean池"。
Spring容器创建"testB"bean,首先去"当前创建bean池"查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数"testC",并将"testB"标识符放到"当前创建bean池"。
Spring容器创建"testC"bean,首先去"当前创建bean池"查找是否当前bean正在创建,如果没发现,则继续准备其需要的构造器参数"testA",并将"testC"标识符放到"当前创建Bean池"。
到此为止Spring容器要去创建"testA"bean,发现该bean标识符在"当前创建bean池"中,因为表示循环依赖,抛出BeanCurrentlyInCreationException。
说明:
Spring中bean默认是单例的,对于singleton作用于的Bean,可通过setAllowCircularReferences(false)来禁用循环引用
2.Setter循环依赖 --- 原理, Spring支持setter方式注入属性的循环依赖!
setter注入方式构成的循环依赖,通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(eg:setter注入)的bean来完成的,
而且只能解决Singleton单例作用域的bean循环依赖,通过提前暴露一个单例工厂方法,从而使其他bean能引用到该bean(注意:此时仅仅只是生了一个bean,该bean还未调用其他方法,如setter注入)
对单例Bean循环依赖的处理:通过递归方法,找出当前Bean的所有依赖Bean,然后提前缓存起来
原理:
创建Bean A时,先通过无参构造器创建一个A实例,此时属性都是空的,但对象引用已经创建创建出来,然后把Bean A的引用提前暴露出来,
然后setter B属性时,创建B对象,此时同样通过无参构造器,构造一个B对象的引用,并将B对象引用暴露出来。
接着B执行setter方法,去池中找到A(因为此时,A已经暴露出来,有指向该对象的引用了),这样依赖B就构造完成,也初始化完成,然后A接着初始化完成,
循环依赖就这么解决了!!!
总结:
先创建对象引用,再通过setter()方式,给属性赋值,层层创建对象 !!!
Bean A初始化时,先对其依赖B进行初始化,同时,通过默认无参构造器,生成自己的引用,而不调用其setter()方法,
当B对象创建时,如果还依赖C,则也通过无参构造器,生成B的引用,
C对象创建时,如果引用了A,则去对象池中查到A的引用,然后调用setter()方式,注入A,完成C对象的创建
C创建完成后,B使用setter()方式,注入C,完成B对象创建,
B对象场景完成后,A使用setter()方式,注入B,完成A对象创建,
最终,完成setter()方式的循环依赖!
如果循环依赖的都是单例对象(都是通过setter方式注入属性的),那么这个肯定没问题,放心使用即可!!!
如果一个是单例,一个是原型,那么一定要保证单例对象能提前暴露出来,才可以正常注入属性!!!
3.prototype范围的依赖处理
对于"prototype"作用域bean,Spring容器无法完成依赖注入,因为Spring容器不进行缓存"prototype"作用域的bean,因此无法提前暴露一个创建中的bean
这个spring也无能为力,因为是原型对象,A创建的时候不会提前暴露出来,所以,每次都是要创建,创建的时候,发现有相同的对象正在创建,同样报错,循环依赖错误
4.Spring创建Bean的源码解释:
1.创建Bean的入口
AbstractBeanFactory-->doGetBean()
Object sharedInstance = getSingleton(beanName); //从缓存中查找,或者如果当前创建池中有并且已经暴露出来了,就返回这个对象
2.创建单例Bean方法
DefaultSingletonBeanRegistry-->getSingleton(String beanName, ObjectFactory<?> singletonFactory)
3.创建真正对象
AbstractAutowireCapableBeanFactory-->doCreateBean
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
} 注意这一步很关键,是调用构造方法创建一个实例对象,如果这个构造方法有参数,而且就是循环依赖的参数,那么这个对象就无法创建了,
因为到这里对象没有创建,也没有暴露当前对象,如果是无参的构造方法,那么就可以,先创建一个对象,尽管所有的属性都为空
68.Spring事务管理的原理?
参考:http://www.codeceo.com/article/spring-transactions.html
声明式事务管理,在Service之上或Service的方法之上,添加 @Transactional注解
@Transactional如何工作?
Spring在启动时,会去解析生成相关的Bean,这是会查看拥有相关注解的类和方法,
并且为这些类和方法生成代理,并根据 @Transactional的相关参数进行相关配置注入,
这样就在代理中把相关的事务处理掉了(开启正常提交事务,异常回滚事务)
真正的数据库层,事务提交和回滚是通过binlog和redo log实现的
Spring事务管理机制实现原理:
参考:
http://www.jianshu.com/p/4312162b1458
http://www.92to.com/bangong/2016/11-05/12533010.html
在调用一个需要事务的组件时,管理器首先判断当前调用(即:当前线程)有没有事务,如果没有事务则启动一个事务,并把事务与当前线程绑定,
Spring使用TransactionSynchronizationManager的bindResource方法将当前线程与一个事务绑定,采用的方式就是ThreadLocal,
参考:DataSourceTransactionManager的启动事务用的代码 doBegin()
通过动态代理或AOP方式,对所有需要事务管理的Bean进行加载,生成代理对象,并根据配置在invoke()方法中对当前调用的方法名进行判定,
并在method.invoke()方法前后为其加上合适的事务管理代码,根据method.invoke()执行结果,正常提交事务,异常回滚事务
实现了EntityManager接口的持久化上下文代理,包含3个组成部分:
1.EntityManager Proxy本身
2.事务的切面
3.事务管理器
遇到过的问题:
参考:59,为何声明式事务没有生效?
69.Spring如何处理高并发?高并发下,如何保证性能?
1.单例模式 + ThreadLocal
单例模式大大节省了对象的创建和销毁,有利于性能提高,ThreadLocal用来保证线程安全性
Spring单例模式下,用ThreadLocal来切换不同线程直接的参数,用ThreadLocal是为了保证线程安全,实际上,ThreadLocal的key就是当前线程的Thread实例
单例模式下,Spring把每个线程可能存在线程安全问题的参数值放进了ThreadLocal,虽然是一个实例,但在不同线程下的数据是相互隔离的,
因为运行时创建和销毁的bean大大减少了,所以大多数场景下,这种方式对内存资源的消耗较少,并且并发越高,优势越明显
2.ThreadLocal
相比同步机制,ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。
因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,
在编写多线程代码时,可以把不安全的变量封装进ThreadLocal !!!
3.Spring MVC在并发访问时,是否会存在线程安全问题?
参考:
Struts2是基于类的拦截(每次处理请求,都会实例化一个对象Action,不会有线程安全问题)
Spring MVC 是基于方法的拦截,粒度更细,而Spring的Controller默认是Singleton的,即:每个request请求,系统都会用同一个Controller去处理,
Spring MVC和Servlet都是方法级别的线程安全,如果单例的Controller或Servlet中存在实例变量,都是线程不安全的,而Struts2确实是线程安全的
优点:
不用每次创建Controller,减少了对象创建和销毁
缺点:
Controller是单例的,Controller里面的变量线程不安全
解决方案:
1.在Controller中使用ThreadLocal变量,把不安全的变量封装进ThreadLocal,使用ThreadLocal来保存类变量,将类变量保存在线程的变量域中,让不同的请求隔离开来
2.声明Controller为原型 scope="prototype",每个请求都创建新的Controller
3.Controller中不使用实例变量
Spring MVC 如何保证request对象线程安全?
参考:
InvocationHandler接口:这是springmvc保证request对象线程安全的核心。
通过实现该接口,开发者能够在Java对象方法执行时进行干预,搭配Threadlocal就能够实现线程安全
问题:判断一下程序是否线程安全?
@Controller
public class UserController{
@Autowired
private HttpSession session
@RequestMapping(xxxxxxx)
public void getUser{
session.get ...
session.set...
....
}
}
结论:
该程序是线程安全的
解析:
项目启动和运行时,Controller对象中的HttpSession并不是HttpSession实例,而是一个代理,
是org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler代理了HttpSession ,可通过这个代码求证:System.out.println(Proxy.getInvocationHandler(session));
只要当你真正调用HttpSession中的非java.lang.Object方法时才会真真去调用被代理的HttpSession里面的方法
说一下session.get ...过程:首先从对象工厂从Threadlocal中取得HttpSession实例,然后通过反射调用该实例的set方法
特别注意:
1.Spring 应该是在请求进来的时候ThreadLocal.set(Session),然后在请求的生命周期中都是一个Thread ,执行完后ThreadLocal.remove(Session)
2.一个请求使用一个ThreadLocal,绑定对应的HttpSession,所以是线程安全的
3.对于 "注入" 到Controller中的单例对象, 都是由Spring统一管理的,Spring对注入Controller的对象使用了ThreadLocal + 代理机制,保证了线程安全
4.但是,对于在Controller中直接定义的实例变量,是线程不安全的!!!
eg:
@RestController
@RequestMapping("/test1")
public class ControllerTest1 {
private int i = 0;
@GetMapping("/count")
public void test1(){
System.out.println(i++);
}
//验证Controller中的实例变量,线程不安全
@GetMapping("/t1")
public void test3(){
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i=0;i<1000;i++) {
service.execute(new Runnable() {
@Override
public void run() {
HttpUtils.sendGet("http://localhost:8080/test1/count", null);
}
});
}
}
}
调用:http://localhost:8080/test1/t1 方法,使用多线程对i进行操作,发现i的结果不是999,证明Controller中的实例变量是线程不安全的!
结论:
1.对于单例的Controller,Service中定义的实例变量,都不是线程安全的!!!
2.尽量避免在Controller和Service中定义多线程共享的实例变量
3.Spring使用ThreadLocal + InvocationHandler(动态代理)提供了高并发访问的性能
4.对于Controller和Service中的实例变量,多线程访问时,需要加锁处理 或 设置 scope = "prototype"为每个请求创一个对象
5.对于 @Autowire注入的HttpServletRequest和HttpSession,Spring进行了特殊处理,不会有线程安全问题
70.ConcurrentLinkedQueue与BlockingQueue
2类线程安全的队列:
1.阻塞队列 --- 阻塞算法 --- 队列使用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式实现
2.同步队列 --- 非阻塞算法 --- 使用循环CAS方式实现
LinkedBlockingQueue 线程安全的阻塞队列,实现了BlockingQueue接口,BlockingQueue继承自java.util.Queue接口,
并在接口基础上增加了take()和put()方法, 这2个方法正式队列操作的阻塞版本
先进先出,可以指定容量,默认最大是Integer.MAX_VALUE;其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来!!!
put() 向队列中放数据 take() 从队列中取数据
ConcurrentLinkedQueue 是Queue的一个安全实现.Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性。
非阻塞方式实现的无界线程安全队列 !!!
offer()添加元素, poll()获取元素 isEmpty()判断队列是否为空 (特别注意,不用size(),效率低,会遍历队列,尽量要避免用size而改用isEmpty())
采用CAS操作,允许多个线程并发执行,并不会因为你加锁而阻塞线程,使得并发性能更好!!!
使用:
ConcurrentLinkedQueue源码解析:http://ifeve.com/concurrentlinkedqueue/
总结:
多数生产消费模型的首选数据结构就是队列(先进先出)。Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,
其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列
Java中的7种阻塞队列
阻塞队列: --- 阻塞的是线程操作(拿和取元素)
常用于生产者和消费者场景,是生产者用来存放元素、消费者用来获取元素的容器
put()阻塞:队列满时,阻塞插入元素的线程,直到队列不满
take()阻塞:队列空时,阻塞获取元素的线程,直到队列不空
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。 生产者和消费者直接传递数据,不对数据作缓存,生产者和消费者通过在队列里排队的方式来阻塞和唤醒 --- 速度快
线程数少时,使用SynchronousQueue 速度更快!!!
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的区别和使用场景?
区别:
1.三者都是线程安全的
2.2个BlockingQueue是阻塞的,ConcurrentLinkedQueue是并发的
3.2个BlockingQueue使用锁机制实现阻塞和线程安全(通过ReentrantLock + Condition阻塞容量为空时的取操作和容量满时的写操作),
ConcurrentLinkedQueue使用cas算法保证线程安全
4.ArrayBlockingQueue使用一个锁(lock + 2个Condition),而LinkedBlockingQueue使用2个锁(锁分离,取用takeLock + Condition,写用putLock+Condition),所以LinkedBlockingQueue的吞吐量大,并发性能比Array高
LinkedBlockingQueue,对头和尾采用不同的锁,提高了吞吐量,适合 "消费者生产者" 模式
ArrayBlockingQueue, 数组实现,使用一把全局锁并行对queue的读写操作,同时使用2个Condition阻塞容量为空时的读操作和容量满时的写操作
5.正因为LinkedBlockingQueue使用两个独立的锁控制数据同步,所以可以使存取两种操作并行执行,从而提高并发效率。
而ArrayBlockingQueue使用一把锁,造成在存取两种操作争抢一把锁,而使得性能相对低下。LinkedBlockingQueue可以不设置队列容量,默认为Integer.MAX_VALUE.其容易造成内存溢出,一般要设置其值
使用场景:
阻塞队列优点:
多线程操作不需要同步,
队列会自动平衡负载,即:生产和消费两边,处理快了会被阻塞,减少两边的处理速度差距,
自动平衡负载特性,造成它能被用于多生产者队列,队列满了就要阻塞等着,直到消费者使队列不满才能继续生产
ConcurrentLinkedQueue:
允许多线程共享访问一个集合,多用于消息队列!!!
多消费者消费同一个 用 ConcurrentLinkedQueue:
BlockingQueueue:
多线程共享时阻塞,多用于任务队列!!!
单消费者用 BlockingQueueue:
总结: 单个消费者用LinkedBlockignQueue, 多消费者用ConcurrentLinkedQueue !!!
单生产者,单消费者 用 LinkedBlockingqueue
多生产者,单消费者 用 LinkedBlockingqueue
单生产者 ,多消费者 用 ConcurrentLinkedQueue
多生产者 ,多消费者 用 ConcurrentLinkedQueue
71.Java多线程同步机制:3种类型
volatile 变量:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性保证,不提供原子性。 --- 只保证可见性,不保证原子性,不绝对线程安全!!!
CAS 原子指令:轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性和原子化更新保证。
内部锁(synchronized)和显式锁(各种Lock):重量级多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性和原子性。
参考:
非阻塞算法在并发容器中的实现:ConcurrentLinkedQueue https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html
72.CAS在JDK中的实现
参考:
1.Synchronized锁机制存在的问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险
优化:
偏向锁、轻量级锁、减小锁粒度
2.锁分类
悲观锁 --- 独占锁 --- synchronized是独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁
乐观锁 --- 每次不加锁,而是假设没有冲突,而去完成某项操作,如果因为冲突失败就重试,直到成功为止
3.CAS原理 --- 实现乐观锁
CAS操作:
CAS有3个操作数:
V 内存值
A 旧的预期值
B 要修改的新值
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!!!
非阻塞算法: 一个线程的失败或挂起,不应该影响其他线程的失败或挂起的算法
CAS的硬件基础和实现原理:
现代的CPU提供了特殊指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,
而compareAndSet()就用这些代替了锁定, compareAndSet利用JNI,借助调用的C语言来完成CPU指令的操作
eg:
AtomicInteger 如何实现无锁下的线程安全?
//在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。
private volatile int value;
public final int get(){
return value;
}
//i++操作, 每次从内存中读取数据,然后将此数据和 +1 后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止
public final int incrementAndGet(){
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
39.Redis性能优化
40.MongoDB性能优化
41.MQ性能优化和对比
42.一次搞定Java多线程并发编程
参考:
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
线程安全:
1.Synchronized 线程同步
2.Lock + ReentrantLock 线程安全
线程通信与协作:
0.wait()、notify()、notifyAll() 每个对象都有的3个方法,通道Monitor、waitSet、enterSet用来监听锁,存放线程队列
1.ReentrantLock + Condition 并发控制多路复用
每个ReentrantLock可以创建多个Condition,每个Condition都可以通过控制一个对象锁,来实现多个线程通信
Condition方法:
线程1中调用await()后,线程1将释放锁,等待被唤醒
线程2获取到锁后,执行完毕,调用signal()方法,唤醒线程1
eg:
打印1到9这9个数字,A线程打印1,2,3,然后B线程打印4,5,6,,然后再A线程打印 7,8,9
2.ReadWriteLock 读写锁
3.CountDownLatch
4.CyclicBarrier
5.Semaphore
效率提升:
6.线程池 + 阻塞队列
java schedule设置只运行一次 java schedule的用法和搭配
转载本文章为转载内容,我们尊重原作者对文章享有的著作权。如有内容错误或侵权问题,欢迎原作者联系我们进行内容更正或删除文章。
提问和评论都可以,用心的回复会被更多人看到
评论
发布评论
相关文章