java基础
面向对象三大特性
特性:封装、继承、多态;
封装:对抽象的事物抽象化成一个对象,并对其对象的属性私有化,同时提供一些能被外界访问属性的方法;
继承:子类扩展新的数据域或功能,并复用父类的属性与功能,单继承,多实现;
多态:通过继承(多个⼦类对同⼀⽅法的重写)、也可以通过接⼝(实现接⼝并覆盖接⼝);
多态实现原理
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。
静态绑定与动态绑定:
一种是在编译期确定,被称为静态分派,比如方法的重载;
一种是在运行时确定,被称为动态分派,比如方法的覆盖(重写)和接口的实现。
多态的实现
虚拟机栈中会存放当前方法调用的栈帧(局部变量表、操作栈、动态连接、返回地址)。多态的实现过程,就是方法调用动态分派的过程,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。
基本数据类型和包装类
基本数据类型 | 存储大小 | 取值范围 | 默认值 |
---|---|---|---|
byte | 8 位有符号数 | -27 到 27 - 1 | 0 |
short | 16 位有符号数 | -215 到 215 - 1 | 0 |
int | 32 位有符号数 | -231 到 231 - 1 | 0 |
long | 64 位有符号数 | -263 到 263 - 1 | 0L |
float | 32 位,符合 IEEE 754 标准 | 负数 -3.402823e+38 到 3.402823e+38 | 0.0f |
double | 64 位,符合 IEEE 754 标准 | 负数 -1.797693e+308 到 1.797693e+308 | 0.0d |
char | 16 位 | 0 到 216 - 1 | ‘\u0000’ |
boolean | 1 位 | true 和 false | false |
类型 | 缓存范围 |
---|---|
Byte, Short, Integer, Long | [-128, 127] |
Character | [0, 127] |
Boolean | [false, true] |
static和final关键字
**static:**可以修饰属性、方法
static修饰属性:
类级别属性,所有对象共享一份,随着类的加载而加载(只加载一次),先于对象的创建;可以使用类名直接调用。
static修饰方法:
随着类的加载而加载;可以使用类名直接调用;静态方法中,只能调用静态的成员,不可用this;
**final:**关键字主要⽤在三个地⽅:变量、⽅法、类。
final修饰变量:
- 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
- 如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。
final修饰方法:
把⽅法锁定,以防任何继承类修改它的含义(重写);类中所有的private⽅法都隐式地指定为final。
final修饰类:
final修饰类时,表明这个类不能被继承。final类中的所有成员⽅法都会被隐式地指定为final⽅法。一个类不能被继承,除了final关键字之外,还有可以私有化构造器。(内部类无效)
抽象类和接口
**抽象类:**包含抽象方法的类,即使用abstract修饰的类;抽象类只能被继承,所以不能使用final修饰,抽象类不能被实例化;
**接口:**接口是一个抽象类型,是抽象方法的集合,接口支持多继承,接口中定义的方法,默认是public abstract修饰的抽象方法;
相同点:
- 抽象类和接口都不能被实例化;
- 抽象类和接口都可以定义抽象方法,子类/实现类必须覆写这些抽象方法;
不同点:
- 抽象类有构造方法,接口没有构造方法;
- 抽象类可以包含普通方法,接口中只能是public abstract修饰抽象方法(Java8之后可以);
- 抽象类只能单继承,接口可以多继承;
- 抽象类可以定义各种类型的成员变量,接口中只能是public static final修饰的静态常量;
**抽象类的使用场景:**既想约束子类具有共同的行为(但不在乎其如何实现),又想拥有缺省的方法,又能拥有实例变量;
**接口的应用场景:**约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现;实现类中各个功能之间可能没有任何联系;
泛型以及泛型擦除
**泛型:**泛型的本质是参数化类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
**泛型擦除:**Java的泛型是伪泛型,使用泛型的时候加上类型参数,在编译器编译生成的字节码的时候会去掉,这个过程成为类型擦除。
如List等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。
可以通过反射添加其它类型元素。
反射原理以及使用场景
**Java反射:**是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且都能够调用它的任意一个方法;
**反射原理:**反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成相应的Method、Filed、Constructor等类。
如何得到Class的实例
- 类名.class(就是一份字节码)
- Class.forName(String className);根据一个类的全限定名来构建Class对象
- .每一个对象多有getClass()方法:obj.getClass();返回对象的真实类型
使用场景:
- 开发通用框架-反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过XML文件配置JavaBean、Filter等),为了保证框架的通用性,需要根据配置文件运行时动态加载不同的对象或类,调用不同的方法。
- 动态代理-在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。JDK:spring默认动态代理,需要实现接口;CGLIB:通过asm框架序列化字节流,可配置,性能差
- 自定义注解-注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。
Java异常体系
Throwable是Java语言中所有错误或异常的超类。下一层分为Error 和Exception。
**Error:**是指java运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。
Exception包含:RuntimeException、CheckedException;
编程错误可以分成三类:语法错误、逻辑错误和运行错误。
**RuntimeException:**运行时异常,程序应该从逻辑角度尽可能避免这类异常的发生。如NullPointerException、ClassCastException ;
**CheckedException:**受检异常,程序使用trycatch进行捕捉处理;
ArrayList和LinkedList
ArrayList
底层基于数组实现,支持对元素进行快速随机访问,适合随机查找和遍历,不适合插入和删除。(提一句实际上)
默认初始大小为10,当数组容量不够时,会触发扩容机制(扩大到当前的1.5倍),需要将原来数组的数据复制到新的数组中;当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。
LinkedList
底层基于双向链表实现,适合数据的动态插入和删除;
内部提供了List接口中没有定义的方法,用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。(比如jdk官方推荐使用基于linkedList的Deque进行堆栈操作)
ArrayList与LinkedList区别
都是线程不安全的,ArrayList适用于查找的场景,LinkedList适用于增加、删除多的场景。
实现线程安全:
可以使用原生的Vector,或者是Collections.synchronizedList(List list)函数返回一个线程安全的ArrayList集合。
建议使用concurrent并发包下的CopyOnWriteArrayList
- Vector:底层通过synchronize修饰保证线程安全,效率较差。
- CopyOnWriteArrayList:写时加锁,使用了一种叫写时复制的方法;读操作是可以不用加锁的。
HashMap
HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据。
**扩容情况:**默认的负载因子是0.75,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。
-
创建一个长度为原来数组长度两倍的新数组。
-
1.7采用Entry的重新hash运算,1.8采用高于运算。
哈希函数:
通过hash函数(优质因子31循环累加)先拿到key的hashcode,是一个32位的值,然后让hashcode的高16位和低16位进行异或操作。该函数也称为扰动函数,做到尽可能降低hash碰撞,通过尾插法进行插入。
容量为什么始终都是2^N
先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。方便数组的扩容和增删改时的取模。
JDK1.7与1.8的区别
JDK1.7 HashMap:
底层是 数组和链表 结合在⼀起使⽤也就是链表散列。如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。扩容翻转时顺序不一致使用头插法会产生死循环,导致cpu100%
JDK1.8 HashMap:
底层数据结构上采用了数组+链表+红黑树;当链表⻓度⼤于阈值(默认为 8-泊松分布),数组的⻓度大于 64时,链表将转化为红⿊树,以减少搜索时间。(解决了tomcat臭名昭著的url参数dos攻击问题)
ConcurrentHashMap
可以通过ConcurrentHashMap和Hashtable来实现线程安全;Hashtable 是原始API类,通过synchronize同步修饰,效率低下;ConcurrentHashMap通过分段锁实现,效率较比Hashtable要好。
序列化和反序列化
序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。
**序列化:**将java对象转化为字节序列的过程。
**反序列化:**将字节序列转化为java对象的过程。
优点:
- 实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里)Redis的RDB
- 利用序列化实现远程通信,即在网络上传送对象的字节序列。Google的protoBuf。
**反序列化失败的场景:**序列化ID:serialVersionUID不一致的时候,导致反序列化失败。
String
String使用数组存储内容,数组使用final修饰,因此String定义的字符串的值也是不可变的。
StringBuffer对方法加了同步锁,线程安全,效率略低于StringBuilder。
Object类方法
toString默认是个指针,一般需要重写;
equals比较对象是否相同,默认和==功能一致;
hashCode散列码,equals则hashCode相同,所以重写equals必须重写hashCode;
finalize用于垃圾回收之前做的遗嘱,默认空,子类需重写;
clone深拷贝,类需实现cloneable的接口;
getClass反射获取对象元数据,包括类名、方法;
notify、wait用于线程通知和唤醒;
JDK新特性
JDK8
支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能
JDK9
Stream API中iterate方法的新重载方法,可以指定什么时候结束迭代IntStream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);
默认G1垃圾回收器
JDK10
其重点在于通过完全GC并行来改善G1最坏情况的等待时间。
JDK11
ZGC (并发回收的策略) 4TB
用于 Lambda 参数的局部变量语法
JDK12
Shenandoah GC (GC 算法)停顿时间和堆的大小没有任何关系,并行关注停顿响应时间。
JDK13
增加ZGC以将未使用的堆内存返回给操作系统,16TB
JDK14
删除cms垃圾回收器、弃用ParallelScavenge+SerialOldGC垃圾回收算法组合。
将ZGC垃圾回收器应用到macOS和windows平台。
JVM
JVM运行时数据区域
堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器。
Heap(堆):
对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,也是垃圾回收(GC)的主要区域;开启逃逸分析后,某些未逃逸的对象可以通过标量替换的方式在栈中分配。
堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区。
堆内存分配策略:
方法区:
对于JVM的方法区也可以称之为永久区,它储存的是已经被java虚拟机加载的类信息、常量、静态变量;Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace);
当应用中的 Java 类过多时,比如 Spring 等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。
虚拟机栈:
虚拟机栈是线程私有的,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(局部变量表、操作数栈 、动态链接 、返回地址);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
- **局部变量表:**局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量。底层是变量槽(variable slot)
- **操作数栈:**是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。
- **动态链接:**因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。
- 返回地址(returnAddress):类型(指向了一条字节码指令的地址)JIT即时编译器(Just In Time Compiler),简称 JIT 编译器: 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,比如锁粗化等。
本地方法栈:
本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
PC程序计数器:
PC,指的是存放下一条指令的位置的一个指针。它是一块较小的内存空间,且是线程私有的。由于线程的切换,CPU在执行的过程中,需要记住原线程的下一条指令的位置,所以每一个线程都需要有自己的PC。
创建一个对象的步骤
步骤:类加载检查、分配内存、初始化零值、设置对象头、执行init方法
- **类加载检查:**虚拟机遇到 new 指令时,⾸先去检查是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
- **分配内存:**在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存,分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
- **初始化零值:**内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
- **设置对象头:**初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
- **执⾏ init ⽅法:**从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说(除循环依赖),执⾏ new 指令之后会接着执⾏ ⽅法,这样⼀个真正可⽤的对象才算产⽣出来。
过程:加载、验证、准备、解析、初始化
加载阶段:
1. 通过一个类的全限定名来获取定义此类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3. 在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。
验证阶段:
- 文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)
- 元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)
- 字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)
- 符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)
准备阶段:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值
解析阶段:
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
**字符串常量池:**堆上,默认class文件的静态常量池
**运行时常量池:**在方法区,属于元空间
初始化阶段:
初始化阶段时加载过程的最后一步,而这一阶段也是真正意义上开始执行类中定义的Java程序代码。
对象的引用(强软弱虚)
普通的对象引用关系就是强引用。
软引用用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
弱引用对象相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
虚引用一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。
双亲委派机制
每⼀个类都有⼀个对应它的类加载器。系统中的 ClassLoder 在协同⼯作的时候会默认使⽤ 双亲委派模型 。即在类加载的时候,系统会⾸先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,⾸先会把该请求委派该⽗类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当⽗类加载器⽆法处理时,才由⾃⼰来处理。当⽗类加载器为null时,会使⽤启动类加载器 BootstrapClassLoader 作为⽗类加载器。
使用好处:
此机制保证JDK核心类的优先加载;使得Java程序的稳定运⾏,可以避免类的重复加载,也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。
破坏双亲委派机制:
- 可以⾃⼰定义⼀个类加载器,重写loadClass方法;
- Tomcat 可以加载自己目录下的 class 文件,并不会传递给父类的加载器;
- Java 的 SPI,发起者 BootstrapClassLoader 已经是最上层了,它直接获取了 AppClassLoader 进行驱动加载,和双亲委派是相反的。
jvm垃圾回收
存活算法和两次标记过程
引用计数法:
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率也很高
缺点:他很难解决对象之间相互循环引用的问题,基本上被抛弃
可达性分析法:
通过一系列的成为“GC Roots”(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI引用)的对象作为起始点,从这些节点ReferenceChains开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;
两次标记过程:
对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。
垃圾回收算法
垃圾回收算法:复制算法、标记清除、标记整理、分代收集
复制算法:(young)
将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收;
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
标记清除:(cms)
标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,需要预留空间给分配阶段的浮动垃圾
标记整理:(old)
标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题
分代收集:
根据各个年代的特点选择合适的垃圾收集算法:
新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
Safepoint 当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的(safe),整个堆的状态是稳定的。如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,造成了整体 GC 的时间变长。