最全的JAVA知识汇总(附讲解和思惟导图)

jvm 一行代码是怎么运行的
首先,java代码会被编译成字节码,字节码就是java虚拟机定义的一种编码格式,须要java虚拟机才可以解析,java虚拟机须要将字节码转换成机器码才能在cpu上执行。 咱们能够用硬件实现虚拟机,这样虽然能够提升效率可是就没有了一次编译处处运行的特性了,因此通常在各个平台上用软件来实现,目前的虚拟机还提供了一套运行环境来进行垃圾回收,数组越界检查,权限校验等。虚拟机通常将一行字节码解释成机器码而后执行,称为解释执行,也能够将一个方法内的全部字节码解释成机器码以后在执行,前者执行效率低,后者会致使启动时间慢,通常根据二八法则,将百分之20的热点代码进行即时编译。JIT编译的机器码存放在一个叫codecache的地方,这块内存属于堆外内存,若是这块内存不够了,那么JIT编译器将再也不进行即时编译,可能致使程序运行变慢。java

jvm如何加载一个类
第一步:加载,双亲委派:启动类加载器(jre/lib),系统扩展类加载器(ext/lib),应用类加载器(classpath),前者为c++编写,因此系统加载器的parent为空,后面两个类加载器都是经过启动类加载器加载完成后才能使用。加载的过程就是查找字节流,能够经过网络,也能够本身在代码生成,也能够来源一个jar包。另外,同一个类,被不一样的类加载器加载,那么他们将不是同一个类,java中经过类加载器和类的名称来界定惟一,因此咱们能够在一个应用成存在多个同名的类的不一样实现。linux

第二步:连接:(验证,准备,解析) 验证主要是校验字节码是否符合约束条件,通常在字节码注入的时候关注的比较多。准备:给静态字段分配内存,可是不会初始化,解析主要是为了将符号引用转换为实际引用,可能会触发方法中引用的类的加载。c++

第三步:初始化,若是赋值的静态变量是基础类型或者字符串而且是final的话,该字段将被标记为常量池字段,另外静态变量的赋值和静态代码块,将被放在一个叫cinit的方法内被执行,为了保证cinit方法只会被执行一次,这个方法会加锁,咱们通常实现单例模式的时候为保证线程安全,会利用类的初始化上的锁。 初始化只有在特定条件下才会被触发,例如new 一个对象,反射被调用,静态方法被调用等。算法

java对象的内存布局
java中每个非基本类型的对象,都会有一个对象头,对象头中有64位做为标记字段,存储对象的哈希码,gc信息,锁信息,另外64位存储class对象的引用指针,若是开启指针压缩的话,该指针只须要占用32位字节。设计模式

Java对象中的字段,会进行重排序,主要为了保证内存对齐,使其占用的空间正好是8的倍数,不足8的倍数会进行填充,因此想知道一个属性相对对象其始地址的偏移量须要经过unsafe里的fieldOffset方法,内存对齐也为了不让一个属性存放在两个缓存行中,disruptor中为了保证一个缓存行只能被一个属性占用,也会用空对象进行填充,由于若是和其余对象公用一个缓存行,其余对象的失效会将整个缓存行失效,影响性能开销,jdk8中引入了contended注解来让一个属性独占一个缓存行,内部也是进行填充,用空间换取时间,如何计算一个对象占用多少内存,若是不精确的话就进行遍历而后加上对象头,这种状况没办法考虑重排序和填充,若是精确的话只能经过javaagent的instrument工具。数组

反射的原理
反射真的慢么?缓存

首先class.forname和class.getmethod 第一个是一个native方法,第二个会遍历本身和父类中的方法,并返回方法的一个拷贝,因此这两个方法性能都很差,建议在应用层进行缓存。 而反射的具体调用有两种方式,一种是调用本地native方法,一种是经过动态字节码生成一个类来调用,默认采用第一种,当被调用15次以后,采用第二种动态字节码方式,由于生成字节码也耗时,若是只调用几回不必,而第一种方式因为须要在java和c++之间切换,native 方法自己性能消耗严重,因此对于热点代码频繁调用反射的话,性能并不会不好。安全

属性的反射,采用unsafe类中setvalue来实现,须要传入该属性相对于对象其始地址的偏移量,也就是直接操做内存。其实就是根据这个属性在内存中的起始地址和类型来读取一个字段的值,在LockSupport类中,park和unpark方法,设置谁将线程挂起的时候也有用到这种方式。网络

动态代理
java自己的动态代理也是经过字节码实现的多线程

Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

工具类中须要提供 类加载器,须要实现的接口,拦截器的实现,也就是须要在InvocationHandler中调用原方法并作加强处理。而且这个实现,必定会被放到新生成的动态代理类里。

生成动态代理类的步骤:先经过声明的接口生成一个byte数组,这个数组就是字节流,经过传入的类加载进行加载生成一个class对象,这个class 里面有个构造方法接收一个参数,这个参数就是InvocationHandler,经过这个构造方法的反射获取一个实例类,在这个class里面,接口的实现中会调用InvocationHandler,而这个class对象为了防止生成太多又没有被回收,因此是一个弱引用对象。

