java面试基础知识点总结
2023-05-28 09:30:00 4 举报
AI智能生成
主要总结了Java日常面试的基础知识点,分模块展示方便复习。
作者其他创作
大纲/内容
java基础
数据类型
java8种基本数据类型
boolean ~
为什么boolean没有规定字节数?
byte 1个字节
char 2个字节
shot 2个字节
int 4个字节
long 8个字节
fload 4个字节
double 8个字节
8种包装类型
分类
Boolean
Byte
Character
Short
Integer
Long
Double
缓冲池
什么是缓冲池?
那些包装类具有缓冲池?
Byte
Short
Character
Integer
Long
String相关
String
改变
在java8中String使用char数组存储数据
在java9中String使用byte数组存储字符串,并且同时使用coder来标识使用了哪种编码
String为什么被声明为final(声明为final的好处?)
什么是StringPool(StringTable)
StringBuilder
线程不安全,可以被改变
StringBuffer
线程安全,内部被synchronized同步
运算
switch
支持类型
byte和Byte
char和Character
short和Short
int和Integer
String
enum
注意:从java7开始才能使用char(Character)和String
关键字
final
可以声明的位置
类
该类不能被继承
方法
该方法不能被重写
数据
基本类型
使数值不能够被修改
引用类型
引用不能变,但是被引用变量的对象本身可以被修改
final修饰的变量也可以被修改
使用java提供的反射技术,并且关闭安全校验也可以修改其值
static
可以声明的位置
类
方法
成员变量
静态代码块
初始化顺序
1.父类(静态变量,静态代码块)
2.子类(静态变量,静态代码块)
3.父类(实例变量,普通代码块)
4.父类构造方法
5.子类(实例变量,普通代码块)
6.子类构造方法
继承
访问权限
private 类访问
default 包访问
protected 继承访问
这个修饰符在继承体系中成员对于子类可见,但是这个修饰符对于类没有意义
public
重写
子类只能够重写父类非私有的的,非静态的方法
子类重写父类的方法可以扩大访问权限
子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
抽象类和接口
接口
新特性
在java8中接口可以有default方法,也可以有静态方法
但是静态方法不能够被重写,default方法可以被重写
接口中的方法默认都是public的,并且不允许定义为其他的
接口中的字段都是static和final的
抽象类
抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类不能被实例化,只能被继承。
异常
分类
Exception
RuntimeException
非运行时异常
运行时异常和非运行时异常区别?
Error
OutOfMemoryError
StackOverflowError
ThreadDeath
...
Exception和Error的区别?
泛型(参数化类型)
分类
泛型类
如果一个类继承了一个泛型类,要么在子类的泛型声明上上声明父类泛型参数,要么指定父类泛型的具体类型。
泛型方法
泛型接口
泛型抽象类
泛型的通配符和泛型的上限和下限
在使用”?“只能接收,不能修改。
上边界通配符会导致修改失效,但是获取有效
下边界通配符会导致获取有效,但是修改失效
原因
容器
Collation
List
ArrarList
动态数组实现
初始容量10(只有在第一次添加的时候才会真正分配初始容量),每次扩容1.5倍,如果扩容1.5倍还不够就会使用最小容量
线程不安全
实现了RandomAccess接口,支持快速随机访问
LinkedList
双向链表
线程不安全
插入和删除时间复杂度几乎为O(1)
不支持快速随机访问
空间消耗比ArrayList消耗更大(因为每个节点都比ArrayList多了两个引用)
Vector
动态数组实现
线程安全
初始容量10,每次扩容2倍,并且可以指定每次容量的增量
可以理解为加了synchronized的ArrayList
CopyOnWriteArrayList
原理
写入时复制
特点
适合大量读操作,但是不适合大量的写操作
可以读写分离,但并不是读写锁实现的
线程安全
初始容量0,每次写入的时候容量扩容,新的容量=插入容量+旧容量
Set
TreeSet
TreeMap换皮
HashSet
HashMap换皮
LinkedHashSet
LinkedHashMap换皮
CopyOnWriteSet
和CopyOnWriteArrayList原理一样
Queue
Deque
常用子类
ArrayDeque
BlockingQueue
常用子类
LinkedBlockingQueue
ArrayBlockingQueue
四组常用API
抛出异常
add
remove
element
有返回值,不会抛出异常
offer
poll
peek
阻塞等待
put
take
超时等待
offer(,,)
poll(,)
使用阻塞队列实现生产者和消费者模型
AbstractQueue
Map
HashMap
升级
在JDK1.7中HashMap使用数组加链表来实现
在JDK1.8中HashMap使用数组+链表+红黑树实现
JDK1.8原理
初始容量16(第一次插入才会真正分配)
每次扩容2倍
如果当前桶的容量大于装填因子*总容量就触发扩容
如果一个链表的节点个数超过8个,并且桶的长度超过64个则链表升级为红黑树,如果没有超过64个则单纯扩容2倍
首先先把hash值高16位和低16位按位与计算,然后计算桶位置算法是 index = (n - 1) & hash相当于index=hash%n
如果一颗红黑树的节点小于6个则红黑树退化为链表
线程不安全
可以存NULL
HashTable
线程安全
默认初始容量11,装填因子0.75
每次扩容容量是2倍+1
直接使用hasoCode的值作为hash值
TreeMap
红黑树实现
key自动排序
LinkedHashMap
链表实现
应用场景
HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap
线程不安全
ConcurrentHashMap
JDK1.7
实现
分段锁 segment,锁的粒度更加精细
分段的数组+链表的形式
分段的数组+链表的形式
原理
get
get不需要加锁,采取volatile修饰共享变量
这样每次get的时候都可以获取最新的结构更新
由于遍历不需要加锁的原因是因为next是final。要么
是不为空返回,为空的话就加锁重读
这样每次get的时候都可以获取最新的结构更新
由于遍历不需要加锁的原因是因为next是final。要么
是不为空返回,为空的话就加锁重读
put
1.二阶段hash,第一阶段是定位到哪个segment
第二阶段是定位到哪个entry
第二阶段是定位到哪个entry
2.entry中,除了value,还有key,hash和next都是final修饰
意味着不能从中心或者尾部添加或者删除节点,一律添加到头部
意味着不能从中心或者尾部添加或者删除节点,一律添加到头部
3.通过count来统计段内的数据个数,只有新增和删除
会修改这个值,每次都是在上述俩个步骤的最后一步进行修改
会修改这个值,每次都是在上述俩个步骤的最后一步进行修改
remove
由于是final,所以删除节点的时候会删除某个节点
然后那个节点之上都复制,然后最后一个节点指向
被删节点的下一个节点
然后那个节点之上都复制,然后最后一个节点指向
被删节点的下一个节点
resize
扩容只会对段扩容而非整个桶
跟HashMap不同的是,段是先判断
是否需要扩容再put,而hashmap是
先put再判断是否要扩容
跟HashMap不同的是,段是先判断
是否需要扩容再put,而hashmap是
先put再判断是否要扩容
size
先尝试不锁住segment的方式来统计segment的大小
如果统计过程中,容器的count发生变化则采取加锁的方式
如果统计过程中,容器的count发生变化则采取加锁的方式
JDK1.8
取消了分段锁,而是采取了cas和synchronized来保证并发安全,
synchronized只锁住当前链表或者红黑二叉树的首节点,只要hash
不冲突,就不会产生并发,效率很高
synchronized只锁住当前链表或者红黑二叉树的首节点,只要hash
不冲突,就不会产生并发,效率很高
可以认为是JDK1.8版本你的HashMap+CAS和synchronized实现的
并发
进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
一个进程可以有多个线程,也就是进程是线程的容器,每个进程都至少有一个线程
并发编程模型中的两个关键问题
线程之间如何通信
线程之间如何同步
通信的两种机制
共享内存
消息传递
JMM(java内存模型)
Java的并发采用的是共享内存模型
主内存和工作内存
处理器上的寄存器的读写比内存快几个数量级,为了解决这种,加入了高速缓存
带来的问题?
缓存一致性问题
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,它试图屏蔽各种操作系统的内存访问差异,以实现java程序在各种平台下都能达到一致的内存访问效果
8总内存间交互操作
read 把一个变量的值从主内存传输到工作内存
load 在read之后执行,把read得到的值放入工作内存的变量副本中
use 把工作内存中一个变量的值传递给执行引擎。
assign 作用于工作内存的变量,它把一个从执行引擎收到的值赋值给工作内存的变量
store 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
write 作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
lock 作用于主内存的变量,把一个变量标识为一条线程独占状态
unlock 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
happens-before
从JDK5开始,java使用happens-before概念来阐述操作间的内存可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
规则
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
线程中断规则:对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
java内存模型中的重排序
为什么需要指令重排序?
因为指令重排序能够提高性能
指令重排序的种类
编译器优化的重排序
指令级并行的重排序
内存系统的重排序
规则
as-if-serial 不管如何重排序,都必须保证代码在单线程下的运行正确。
带来的问题
解决方案
使用锁同步
在临界区内的代码可以重排序,但是由于互斥特性,其他线程感受不到重排序带来的影响
基于happens-Before规则编程
可以使用内存屏障的方法禁止重排序,我们遵守happens-Before规则底层就是使用了内存屏障
实现线程的方式
继承Thread类重写run方法
缺点:java中类只能支持单继承。
实现Runnable接口
缺点:不能获取返回值,还不能抛出异常
实现Callable接口以及call方法,通过FutureTask包装器来创建Thread线程
特点:可以获取线程的返回值,可以抛出异常,并且get方法在获取返回值的时候会被阻塞,而且结果还会被缓存
线程的启动方式
线程的启动方式只有一种,将实现接口或者被继承的类放入Thread类中通过start启动
java的线程启动是依赖C或C++实现的,java本身不能够开启线程,java的start方法底层依赖start0方法启动,而start0方法是一个native方法
JAVA程序运行的时候至少有两个线程
主线程
GC线程
java中线程的6个状态
NEW 新生
RUNNABLE 运行
BLOCKED 阻塞
WAITING 等待
TIMED_WAITING 超时等待
TERMINATED 终止
线程操作的常用方法
new Thread().setDaemon(boolean on)
设置为守护线程
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
必须在开启线程前使用setDaemon方法
Thread.sleep(long millis)
休眠当前线程,单位为毫秒
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回
sleep不会释放锁
建议使用TimeUnit类代替sleep,因为TimeUnit能够更精细化控制
如果线程睡眠时间到了,该线程也不会立即执行,只是从睡眠状态变成了可运行状态
new Thread().setPriority(int newPriority)
设置线程的优先级
优先级分为1~10,数值越大优先级越高,CPU优先执行优先级高的任务
优先级高的任务并不一定先执行
Thread.yield()
线程让步,当前线程会释放CPU的执行权,转入就绪状态。
yield不会释放锁
new Thread().join(long millis, int nanos)
阻塞调用此方法的线程进入 TIMED_WAITING 状态,直到目标线程完成,此线程再继续;
new Object()
wait
让线程进入等待
会释放锁,但是不会释放所有的锁,只会释放当前对象的锁
notify和notifyAll
notify随机唤醒一个线程,如果被唤醒的线程之前放弃的锁被其他对象所有,那么这个线程会进入阻塞状态,他必须等待其他线程释放这个锁,才能开始执行。
notifyAll唤醒所有的线程
使用notifyAll和wait实现一个生产者和消费者模型
中断
inInterrsupted和interrupted区别
isInterrupted是非静态的
interrupted是静态的,而且本质上是调用了当前线程中的isInterrupted 方法,不过传入了一个参数true
interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态
isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态
synchronized
锁升级过程
无锁
偏向锁
轻量级锁
重量级锁
特性
可重入性
原理
每个synchronized都和一个monitor关联,当获得锁的时候monitor中的成员变量recursions就+1
可以避免死锁,如果没有可重入特性,那么线程第二次获得该锁的时候就会死锁
不可中断特性
异常会释放锁
锁的对象
在静态方法上synchronized锁的是当前类的class对象
在普通方法上synchronized锁的是当前对象
传入对象的时候synchronized锁的是传入对象
CAS(乐观锁)
简介
Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数
据进行操作的一种特殊指令。
据进行操作的一种特殊指令。
作用
CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共
享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧
的预估值X等于内存中的值V,就将新的值B保存到内存中。
享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧
的预估值X等于内存中的值V,就将新的值B保存到内存中。
使用CAS和阻塞队列实现一个锁
JUC
什么是JUC
JUC是java.util.concurrent工具包的简称,他是并发大师Doug Lea的杰作
AQS(AbstractQueuedSynchronizer)队列同步器
是JUC的核心
原理
围绕着一个同步队列和park还有自旋锁实现
获取公平锁和非公平锁的区别
公平锁线程在唤醒的时候不能插队,非公平锁可以插队
详解(以公平锁为例)
lock加锁
lock方法真正调用的是acquire(1方法)
acquire方法
tryAcquire
返回true
加锁成功,不在执行后续方法
返回false
调用addWaiter
调用AcquireQueued
unlock解锁
真正调用的是sync.release方法
该方法会唤醒第一个排队线程,并且把排队线程的上一个节点的ownerThread置为null,然后让head指向该节点
ReentrantLock
可重入可中断锁,他是Lock最重要的实现类之一
FairSync()这个方法是公平锁,调用NonfairSync()方法是非公平锁
CountDownLatch(减法计数器)
用法
创建该对象的时候可以传入一个数值,每次调用countDown就会减一,计数器为零的时候await才不会阻塞
CyclicBarrier(加法计数器)
用法
创建该对象的时候需要设置一个值,并且可以写一个实现Runnable的类,每次调用await就会加1,当加到设定的值后就会触发实现Runnable类的线程开启
Semaphore(信号量)
用法
可以用来做多线程限流,创建该对象的时候可以传入一个资源数量,每次调用acquire就会减少一个资源,资源为零的时候在调用该方法会导致他的线程进入阻塞状态,除非调用release释放锁。
读写锁ReentrantReadWriteLock
读锁和写锁互斥
写锁与写锁互斥
读锁和读锁可以共存
演示
阻塞队列
详情参考容器中的BlockingQueue
SynchronousQueue
容量为一的同步队列
线程池
阿里巴巴开发规范中不允许使用Executors去创建线程池,而是推荐使用ThreadPoolExecutor的方式创建
三大方法
Executors.newSingleThreadExecutor();// 单个线程
Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
Executors.newCachedThreadPool(); // 可伸缩
七大参数
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle // 拒绝策略
4种拒绝策略
假设现在有一个银行,银行的窗口就是线程,进来办理业务的人就是task
new ThreadPoolExecutor.AbortPolicy() // 满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出常!
ForkJoin
Future
MYSQL
引擎
MyISAM
文件
frm文件:存储表的定义数据
MYD文件:存放表具体记录的数据
MYI文件:存储索引
特点索引存放的是数据具体存放在磁盘上的地址
InnoDB
一张表最多有16个索引,每个索引的最大长度是255个字节
事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,上图也看到了,InnoDB是默认的MySQL引擎。
索引
- 为什么MYSQL使用B+树作为索引数据结构?
索引的分类
单值索引
即一个索引只包含单个列,一个表可以有多个单列索引(建议一张表索引不要超过5个
优先考虑复合索引)
优先考虑复合索引)
复合索引(联合索引)
即一个索引包含多个列
聚簇索引
将数据存储与索引放到了一块,找到索引也就找到了数据
聚簇索引具有唯一性
唯一索引
索引列的值必须唯一,但允许有空值
为什么推荐尽量使用复合索引而不是使用唯一索引呢?
因为MYSQL每次查询只能使用一个索引,如果我们sql语句查询条件包含两个字段,那么使用单值索引需要查询两次,但是复合索引只需要一次即可,有时候覆盖索引完全覆盖可以不回表查询
索引的优缺点
优点
提高数据检索效率,降低数据库IO成本
通过索引列对数据排序,降低数据排序成本,降低CPU的消耗
缺点
实际上索引也是一张表,该表保存了主键和索引字段,并指向实体表的记录,所以索引列也是要占用空间的
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如果对表INSERT,UPDATE和DELETE。
因为更新表时,MySQL不仅要不存数据,还要保存一下索引文件每次更新添加了索引列的字段,
都会调整因为更新所带来的键值变化后的索引信息
因为更新表时,MySQL不仅要不存数据,还要保存一下索引文件每次更新添加了索引列的字段,
都会调整因为更新所带来的键值变化后的索引信息
索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立优秀的索引,或优化查询语句
什么是回表?
索引优化
Explain(查询语句执行计划)
作用
表的读取顺序
数据读取操作的操作类型
哪些索引可以使用
哪些索引被实际使用
表之间的引用
每张表有多少行被优化器查询
字段
ID
查询序号,id相同从上往下,id不同id越大优先级越高
select_type
表示示查询中每个select子句的类型
table(重要)
显示这一步所访问数据库中表名称(显示这一行的数据是关于哪张表的),有时不是真实的表名字,可能是简称,例如上面的e,d,也可能是第几步执行的结果的简称
partitions
代表分区表中的命中情况,非分区表,该项为null
type(重要)
对表访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型”。
ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用(该查询可以利用的索引,如果没有任何索引显示 null)
Key(重要)
key列显示MySQL实际决定使用的键(索引),必然包含在possible_keys中
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
ref
列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
rows(重要)
表示MySQL估计未来找到所需要的行而要读取的行数
Extra(重要)
这一列包含的是不适合在其他列显示的额为信息
什么是复合索引的最左匹配原则?
索引失效的情况
如果查询条件用or,必须or条件中的每个列都加上索引,否则无效。(尽量使用union代替)
复合索引未用左列字段;
like以%开头;
需要类型转换;
where中索引列有运算;
where中索引列使用了函数;
如果mysql觉得全表扫描更快时(数据少)
关联查询优化
保证被驱动表的join字段有索引
left join时,选择小表为驱动表,大表为被驱动表,因为驱动表一定要做全表扫描。
inner join时,mysql会自己帮你把小结果集的表选为驱动表
子查询尽量不要放在被驱动表。因为子查询会生成虚拟表导致有可能使用不到索引
能够直接关联查询,尽量不用子查询。
事务
在mysql中myisam不支持事务
事务四大特性(ACID)
原子性
一致性
隔离性
持久性
事务隔离级别
读未提交:read uncommitted
读已提交:read committed
Oracle默认隔离级别
可重复读:repeatable read
MySQL默认级别
串行化:serializable
事务带来的问题
脏读
一个事务读取了另一个事务未提交的数据
不可重复读
一个事务两次读取同一个数据,两次读取的数据不一致
幻读
一个事务两次读取一个范围的 记录,两次读取的记录数不一致。
更新丢失
一个事务的更新覆盖了另一个事务的更新,解决办法使用乐观锁或者使用排它锁
锁
分类
按照锁机制分类
共享锁(读锁)
排他锁(写锁)
按照锁的粒度分类
表锁(偏读)
行锁(偏写)
页锁(DBD引擎采用)
innoDB使用的是行锁myisam使用的是表锁
行锁退化到表锁
更新的时候没有索引或者索引失效时,InnoDB 的行锁变表锁
间隙锁
间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制,
java虚拟机
意义
屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
运行时数据区组成
线程私有
程序计数器
当前线程所执行的字节码的行号指示器:
- 如果正在执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
- 如果正在执行Natvie方法,计数器值为空(Undefined)
作用
Java虚拟机字节码解释器通过改变这个计数器的值来选取下一条要执行的字节码指令
JVM规范中唯一没有规定任何OutOfMemoryError的情况
虚拟机栈
用途:为JVM执行Java方法服务
编译期间完成分配
结构:栈帧:局部变量表、操作数栈、动态链接、方法出口
基本类型变量,(boolean,byte,char,short,int,float,long,double)
对象句柄
方法参数
方法的局部变量
两种异常:StackOverFlowError、如果可以动态扩展的话会产生(OutOfMemoryError) -Xss设置栈大小
本地方法栈
用途:为虚拟机使用到的native方法服务
两种异常:StackOverFlowError、OutOfMemoryError
线程共有
元空间
用途:用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
需要垃圾回收
运行时常量池
内容:存放编译产生的字面量(常量final)和符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
特点:运行时常量池相对于Class文件常量池的一个重要特征是具备动态性
Java语言并不要求常量一定只有编译期才能产生,运行期间也能将新的常量放入池中
异常:(无法扩展)OutOfMemoryError
方法区的GC
参考垃圾对象的判定
GC堆
目的:存放对象实例和数组
需要垃圾回收gc管理的主要区域
分代处理
目的:更好的回收内存和更快的分配内存
新生代
Eden空间
From Survivor空间
To Survivor空间等
老年代
空间结构:逻辑连续的空间,物理可以不连续
优先分配TLAB(Thread Local Allocation Buffer),减少加锁,提高效率
异常:如果在堆中没有完成内存分配,并且堆也无法扩展时,会抛出OutOfMemoryError
-Xms初始堆大小 -Xmx最大堆大小
并不是所有的对象都在堆中分配
对象的创建
1、检查参数是否在常量池中定位到一个类的符号引用;该类是否被加载、解析、初始化过
若没有做进行类加载
2、若有则分配内存
内存绝对规整
用“指针碰撞”来分配内存
内存不规整
用“空闲列表”来分配内存
线程安全
对分配内存空间的工作进行同步处理
采用CAS+失败重试的方式保证更新操作的原子性
每个线程分配一块本地线程分配缓冲区
TLAB
-XX:+/-UseTLAB
3、始化已分配内存为零值(保证类型不赋初值可以使用)
4、上面工作完成后,执行init方法按照程序员意愿初始化对象
对象的内存布局
对象头
存储运行时数据
存储类型指针
如果对象是数组对象,还有一个保存数组长度的空间,占4个字节
实例数据
是对象真正存储的有效信息
对齐填充
起占位符的作用
对象创建流程图
对象的访问定位
使用句柄
堆中有句柄池,存储到实例数据和类型数据的指针;栈中的引用指向对象的句柄地址
优点
- reference中地址相对稳定;
- 对象被移动(GC时)时只会改变句柄中的实例数据指针
直接指针
栈中的引用直接存储对象地址,到方法区中类型数据的指针包含在对象实例数据中
优点
访问速度快,节省了一次指针定位的开销
OOM
虚拟机栈和本地方法栈溢出
线程请求栈深度大于最大深度StackOverFlowError
设置-Xss128k,在单线程下,通过不断调用递归方法。
新线程拓展栈时无法扩展出现OutOfMemoryError错误
不断创建新线程,并让创建的线程不断运行
-Xss
方法区和运行时常量池溢出
java.lang.OutOfMemoryError后会跟PermGen space
不断创建新的字符串常量,并添加到list中
-XX:PermSize和-XX:MaxPermSize
堆溢出
java.lang.OutOfMemoryError:Java heap space
内存泄漏
通过不断创建新对象,并放入list中,保证GCRoots到对象之间路径可达
内存溢出
-Xms -Xmx
本机直接内存溢出
在Heap Dump文件中没有明显异常
-XX
垃圾对象的判定
对象的引用
强引用
存在就不回收
软引用
将要发生内存溢出之前
实现缓存
弱引用
下一次垃圾回收
回调函数中防止内存泄露
虚引用
对对象的生存时间没有影响
能在这个对象被收集器回收时收到一个系统通知
引用计数法
难以解决对象之间相互循环引用的问题
可达性分析法
从GC Roots向下搜索建立引用链;一个对象如果到GC Roots没有任何引用链相连时,证明对象不可用
GC Roots
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
注意:当对象被标记为可回收后,对象不会马上被回收,而是会去执行finalize方法,如果finalize方法被覆写了后引用了其他对象,那么这个对象有可能会复活。
堆中垃圾回收过程
1、如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
2、判断对象是否有必要执行finalize()方法,(没有覆盖,被调用过,都没有必要执行),放入F-Queue队列
3、放入F-Queue中,进行第二次标记
4、被拯救的移除队列,被两次标记的被回收
方法区中垃圾回收
废弃常量
没有任何一个对象引用常量池中的“abc”常量
无用的类(满足条件可以被回收,非必然)
1、该类所有的实例都已经被回收
2、加载该类的加载器被回收
3、该类对应的javalang.Class对象没有在任何地方被引用,无法通过反射访问该类的方法
垃圾回收算法
标记-清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,产生大量不连续的内存碎片,连续内存不足会再次触发GC
标记-复制算法
将内存等分,每次用一块,当这块内存用完了,就将活着的对象复制到另一块,然后把前者清空
- 对象存活率较高时就要进行较多的复制操作,效率将会降低
- 空间利用率低
标记-整理算法
所有存活的对象移向一端,然后直接清理掉端边界以外的内存
分代收集算法
新生代
复制算法
老年代
标记-整理或标记-清除
HotSpot算法
枚举根结点
当执行系统停顿下来后,并不需要一个不漏的检查完所在执行上下文和全局的引用位置,在HotSpot的实现中,使用一组称为OopMap的数据结构来存放对象引用
安全点
在这些特定的位置,线程的状态可以被确定
位置
方法调用指令
循环跳转指令
异常跳转指令
中断方式
抢占式
GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑在安全点上
主动式
设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起
安全区域
背景:线程Sleep状态或者Blocked状态的时候,无法响应JVM中断,走到安全的地方,JVM也不能等他们,这样就无法进行GC
安全区域是指在一段代码中,引用关系不会发生变化,这个区域中的任何地方开始GC都是安全的。
垃圾收集器
Serial收集器
特点
新生代收集器
采用复制算法
单线程收集
进行垃圾收集时,必须暂停所有工作线程,直到完成
应用场景
是HotSpot在Client模式下默认的新生代收集器
简单高效(与其他收集器的单线程相比)
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
参数设置
"-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;
ParNew收集器
特点
新生代收集器
采用复制算法
除了多线程外,其余的行为、特点和Serial收集器一样
应用场景
Server模式下,ParNew收集器是一个非常重要的收集器
单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销
参数设置
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
Parallel Scavenge收集器
特点
新生代收集器
采用复制算法
多线程收集
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)
应用场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互
参数设置
"-XX:MaxGCPauseMillis"控制最大垃圾收集停顿时间
"-XX:GCTimeRatio" 设置垃圾收集时间占总时间的比率
"-XX:+UseAdptiveSizePolicy"
Serial Olc收集器
特点
针对老年代
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact)
单线程收集
应用场景
主要用于Client模式
在Server模式中
在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配)
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
Parallel Old收集器
特点
针对老年代
采用"标记-整理"算法
多线程收集
应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器
特别是在Server模式,多CPU的情况下
在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合
参数设置
"-XX:+UseParallelOldGC":指定使用Parallel Old收集器
CMS收集器
特点
针对老年代
基于"标记-清除"算法(不进行压缩操作,产生内存碎片)
以获取最短回收停顿时间为目标
并发收集、低停顿
需要更多的内存(看后面的缺点)
应用场景
与用户交互较多的场景
希望系统停顿时间最短,注重服务的响应速度
以给用户带来较好的体验
如常见WEB、B/S系统的服务器上的应用
参数设置
"-XX:+UseConcMarkSweepGC":指定使用CMS收集器
运行过程
初始标记
并发标记
重新标记
并发清除
缺点
对CPU资源非常敏感
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
产生大量内存碎片
G1收集器
特点
并行与并发
GC收集线程并行
用户线程与GC线程并发
分代收集,收集范围包括新生代和老年代
空间整合:结合多种垃圾收集算法,空间整合,不产生碎片
可预测的停顿:低停顿的同时实现高吞吐量
应用场景
面向服务端应用,针对具有大内存、多处理器的机器
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案
运行过程(不计Remembered Set操作)
初始标记
标记GC Root能直接关联到的对象
需要停顿用户线程
并发标记
对堆中对象可达性分析
并发执行
最终标记
修正并发标记中因用户线程运行发生改变的标记记录
需要停顿线程
筛选回收
对Region的回收价值和成本排序,根据参数指定回收计划
可以并发
参数设置
"-XX:+UseG1GC":指定使用G1收集器
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒
"-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region
内存分配和回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
大对象直接进入老年代
所谓大对象,就是需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串以及数组
长期存活的对象将进入老年代
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无须等到MaxTenuringThreshold中要求的年龄
空间分配担保
Minor GC之前,JVM检查老年代最大连续空间是否大于新生代所有对象的空间,成立则确保Minor GC安全
不成立,参看参数HandlePromotionFailure是否允许担保失败,允许则检查老年代最大连续空间是否大于历次晋升的对象的平均大小,大于则尝试Minor GC
否则,进行Full GC
虚拟机类加载机制
类加载的时机
生命周期
加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分统称为连接
以下情况对类进行初始化
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发父类的初始化
当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
类加载的过程(5)
加载
完成3件事
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类的来源
从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
从网络中获取,这种场景最典型的应用就是applet
运行时计算生成,这种场景使用得最多的就是动态代理基础
由其它文件生成,典型场景就是JSP应用,即由JSP文件生成赌赢的Class类
从数据库中读取,这种场景相对的少些,例如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
验证
验证是连接阶段的第一步,这一阶段的目的就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
验证项
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区汇总进行分配
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
解析动作分类
类或接口的解析
字段解析
类方法解析
接口方法解析
初始化
类初始化是类加载过程中的最后一步,这时才真正开始执行类中定义的Java程序代码
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要它们的类加载器不同,那这两个类就必定不相等
双亲委派模型
分类
启动类加载器
这个类加载器使用C++语言实现,是虚拟机自身的一部分
其他类加载器
这些类加载器由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
扩展类加载器
应用类加载器
作用
保护Java底层类,防止被破坏
原理
当应用类加载器需要加在某个类时,这个时候它不会自己直接加载而是先去缓存查询,如果没有再委托父类加载器加载,父类加载器也是相同逻辑,直到到了启动类加载器如果还是没有,那么启动类加载器就会委派子类加载器加载,直到应用类加载器自己去加载。
IO
BIO
磁盘操作:File
字节操作:InputStream 和 OutputStream
字符操作:Reader 和 Writer
对象操作:Serializable
网络操作:Socket
新的输入/输出:NIO和AIO
NIO
NIO是指JDK1.4开始提供的新的api,他是一个非阻塞面向缓冲区的高性能IO
如果理解NIO是面向缓冲区的呢?
NIO和BIO的区别
NIO是面向块的BIO是面向流的
NIO是非阻塞的,BIO是阻塞的
非阻塞的有什么用?
直接缓冲区与非直接缓冲区
非直接缓冲区需要经过一个copy的阶段从内核空间copy到用户空间
直接缓冲区不需要经过copy阶段
使用直接缓冲区的两种方式
使用ByteBuffer的allocateDirect方法
使用FileChannel的map方法
NIO的三个核心部分
buffer缓冲区
本质
数组
状态变量
capacity 最大容量
position 当前已经读写的字节数
还可以读写的字节数
常用实现
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Channel管道
管道的理解
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。可以理解为管道就是火车道,缓冲区就是火车。
常用管道
FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
管道和流的区别
管道可以同时进行读写,而流只能读或者只能写
管道可以实现异步读写数据
管道可以从缓冲读数据,也可以写数据到缓冲
Selector选择器
一个组件,可以检测多个NIO channel,看看读或者写事件是否就绪。
多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
5种IO模型
阻塞IO
非阻塞IO
信号驱动IO
IO多路转接
异步IO
阻塞程度
阻塞IO>非阻塞IO>多路转接IO>信号驱动IO>异步IO
效率
阻塞IO<非阻塞IO<多路转接IO<信号驱动IO<异步IO
0 条评论
下一页