文档章节

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

p
 pigpdong2
发布于 04/29 11:52
字数 4928
阅读 2842
收藏 355

微信公众号:内核小王子 关注可了解更多关于数据库,JVM内核相关的知识; 如果你有任何疑问也可以加我pigpdong[^1]

jvm 一行代码是怎么运行的

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

jvm如何加载一个类

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

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

第三步:初始化,如果赋值的静态变量是基础类型或者字符串并且是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下来,分析下哪些对象的实例个数很多,以及哪些对象占用空间最多,然后结合代码进行分析。

并发和锁

线程的状态机

线程池参数:核心线程数,最大线程数,线程工厂,线程空闲时间,任务队列,拒绝策略 先创建核心线程,之后放入任务队列,任务队列满了创建线程直到最大线程数,在超过最大线程数就会拒绝,线程空闲后超过核心线程数的会释放,核心线程也可以通过配置来释放,针对那些一天只跑一个任务的情况。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的文件都加载到内存。

设计模式

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

微信公众号:内核小王子 关注可了解更多关于数据库,JVM内核相关的知识; 如果你有任何疑问也可以加我pigpdong[^1]

历史文章:

JAVA和操作系统交互细节

通过MySQL存储原理来分析排序和锁

最全的微服务知识总结

网络内核之TCP是如何发送和接收消息的

© 著作权归作者所有

p
粉丝 66
博文 8
码字总数 42519
作品 0
杭州
私信 提问
加载中

评论(3)

S
Sunset_
我觉得很好,qqq
msscn
msscn
每个东西都提个点,如果细化还需要另行百度
Polly蜀黍
Polly蜀黍
虽然还没细看,但觉得不错
java思维导图90天训练营第一期,向架构师前进一步

作者寄语 嗨,大家好,我是java思维导图的小编吕一明。这次训练营为期90天,主要针对有些java基础,但是项目经验比较缺乏的程序员。内容涵盖了主流的spring,redis,rabbitmq,MongoDB等技术...

java思维导图
2018/04/26
0
0
工作5年的Java程序员,才学会阅读源码,可悲吗?

最近一位5年开发经验的群友与我聊天 他说:最近慢慢的尝试去看spring的源码,学习spring,以前都只是会用就行了,但是越是到后面,发现只懂怎么用还不够,在面试的时候经常被问到一些开源框架...

Java架构
02/14
0
0
34张史上最全IT架构师技术知识图谱【只收藏不看系列】

本文是笔者多年来积累和收集的知识技能图谱,小编极力推荐分享给身边的技术人儿,希望这份技术知识图谱能够帮助到每一位奋斗在技术路上的小伙伴。 下面是笔者多年来积累和收集的知识技能图谱...

我最喜欢三大框架
03/04
0
0
限时下载 | 132G编程资料:Python、JAVA、C,C++、机器人编程、PLC,入门到精通~

当程序员处瓶颈期应如何提高自己?有很多关于“学习编程”的资源,能够让人从 0 到新手(虽然这些资源中大多数的质量是值得商榷的),但是怎么样才能将中级水平提高到专家级? 良好的编程能力...

feimawangfmi
2018/05/28
0
0
一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

1.前言 本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构。 项目地址:github.com/biaochenxuy… 文章的图文结合版 Vue-family.md Vue-family.pdf 2. Vue ...

全栈修炼
05/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Kubernetes云供应商架构的未来

首先,我想分享SIG的使命,因为我们用它来指导我们现在和将来的工作。从我们的章程中直接来看,SIG的使命是简化,开发和维护云供应商集成,作为Kubernetes集群的扩展或附加组件。这背后的动机...

Linux就该这么学
15分钟前
0
0
线程池没你想的那么简单

前言 原以为线程池还挺简单的(平时常用,也分析过原理),这次是想自己动手写一个线程池来更加深入的了解它;但在动手写的过程中落地到细节时发现并没想的那么容易。结合源码对比后确实不得...

crossoverJie
23分钟前
13
0
Scientific Linux开发停止 相关设备将迁移至CentOS上

在经历了将近14年的版本更迭之后,这个专注于科学领域的GNU/Linux发行版本不会发布下个重大版本更新--Scientific Linux 8了。 目前维护该发行版本的成员最终决定是时候休息了,今后将不再发布...

linuxCool
28分钟前
0
0
Redux

Redux概念 Redux = Reducer + Flux,数据层框架,将所有数据都存储到store中 Redux的工作流程 Antd的使用 安装npm install antd --save import 'antd/dist/antd.css'import { Input, Butto......

星闪海洋
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部