jvm的内存模型
并发问题的根源:可见性,原子性,乱序执行

java内存模型定义了一些规则来禁止cpu缓存和编译器优化,happen-before用来描述两个操做的内存的可见性,有如下6条

1.程序的顺序执行,前一个语句对后一个语句可见 (当两个语句没有依赖的状况下仍是能够乱序执行)
2.volatile变量的写对另外一个线程的读可见
3.happen-before 具备传递性
4.一个线程对锁的释放对另一个线程的获取锁可见 (也就是一个线程在释放锁以前对共享变量的操做,另一个线程获取锁后会看的到)
5.线程a调用了线程b的start()方法,那么线程a在调用start方法以前的操做,对线程b内的run()方法可见
6.线程a调用了线程b的join方法,那么线程b里的全部操做,将对线程a调用join以后的操做可见。
jvm的垃圾回收
两种实现:引用计数和可达性分析,引用计数会出现循环引用的问题,目前通常采用可达性分析。

为了保证程序运行线程和垃圾回收线程不会发生并发影响,jvm采用安全点机制来实现stop the world,也就是当垃圾收集线程发起stop the world请求后,工做线程开始进行安全点检测,只有当全部线程都进入安全点以后,垃圾收集线程才开始工做,在垃圾收集线程工做过程当中,工做线程每执行一行代码都会进行安全点检测,若是这行代码安全就继续执行,若是这行代码不安全就将该线程挂起,这样能够保证垃圾收集线程运行过程当中,工做线程也能够继续执行。

安全点:例如阻塞线程确定是安全点,运行的jni线程若是不访问java对象也是安全的,若是线程正在编译生成机器码那他也是安全的,Java虚拟机在有垃圾回收线程执行期间,每执行一个字节码都会进行安全检测。

基础垃圾收集算法:清除算法会形成垃圾碎片,清除后整理压缩浪费cpu耗时,复制算法浪费内存。

基础假设:大部分的java对象只存活了一小段时间,只有少部分java对象存活好久。新建的对象放到新生代,当通过屡次垃圾回收还存在的,就把它移动到老年代。针对不一样的区域采用不一样的算法。由于新生代的对象存活周期很短,常常须要垃圾回收,因此须要采用速度最快的算法,也就是复制,因此新生代会分红两块。一块eden区,两块大小相同的survivor区。

新的对象默认在eden区进行分配,因为堆空间是共享的,因此分配内存须要加锁同步,否则会出现两个对象指向同一块内存,为了不频繁的加锁,一个线程能够申请一块连续内存,后续内存的分配就在这里进行,这个方案称为tlab。tlab里面维护两个指针,一个是当前空余内存起始位置,另一个tail指向尾巴申请的内存结束位置,分配内存的时候只须要进行指针加法并判断是否大于tail,若是超过则须要从新申请tlab。

若是eden区满了则会进行一次minorGc ,将eden区的存活对象和from区的对象移动到to区,而后交换from和to的指针。

垃圾收集器的分类:针对的区域,老年代仍是新生代,串行仍是并行,采用的算法分类复制仍是标记整理

g1 基于可控的停顿时间,增长吞吐量,取代cms g1将内存分为多个块,每一个块均可能是 eden survivor old 三种之一 首先清除全是垃圾的快 这样能够快速释放内存。

若是发现JVM常常进行full gc 怎么排查?

不停的进行full gc表示可能老年代对象占有大小超过阈值,而且通过屡次full gc仍是没有降到阈值如下,因此猜想可能老年代里有大量的数据存活了好久,多是出现了内存泄露,也多是缓存了大量的数据一直没有释放,咱们能够用jmap将gc日志dump下来,分析下哪些对象的实例个数不少,以及哪些对象占用空间最多,而后结合代码进行分析。

并发和锁
线程的状态机
最全的JAVA知识汇总(附讲解和思惟导图)
线程池参数:核心线程数,最大线程数,线程工厂,线程空闲时间,任务队列,拒绝策略 先建立核心线程,以后放入任务队列,任务队列满了建立线程直到最大线程数,在超过最大线程数就会拒绝,线程空闲后超过核心线程数的会释放,核心线程也能够经过配置来释放,针对那些一天只跑一个任务的状况。newCachedThreadPool线程池会致使建立大量的线程,由于用了同步队列。

synchronized

同步块会有一个monitorenter和多个monitorexist ,重量级锁是经过linux内核pthread里的互斥锁实现的,包含一个waitset和一个阻塞队列。 自旋锁,会不停尝试获取锁,他会致使其余阻塞的线程没办法获取到锁,因此他是不公平锁,而轻量级锁和偏向锁,均是在当前对象的对象头里作标记,用cas方法设置该标记,主要用于多线程在不一样时间点获取锁,以及单线程获取锁的状况,从而避免重量级锁的开销,锁的升级和降级也须要在安全点进行。

