- Java语言的特性
Java的三大特性:封装、继承、多态
封装:隐藏对象的属性和实现细节,仅对外提供公共的访问方式。好处:提高重用性和安全性。
继承:从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,平能扩展新的能力。好处:提高代码的重用性。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,就是多态,简单点说:就是用父类的引用指向子类的对象。
表现形式:方法重写,方法重载,接口和接口的继承,类和类的继承
方法重载:同一个类中,有多个方法名相同,但参数列表不同
方法重写:子类重写父类的方法
好处:当父类中的方法,无法满足子类的需求时,子类可以对父类的方法进行扩展。
- Java的基本数据类型
类型 | 字节大小 | 长度 |
---|---|---|
byte | 1 | -128-127 |
char | 2 | 0-65535 |
short | 2 | -215-215-1 |
int | 4 | -231-231-1 |
long | 8 | -263-263-1 |
float | 4 | -2128-2128 |
double | 8 | -21024-21024 |
boolean | 1/8 | true、false |
- String和StringBuffer、StringBuilder的区别
- String:用final修饰的char[]数组,不可变;
- StringBuffer:synchronized修饰方法,线程安全,通过System.copyArray()方法动态修改;
- StringBuild:线程不安全;
- Java类的加载顺序
- 首先加载父类的静态字段或者静态语句块;
- 子类的静态字段或者静态语句块
- 父类的普通变量以及语句块
- 父类的构造方法被加载
- 子类的变量或者语句块被加载
- 子类的构造方法被加载
- Java类加载的过程
加载-验证-准备-解析-初始化-使用-卸载
验证-准备-解析 也被称为连接阶段
-
加载:把代码数据加载到内存中,即JVM将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在JVM的方法区创建一个对应的class对象,这个class对象就是这个类各种数据的访问入口。
- 验证:虚拟机对代码数据进行效验,查看是否按照JVM规范编写。
JVM规范效验:JVM会对字节流进行文件格式效验,判断其是否符合;
JVM规范:是否能被当前版本的虚拟机处理;
代码逻辑效验:JVM会对代码组成的数据流和控制流进行效验,确保JVM运行该字节码文件后不会出现致命错误; -
准备:JVM开始为类变量分配内存并初始化。
内存分配的对象:Java中的变量有【类变量】和【类成员变量】两者类型,类变量指的是被static修饰的变量,而其他所有类型的变量都属于类成员变量。
在准备阶段,JVM只会为类变量分配内存,而不会为类成员变量分配内存。
类成员变量的内存分配需要等到初始化阶段才开始。
初始化的类型:在准备阶段,JVM会为类成员变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予,Java语言中该数据类型的零值,而不是用户代码里初始化的值。 -
解析:JVM针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类引用进行解析。
这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中直接引用。 - 初始化:JVM会根据语句执行顺序对类对象进行初始化。
- 使用:当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的class对象,最后负责运行的JVM也退出内存。
- Java类加载器
- Bootstrap ClassLoader:最顶层的加载类,主要加载核心类库,也就是环境变量下面%JRE_HOME/lib下的rt.jar、resource.jar、charsets.jar和class等。还可以通过启动jvm时指定位置;
-
Extention ClassLoader:扩展的类加载器,加载目录%JRE_HOME/lib/ext目录下的jar包和class文件。
还可以加载-Djava.ext.dirs选项指定的目录; - Appclass Loader:也称为SystemAppClass,加载当前应用的classpath的所有类;
- Custom ClassLoader:自定义加载类;
- Java双亲委派机制
定义:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
作用:
- 防止重复加载同一个.class。通过委托去向上问一问,加载过了,就不用再加载一遍了,保证数据安全。
- 保证核心.class不能被篡改。透过委托方式,不会去篡改核心.class,及时篡改了也不会去加载,即使加载了也不会是同一个.class对象了。不同的类加载器加载同一个.class也不是同一个Class对象,这样保证了Class执行安全。
- Java反射机制(//www.greatytc.com/p/9be58ee20dee)
定义:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java的反射机制。
反射机制的相关类:
- CLass类:代表类的实体,在运行的Java应用程序中表示类和接口
- Field类:代表类的成员变量(成员变量也称为类的属性)
- Method类:代表类的方法
- Constructor:代表类的构造方法
- Java的内存模型(//www.greatytc.com/p/0ecf020614cb)
包括程序计数器、 虚拟机栈、本地方法栈、方法区、堆
- 程序计数器:主要功能是记录当前线程执行程序的位置,通过改变计算数值来确定执行下一条指令。每个线程的创建,都会创建一个程序计数器,并且对于每个线程而言都是相互独立的。
- 虚拟机栈:主要功能是临时存储线程执行到的每个方法需要的参数,其内存空间在编译时就已确定。每创建一个线程,则创建一个虚拟机栈。线程每执行到一个方法,对应的栈里就会创建一个栈帧,栈帧会存储局部变量表、动态链接、操作数栈和方法出口等信息,执行方法,栈帧入栈,方法执行完,栈帧出栈。
- 本地方法栈:和Java虚拟机栈一样,只是记录native方法执行。
- 堆:堆内存是存放所有对象实例,也是jvm的GC主要对象。
- 方法区:主要存储虚拟机栈加载类信息、常量、静态变量。
- Java的特性
- 原子性:指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行;(处理器会自动保证基本的内存操作是原子性的;使用线程锁保证原子性;使用缓存锁保证原子性 eg:CAS)。
- 可见性:指当一个线程修改了线程共享变量的值,其他线程能够立即得知这个修改(比如使用volatile)。
-
有序性:即程序执行的顺序按照代码的先后顺序执行;
(Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。不过可以通过synchronized禁止指令重排)
- JVM垃圾回收机制//www.greatytc.com/p/23f8249886c6
一、引用计数法:给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1,用对象计数器是否为0来判断对象是否可被回收。
缺点:无法解决循环引用的问题。
二、可达性分析算法:通过GC ROOT的对象作为搜索起始点,通过引用向下搜索,所走过的路径称为引用链。通过对象是否有到达引用链的路径来判断对象是否可被回收。
可作为GC ROOT对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象,方法区中常量引用的对象、本地方法栈中JNI引用的对象。
- 垃圾回收算法
一、标记-清除算法:最基础的一种垃圾回收算法,它分为两部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾领出来清理掉。
缺点:会造成内存碎片,需要开辟大内存空间时,因为是不连续的,导致用不了而浪费。
二、复制算法:在标记清楚算法基础上演化而来,解决标记清除算法的内存碎片问题。将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。保证了内存连续可用,内存分配时也就不用考虑内存碎片等复杂情况。
缺点:只能使用一半内存
三、标记-整理算法:标记-整理算法标记过程仍然与标记-清楚算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,而清理掉端边界以外的内存区域。
标记-整理算法解决了内存碎片问题,也规避了复制算法只能利用一半内存区域的弊端。标记-整理算法对内存变动更频繁,需要整理所有的存活对象的引用地址,在效率上比复制算法要差很多。
四、分代收集算法:分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳,根据对象存活周期的不同将内存划分为几块。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法-或者标记-整理算法来进行回收。
- Java多线程
继承Thread类、实现Runable接口、线程池创建多线程、实现Callable接口
- Java线程池
- corePoolSize:核心线程池大小
- maximumPoolSize:线程池最大大小
- keepAliveTime:线程存活保持时间
- timeUnit:时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- handler:线程饱和策略
- Java 常用线程池
-
newFixedThreadPool
固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。 -
newCachedThreadPool
带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。 -
newSingleThreadExecutor
单线程线程池,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。 -
newScheduledThreadPool
调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。
- Java线程同步和异步
- 同步:单线程执行,所有操作都是同步的。
- 异步:并发都是异步的。
- Java线程之Join、Yield和Sheep,Wait和notify
- Yield:暂停当前线程执行,让其他线程参与竞争,系统选择其他相同的或者更高优先级的线程;
- Join:是当前的线程暂停执行,等待调用该方法的线程结束后再继续执行本线程;
- Sleep:属于Thread类,暂停线程执行,线程进入阻塞状态,让出CPU,不会释放对象锁,到指定时间会自动醒过来;
- Wait:必须在synchronized内部执行,属于Object类,暂停线程执行,线程进入休眠状态,释放对象锁,加入等待对象池,只有调用notify()和notifyAll()方法才可以执行;
- Java之死锁
死锁:死锁是操作系统层面的一个错我,是进程死锁的简称。
必要条件:
- 互斥条件:即当资源被一个线程使用,别的线程不能使用。
- 不剥夺条件:资源请求者不能强制从占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待:即存在一个等待队列,P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,这样既形成了一个等待环路。
- Java之volatile关键字
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了每个变量的值,新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- Java同步锁-synchronized关键字和Lock接口
synchronized:可以修饰静态方法、成员函数,还可以直接定义代码块
(https://cloud.tencent.com/developer/article/1465413)
- 对成员函数加锁,必须获得该类的实例对象的锁才能进入同步块;
- 对静态方法加锁,必须获得该类的锁才能进入同步块;
- 对同步块加入class,执行前必须获得该class类的锁;
- 对同步块加入Instance,执行前必须先获得实例对象的锁;
- synchronized锁的实现:
有两种方式:它们的底层实现一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。
同步方式识别不同。
- 对方法上锁:通过flags标志
- 构造同步代码块:通过monitorenter和monitorexit指令操作
- synchronized锁的底层实现原理:
在JVM中,对象分成三部分存在:对象头、实例数据、对其填充。
对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
对象头主要结构由Mark Word 和 CLass Metadata Address,其中Mark Word存储着锁信息。
JDK6之后,synchronized优化之后有四个状态:无状态锁、偏向锁、轻量级锁、重量级锁,锁的状态在对象头Mark Word 中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark down数据。
- synchronized锁膨胀
偏向锁:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时即使偏向的。
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark down的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark down的锁标记位为偏向锁以及当前线程ID等于Mark down的ThreadID即可。
轻量级锁:轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后的交替
执行同步块。
重量级锁:重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
- 锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,
去除不可能存在竞争的锁。
- 锁优化:锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免重复加锁和释放锁。
- 自旋锁:通过让线程执行循环等待锁的释放,不让出CPU。如果等到锁,就顺利进入临界区。如果还不能获得锁,那么久挂起。但是如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
- 自适应自旋锁:相当于对自旋锁的进一步优化。它的自旋次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定,这就解决了自旋锁带来的缺点。
- Java之Lock接口
- 定义:是一个接口,用来手动的获取和释放锁,并且发生异常时不会主动的释放锁,所有必须在try...catch块中,在finally中释放锁,保证锁一定会被释放,不会发生死锁。
-
原理:(https://blog.csdn.net/future234/article/details/80576623)
AQS(队列同步器,双向链表) + int值 + CAS自旋
- synchronized和lock的区别
- 首先synchronized是java内置关键字,Lock是Java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁,Lock需在finally中手工释放锁,否则容易造成线程死锁;
- synchronized的锁可重入、不可中断、非公平。Lock锁可重入、可判断、公平和非公平都可实现;
- Lock建议使用在低锁冲突的情况下;
- Java中的引用类型
1、强引用代码中普遍存在的类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2、软引用描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。
3、弱引用描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。
4、虚引用这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用。
- Java集合
- Array:数组是相同类型数据的有序集合,在堆中被分配连续空间,通过下标寻找,查找速度快,插入慢。
- List:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。每个链表包含多个节点,节点包含两个部分,一个是数据域,用来存储数据,一个是引用域(相当于指针,双向链表有两个指针,分别指向上一个和下一个节点),指向下个节点。查找慢,插入、删除快。
- Map:是一种键-值对集合,Map集合中的每一个元素都包含一个键对象和一个值对象
- ArrayList:
- 内部采用数组实现,默认大小10;
- 扩容newCapacity = oldCapacity + (oldCapacity >> 1);
- 扩容时 调用native System.arrayCopy 方法拷贝;
- 可以存在多个null;
- LinkedList:
- 内部采用双向链表存储;
- 可以存在多个null;
- Vector:
- 内部数组实现;
- 默认大小为10,capacityIncrement设置自增长大小 没有则自增张一倍;
- 内部方法被synchronized修饰,线程安全;
- HashMap:(//www.greatytc.com/p/ee0de4c99f87)
- 内部由数组+链表+红黑树实现;
- 默认大小16,默认负载系数0.75,阈值
threshold = loadFactor(负载系数) * capacity(容量)
;- resize()扩容直接扩大一倍
newThr = oldThr << 1; // double threshold
;- 当链表长度大于8且容量大于64,转红黑数 当链表长度小于6,还原成数组;
- 扩容因子默认0.75,是根据泊松分布(二项分布)测得;
- key和value可以为null,key至多只有一个;
- 树:树是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。
(//www.greatytc.com/p/683ccf7b4712)
特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
- 二叉树:每个节点最多含有两个子节点的树称为二叉树
- 满二叉树:一颗有n层的二叉树,除第n层外,每层都有两个子节点
- 完全二叉树:假设二叉树有h层,除第好层外,其他各层的节点数均已达到最大个数(1至h-1层为满二叉树),第h层所有的节点都集中在最左边,这棵树就是满二叉树。
- 二叉查找树:又称为二叉搜索树、有序二叉树、排序二叉树
特点:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的跟节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于他的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的点;
- 平衡二叉树(AVL树)
因为二叉树某些情况会退化成链条表,时间复杂度O(n),效率低下,这时需要平衡二叉树。它是严格的平衡二叉树,平衡条件必须满足,所有只有通过旋转保持平衡,而旋转是非常耗时的,适用于插入和删除次数比较少,但查找多的情况。
特点:
- 左树所有节点比根节点小,右树所有节点比根节点大
- 左树和右树的高度最大差值为1
- 红黑树
通过对任何一条从跟到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,它是一种弱平衡二叉树,相比AVL树来说,它的旋转次数少,对于搜索、插入、删除操作较多的情况下,优先用红黑树。
特点:
- 每个节点是黑色或红色;
- Root节点一定是黑色;
- Null节点一定是黑色;
- 不能出现连续的红色节点;
- 从任意一个节点达到子节点的黑色数目相同;
- Java值传递和引用传递(https://mp.weixin.qq.com/s/Qp6Cc0mlRLnrToNy5-3zeg)
(可以说是Java不同数据类型储存的方式不同导致有值传递和引用传递这种说法)
形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法func执行完成后,a就会被销毁释放空间,也就是不存在了
实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并且在方法被调用时传入。
值传递:在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
引用传递:”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。
基本数据类型:我们声明并初始化基本数据类型的局部变量时,变量名以及字面量值都是存储在栈中,而且是真实的内容。在值传递过程中,我们知道栈帧是私有的、独立的,传递过去的值是拷贝过去的副本,然后继续修改变量的值,不会影响原值。
引用数据类型:”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。引用传递,也是拷贝副本,但是它们的引用地址指向同一个,看起来像是同一个,实际上重新new一个对象会发现,原来的对象没有改变。
在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:
如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。
如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。