reentrantlock相对synchronized的优点:能够控制公平仍是非公平,带超时,响应中断。
CyclicBarrier 多个线程相互等待,只有全部线程所有完成后才通知一块儿继续 (调用await 直到全部线程都调用await才一块儿恢复继续执行)
countdownlatch 一个线程等待,其余线程执行完后它才能继续。(调用await后被阻塞,直到其余地方调用countdown()将state减到1 这个地方的其余能够是其余多个线程也能够其余单个任务)
semaphore 同一个时刻只运行n个线程,限制同时工做的线程数目。
阻塞队列通常用两个锁,以及对应的条件锁来实现,默认为INTEGER.MAX为容量,而同步队列没有容量,优先级队列内部用红黑树来实现。

若是要频繁读取和插入建议用concurrenthashmap 若是频繁修改建议用 concurrentskiplistmap,copyonwrite适合读多写少,写的时候进行拷贝,并加锁。读不加锁,可能读取到正在修改的旧值。concurrent系列实际上都是弱一致性,而其余的都是fail-fast,抛出ConcurrentModificationException,而弱一致性容许修改的时候还能够遍历。例如concurrent类的size方法可能不是百分百准确。

AQS 的设计,用一个state来表示状态,一个先进先出的队列,来维护正在等待的线程,提供了acquire和release来获取和释放锁,锁,条件,信号量,其余并发工具都是基于aqs实现。

字符串
字符串能够经过intern()方法缓存起来,放到永久代,通常一个字符串申明的时候会检查常量区是否存在,若是存在直接返回其地址,字符串是final的,他的hashcode算法采用31进制相加,字符串的拼接须要建立一个新的字符串,通常使用stringbuilder。String s1 = "abc"; String s2 = "abc"; String s1 = new String("abc"); s1和s2多是相等的,由于都指向常量池。

集合
vector 线程安全,arraylist 实现 randomaccess 经过数组实现支持随机访问,linkedlist 双向链表能够支持快速的插入和删除。
treeset 依赖于 treemap 采用红黑树实现,能够支持顺序访问,可是插入和删除复杂度为 log(n)
hashset 依赖于 hashmap 采用哈希算法实现,能够支持常数级别的访问,可是不能保证有序
linkedhashset 在hashset的节点上加了一个双向链表,支持按照访问和插入顺序进行访问
hashtable早版本实现,线程安全 不支持空键。
hashmap:根据key的hashcode的低位进行位运算,由于高位冲突几率较高,根据数组长度计算某个key对应数组位置,相似求余算法,在put的时候会进行初始化或者扩容,当元素个数超过 数组的长度乘以负载因子的时候进行扩容,当链表长度超过8会进行树化,数组的长度是2的多少次方,主要方便位运算,另外一个好处是扩容的时候迁移数据只须要迁移一半。当要放 15个元素的时候,通常数组初始化的长度为 15/0.75= 20 而后对应的2的多少次方,那么数组初始化长度为 32.
ConcurrentHashMap 内部维护了一个segment数组,这个segment继承自reentrantlock,他自己是一个hashmap,segment数组的长度也就是并发度,通常为16. hashentry内部的value字段为volatile来保证可见性.size()方法须要获取全部的segment的锁,而jdk8的size()方法用一个数组存储每一个segment对应的长度。
io
输入输出流的数据源有 文件流,字节数组流,对象流 ,管道。带缓存的输入流,须要执行flush,reader和writer是字符流,须要根据字节流封装。

bytebuffer里面有position,capcity,limit 能够经过flip重置换,通常先写入以后flip后在从头开始读。

文件拷贝 若是用一个输入流和一个输出流效率过低,能够用transfer方法,这种模式不用到用户空间,直接在内核进行拷贝。

一个线程一个链接针对阻塞模式来讲效率很高,可是吞吐量起不来,由于没办法开那么多线程,并且线程切换也有开销,通常用多路复用,基于事件驱动,一个线程去扫描监听的链接中是否有就绪的事件,有的话交给工做线程进行读写。通常用这种方式实现C10K问题。

堆外内存(direct) 通常适合io频繁而且长期占用的内存,通常建议重复使用,只能经过Native Memory Tracking(NMT)来诊断,MappedByteBuffer能够经过FileChannel.map来建立,能够在读文件的时候少一次内核的拷贝,直接将磁盘的地址映射到用户空间,使用户感受像操做本地内存同样,只有当发生缺页异常的时候才会触发去磁盘加载,一次只会加载要读取的数据页,例如rocketmq里一次映射1g的文件,并经过在每一个数据页写1b的数据进行预热,将整个1G的文件都加载到内存。

设计模式
建立对象:工厂 构建 单例
结构型: 门面 装饰 适配器 代理
行为型:责任链 观察者 模版
封装(隐藏内部实现) 继承(代码复用) 多态(方法的重写和重载)
设计原则:单一指责,开关原则,里氏替换,接口分离,依赖反转

最全的JAVA知识汇总(附讲解和思惟导图)

相关文章
相关标签/搜索