JAVA基础面试
2023-11-16 19:58:17 0 举报
AI智能生成
包含初级JAVA开发程序员对于JAVA基础,锁,并发,JVM的面试知识体系和内容
作者其他创作
大纲/内容
JAVA基础
面向对象
封装
封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个学生类,其中包含了姓名,年龄,家庭地址等属性,并且有学习,玩耍等方法。
继承
像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。 如Dog类可以继承Animal类,继承过来嘴巴、颜色等属性, 吃东西、奔跑等行为。
多态
多态是指在父类中定义的属性和方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个属性或方法在父类及其各个子类中具有不同含义。
接口和抽象类
接口
接口的职责是为了规范,我们平时也提倡面向接口开发
抽象类
抽象类的主要是为了复用代码,比较典型的就是模版方法模式
异常
Throwable
Exception
Exception是一些代码层面的可预见的可以进行捕获处理的异常
受检异常
受检异常必须要在代码中显式声明并且有对应的处理,比如捕获或者向上抛出。否则编译无法通过,一般是IO操作多一些
非受检异常
非受检异常多是一些代码层面的问题,继承自RuntimeException,不显式的去声明,但是一旦发生,程序就会中断。
常见的RuntimeException
索引越界异常
空指针异常
算数异常
非法参数异常
类转换异常
Error
Error是程序系统或者虚拟机发生严重的错误,比如OOM(内存溢出),SOE(栈溢出)
final
final修饰变量,表示这个变量是最终的,不能被更改
final修饰类,表示这个类不能被继承
final修饰方法,表示该方法不能被子类重写
用final修饰的对象,表示这个对象的引用地址是不可变的,但是对象的内容是可以修改的。例如,final MyClass obj = new MyClass();
将 obj 声明为⼀个不可变引⽤,指向⼀个可变的 MyClass 对象。
将 obj 声明为⼀个不可变引⽤,指向⼀个可变的 MyClass 对象。
包装类
因为JAVA是面向对象的语言,很多场景基本数据类型都不适用,比如创建集合容器要求的就是Object的对象。
为了让基本数据类型也拥有对象的特征,将基本数据类型封装起来,对其添加了属性和方法,方便我们在一些场景下使用
JAVA中提供了基本数据类型和包装类的自动拆装箱机制,简化了我们的很多操作
hashCode 和 equals ⽅法
hashCode方法用于快速比较两个对象是否不相同
equals方法底层默认还是使用“==”符号来进行判断,返回布尔值。
数值精度问题
为什么不能用浮点数来表示金额
浮点数表示的是一个近似值,用浮点数来表示金额会造成精度丢失的问题
可以用JAVA中提供的BigDecimal来表示金额
在BigDecimal中,equals和compareTo
Bigdecima的equals方法会比较两个数的值和标,比如0.1和0.10就会返回false
compareTo方法做了优化,忽略了标的比较,只比较值的大小
在使用BigDecimal时,为了避免精度丢失问题,优先使用参数为String的构造方法
String,StringBuffer和StringBuilder
String
String是不可变的,线程安全的
String为什么设计为不可变的?
线程安全
安全性
字符串可以用来保存url地址,密码等敏感信息,设计为不可变可以保障这些信息的安全性
缓存 ,提高性能
字符串是使用最广泛的数据类型,大量的创建非常耗费资源,所以JAVA在堆内存中维护了一个字符串常量池用来缓存,大大节省了堆内存的空间。两个相同内容的字符串变量可以指向字符串常量池中的同一个字符串对象。
可以作为Hash结构中的key
StringBuffer
StringBuffer的对象是可变的,是线程安全的,但是效率比StringBuilder低
StringBuilder
StringBuilder的对象是可变的,但不是线程安全的,效率较高
String是只读字符串,从底层源码来看是一个final类型的字符数组,一旦定义,无法再增删改,每次对String的操作都会生成新的String对象。所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
反射
反射可以在运行期间动态的获取到一个类的属性和方法
为什么需要反射
一个类在程序运行的时候从云端下载下来,或者从其他文件加载进来,那么我们如何使用这个类呢,这个时候就需要用到反射。
程序为了封装性,会尽量暴露最少的信息给外部类使用,从技术的角度来讲这是没错的。但是需求总是改变的(还记得根据手机壳的颜色来改变APP的主题),所以有时候我需要更大的权限,这个时候就需要使用反射。
JAVA的动态代理
JDK动态代理
JDK动态代理是通过反射来实现的
JDK动态代理面向的是实现了接口的类
CGLIB动态代理
CGLIB动态代理是通过继承来实现的
CGLIB动态代理不能代理被final修饰的类
Spirng 默认采⽤ JDK 动态代理实现机制
反射的缺点
使用反射毕竟它要去动态的解析一个类,所以程序运行的性能会降低一些。
反射打破了我们代码的封装性,增加了维护成本。
try-catch-finally
finally中的代码在try-catch块中的代码执行完毕后一定会执行,无论是否发生异常。即使try或catch块中使用了return语句,finally块中的代码也会在return执行前执行。也就是说,finally块中的代码会在函数返回之前执行。
什么情况下finally代码块不会执行
try或者catch中执行了System.exit(),程序退出
程序发生宕机崩溃
JAVA中是值传递还是引用传递
编程语言中需要进行方法之间的参数传递,这个传递策略就是求值策略
值传递和引用传递就是比较常用的两种求值策略
值传递和引用传递最大的区别就是是否复制出来一个副本进行传递,如果有,那么就是值传递。否则就是引用传递
JAVA中的求值策略是值传递,只不过是把对象的引用关系复制了一份来进行传递。
克隆
为什么需要克隆
一般是需要创建一个相同的对象需要较多的资源,使用克隆可以节省资源,提高效率
如何实现克隆
通过colonable接口实现克隆
深克隆和浅克隆
浅拷贝是复制了对象的基本数据类型的值和引用数据类型的内存地址,当这一个对象修改引用数据类型时,另一个对象也会受到影响
深拷贝是完全独立开辟了一个新的内存空间,复制了对象的所有属性和元素到新的对象中,两个对象之间不会互相影响
集合
ArrayList和LinkedList
ArrayList
ArrayList底层的结构是数组,随着元素的添加,可以动态的改变数组的大小。通过get()和set()方法可以可以访问其内部的元素
ArrayList动态扩容
ArrayList初始容量为零,当第一次添加数据时,容量会初始化为10
添加元素的时候会先判断是否会超出数组容量
如果超出容量,则会按照旧数组的容量创建一个1.5倍容量大小的新数组
将旧数组的元素Copy给新数组,然后将新元素添加进来
LinkedList
LinkedList底层的结构是双向链表,在添加和删除操作的性能要高于ArrayList,但是在查找和更新的操作的性能低于ArrayList。
ArrayList和LinkedList都不是线程安全的,如果要使用线程安全的List
在方法内使用,局部变量是线程安全的
则需要使用Collections工具类的synchronizedList方法来创建List对象,它会对所有读写操作进行同步处理,保证多线程环境下的线程安全。
数组和List互相转换
数组转换成LIst
调用Arrays.asList方法
类似于浅克隆,修改数组的内容,list会受到影响
因为在底层,只不过是使用Arrays类下的ArrayList内部类进行了封装,指向的是同一个引用地址
List装换成数组
调用List.toArray方法
类似于深克隆,修改List内容,数组不会受到影响
因为在底层进行了数组拷贝,创建了一个新的数组。
HashMap
实现原理
HashMap的底层数据结构是哈希表,就是数组加链表或者红黑树的结构。添加数据的时候,对Key进行哈希运算计算出一个哈希值来确定数据在数组中的下标,如果发生哈希冲突,计算出相同的哈希值,就采用拉链法,将数据存放到数组连接的链表中。如果某一个数组元素下的链表长度大于8并且将数组长度大于64时,就将链表转换为红黑树。在扩容的时候,如果红黑树拆分的树的结点数小于6时,就退化为链表。
链表的时间复杂度是O(n)
红黑树的时间复杂度是O(logn)
添加元素
扩容
在初始化的时候,会创建一个长度为16的数组,HashMap的扩容是在判断数组的长度是否超过阈值时进行的,这个阈值是数组的长度乘以加载因子(0.75),当超过这个阈值时,会新建一个两倍长度的数组,遍历原数组上的每一个值,并且重新计算它们在新数组上的位置,将原来数组上的元素都存储到新数组对应的位置上。这个新数组就变成了HashMap的底层数据结构。
加载因子: HashMap的加载因子默认是0.75,这是一个经验上较为理论的值,最大程度的平衡了空间利用率和时间复杂度。
通过扩容,HashMap可以减少链表长度,提高查询、插入和删除操作的效率。
Hash冲突如何解决
采用拉链法 : 在数组下连接链表
开放寻址法: 发生哈希冲突时,就去寻找下一个空的散列地址进行存入
再哈希法 : 发生哈希冲突时,使用新的Hash函数计算哈希值,直到不产生冲突为止
HashMap为什么不是线程安全的
子主题
ConcurrentHashMap
如何保证线程安全
在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用分段锁和“CAS+Synchronized”的机制来保证线程安全。在JDK 1.8中,ConcurrentHashMap会在添加或删除元素时,首先使用CAS操作来尝试修改元素,如果CAS操作失败,则使用Synchronized锁住当前槽,再次尝试put或者delete。这样可以避免分段锁机制下的锁粒度太大,以及在高并发场景下,由于线程数量过多导致的锁竞争问题,提高了并发性能。
Set
Set保证元素不重复
在Java的Set体系中,根据实现方式不同主要分为两大类。HashSet和TreeSet。
TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入null值;底层基于TreeMap
HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束;底层基于HashMap
TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入null值;底层基于TreeMap
HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束;底层基于HashMap
并发
线程基础
进程和线程的区别
进程是正在运行程序的实例,进程中包含多个线程,每个线程执行不同的任务。 进程就好比是一列火车,线程就是一节节车厢
不同的进程使用不同的内存空间,在当前进程下,所有线程共享内存空间。
线程更轻量一些,线程上下文切换成本一般要比进程上下文切换要低
并发和并行
在多核cpu下,并发指的是同一时间应对多件事的能力,多个进程在一个或多个cpu下交替进行。并行是指同一时间下做多个事情的能力,四核cpu同时执行4个进程
线程
创建线程的四种方式
继承Thread类,重写run方法,通过start来开启一个线程
实现Runnable接口,重写run方法,通过start来开启线程
实现Callable接口,重写call方法。但是需要传入一个泛型,来作为这个线程的返回结果类型,通过配合futureTask的get方法来获取返回结果
通过实现Runnable接口,然后创建一个线程池对象,通过线程池来创建线程
为什么要使用线程池来创建对象
1.提高性能:线程池可以重复使用已经被创建的线程,避免了频繁创建和销毁线程的开销,从而提高了性能
2.控制线程数量:线程池限制了线程的数量,避免线程数量过多导致CPU的负荷过大,从而保证了系统的稳定性和可靠性。
3.提高可维护性:使用线程池可以将线程的创建和管理集中在一起,方便维护和管理。
4.提高代码复用性:线程池可以被多个模块或者组件共享,避免了重复编写线程池的代码,提高了代码的复用性。
Runnable和Callable的区别
Callable的Call方法有返回值
run方法和start方法的区别
run方法就是封装了线程的逻辑代码,只是一个普通的代码,可以多次调用
start方法是用来开启线程的,通过线程来调用run方法里面的逻辑代码,start只能被调用一次
如何实现精准的唤醒某一个线程
实现condition接口,condition接口里有await(),signal(),signalAll()方法。await()方法是让线程去等待,直到满足某个条件才被唤醒,而signal()就是用来唤醒某个条件的线程。
状态
线程包含哪些状态
新建(NEW)
可运行(RUNNABLE)
阻塞(BLOCKED)
等待(WAITING)
时间等待(TIMDE_WAITING)
死亡(TERMINATEO)
状态之间如何进行转换
wait()和sleep()
相同点
wait()和sleep()方法都是为了让线程放弃CPU的使用权,进入阻塞状态
不同点
wait()方法是Object类下的成员方法,sleep()方法是Thread的静态方法
wait()执行后会释放对象锁,允许其他线程获取对象锁,使用CPU。(我放弃了CPU,你们可以用)
如果sleep()方法在synchronized代码块中执行,它并不会释放对象锁。(我放弃了CPU,但是你们不可以用)
如何拿到异步执行之后的结果
Callable 配合Future机制
future是通过阻塞等待的方式来实现的,对性能不是很友好
CompletableFuture
将耗时的任务提交给线程池来进行异步处理,在处理完之后会触发一个回调函数来处理异步任务的处理结果。
相当于优化了Future的一个阻塞等待的一个问题
JMM(内存模型)
JMM把内存分成了两块,一块是私有线程的工作区域,是工作内存。一块是所有线程的共享区域(主内存)
线程和线程之间是互相隔离的,线程和线程之间的交互需要通过主内存
线程池
线程池的核心参数有七个
核心线程数
最大线程数
最大线程数=核心线程数+临时线程数
临时线程存活时间数值
临时线程存活时间数值单位
阻塞队列
当没有核心线程可以使用,新来的线程会存放到阻塞队列中等待,阻塞队列满的时候,会创建使用临时线程来处理任务
LinkedBlockingQueue
基于链表实现,默认长度是int的最大值,建议在创建是给定长度
在头结点和尾节点都加锁,出队和入队分别使用锁,互不影响,效率比较高
ArrayBlockQueue
基于数组实现,创建是需要给定长度
只有一把锁,入队和出队都有用着一把锁,效率相对低
线程工厂
可以定制线程的创建。 比如设置线程的名字,是否为守护线程
拒绝策略
当核心线程和临时线程都在使用,阻塞队列也放满的时候,就会触发拒绝策略
直接抛出异常,这是默认策略
用调用者所在的线程来执行任务,比如mian方法下调用,就用主线程来执行
丢弃阻塞队列中最靠前的任务,然后执行当前任务
直接丢弃这个任务
执行流程
如何确定核心线程数
如果是高并发,任务执行短
核心线程数=CPU核数+1
减少线程上下文切换
并发不高,任务执行时间长
IO密集型的任务:文件读写,数据库读写,网络请求
核心线程数=CPU核数*2+1
计算密集型任务:计算型代码,GSON转换
核心线程数=CPU核数+1
线程池有哪些状态
RUNNING
线程池正在运行,可以正常的接受并处理任务。
SHUTDOWN
线程池关闭了,不会再接收任务但是会把阻塞队列中的任务处理完
STOP
线程池停止工作了,不接收任务,也不会处理阻塞队列中的任务,中止所有线程
TIDYING
当所有任务都执行完了,线程池中也没有线程的时候,就会转为TIDYING状态,调用线程池的terminated()方法。
TERMINATED(终止)
调用完terminated()方法后就会转为这个状态。在ThreadPoolExecuter中,terminated()是一个空方法,可以自定义ThreadPoolExecutor重写这个方法。
ThreadLoacl
是JAVA提供的一个线程本地存储机制,可以利用该机制,将一些数据缓存到某个线程内部中,该线程可以在任意时刻,任意方法获取缓存的数据。
ThreadLoacl是一个线程本地存储类,比较常用的方法就是set(), get(), remove(), 它内部有一个ThreadLocalMap的内部类。这个ThreadLocalMap中维护了一个entry[]数组用来存储数据。
在调用set(), get(), remove()方法时,是以ThreadLoacl自己作为key,数据资源作为value,进行操作的。
为了防止ThreadLoacl内存泄漏的问题,建议使用完ThreadLoacl后调用remove方法清除掉数据资源。 因为ThreadLoacl中存在强引用,无法进行GC,日积月累下就会造成OOM的问题。
单机,集群和分布式有什么区别
单机
业务量小的时候把项目代码放到一台机器上部署。
集群
业务量变大的时候,一台机器的资源无法支撑的时候,就把相同的项目代码部署到多个机器上,使用负载均衡来对请求进行处理,将业务请求转发到压力比较小的结点机器上。
分布式
当业务发展到一定程度上,不管怎么增加结点机器,整个集群的性能到不会有多少提升。这个时候就考虑到用分布式结构,分布式就是将项目之中的功能模块分离出来,独立部署,存在依赖的模块之间使用RPC远程调用来传输需要的数据。这样不仅降低了系统之间的耦合度,而且大大提升的系统的性能。比如前后端分离,比如商城项目中的用户模块,订单模块,数据分析模块。
volatile
保证可见性和有序性
底层如何实现
保证了有序性
写屏障,禁止进行指令重排序
保证了可见性
主线程对于volatile修饰的共享数据变量的修改,子线程是可见的。
锁
线程安全
CAS
全称是 compare and swap (比较再交换),它是一种乐观锁的体现,可以在无锁的情况下保证线程共享数据的原子性。
为什么要让CAS
所有线程都是在运行着,没有发生阻塞,
ABA问题
在这个线程中CAS操作拿到共享数据和自己的预期值进行比较的时候,另外一个线程很快拿到共享变量改完,又改了回去。这个CAS操作还是会成功,虽然结果上是一样的,但是有些时候还是会对整个程序又影响。
我淘宝购物又退货。虽然从结果来看,商家和我都没有改变什么,但是本质上这件事是发生过的,这种现象是存在的。
悲观锁
对于这个线程的共享变量,我上锁了,你们其他线程都不能修改,我修改完了,把锁释放了,你们才能修改
synchronized
重量级锁
synchronized(对象锁)采用互斥的方法,让同一时刻只能有一个线程持有对象锁
它的底层由monitor实现的,monitor是jvm的对象,线程获得对象锁需要关联monitor
在monitor中,涉及到了用户态和内核态的转换,进程的上下文切换。开销较大,性能较低。
monitor有三个属性,分别是owner,entrylist,waitset
owner是关联的锁的线程,并且只能关联一个线程。entrylist关联的是出于阻塞状态的线程,waitset关联的是处于等待状态的线程
偏向锁和轻量级锁
轻量级锁
线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级锁修改了对象头的锁标志,相对于重量级做性能提升很多,每次修改都是CAS操作,保证了原子性。
偏向锁
如果在一段很长的时间里都是同一个线程使用锁,这时可以使用偏向锁来优化。在第一次获得锁的时候,会进行一次CAS操作,之后这个线程再次获取锁的时候,只需要在锁的对象头上判断是否是自己的线程ID即可,比每次都有进行CAS操作的开销要低。
锁升级
Reentrantlock
表示支持可重新进入的锁,调用lock()方法获取到锁之后,再次调用lock是不会堵塞的
底层主要使用CAS+AQS队列来实现
支持公平锁和非公平锁,默认是非公平锁,可以在构建对象的时候传入一个参数来修改为公平锁。
synchronized 和 Lock的区别
语法层面来讲:
synchronized是一个关键字,锁住的是对象,lock是一个接口,锁住的是线程。
synchronized会自动释放锁,Lock需要手动释放锁
功能层面:
都是悲观锁,都具备基本的互斥,同步,锁重入等功能
Lock提供了更多的功能,比如公平锁
Lock有更多的实现场景,比如reentrantLock,reentrantkReadWriteLock(读写锁)
性能层面
在没有竞争的情况下,synchronized也做了很多优化,性能还不错
在竞争激烈的情况下,lock的性能会更好一些
乐观锁
先执行一下再去对比,如果和之前的值不一样,就会再重试。
应用场景:在操作数据库的时候,在数据表中添加
版本号
在数据表中添加版本号,每次修改数据就将版本号+1。在更新数据的时候,先读取到数据的版本号,然后进行比对,如果相同,就进行修改成功。如果不相同,就说明有其他事务已经修改过这条数据,就需要进行回滚,然后重试。
时间戳
和版本号同理,时间戳记录最后更新时间。
AQS
是多线程的队列同步器,是一种锁机制
他维护了一个先进先出的双向队列,用来保存排队的线程
它里面有一个state参数,0表示无锁,1表示加锁。线程通过修改这个参数来加锁获取到了资源。
多个线程之间通过CAS操作来保证这个参数的原子性。
公平锁和非公平锁
排队点单的例子
公平锁和非公平锁
可重入锁和不可重入锁
可重入锁
一个线程可以多次的进行加锁,解锁也需要相同次数的解锁
ReentrantLock
Synchronized
不可重入锁
双重检测锁
是单例设计模式中的双重检测锁,避免对象的多次实例化
为什么会有双重检测锁
我来获取对象的时候,不管这个对象创没创建,我都要先加锁才能完成这个操作,导致很慢。所有就有了双重检测锁。
怎么做的
出现并发问题的根本原因
原子性
就是指一个线程在CPU中的执行是不可中断的也不可暂停的,要么执行完成,要么不执行。
如何保证原子性: 加锁 Synchronized 和 lock
可见性
就是在JMM内存模型中,线程之间是不可见的,这就导致多个线程对于共享数据同一时刻进行了同样的操作。
如何保证可见性: 1.在共享变量数据上添加volatile关键字 2.加锁Synchronized 和 lock
有序性
CPU为了提高运行效率,它会对代码的执行顺序进行优化,CPU中执行的顺序不是你代码中的顺序,但是执行结果是一致的。
如何保证有序性: 使用volatile关键字,禁止进行指令重排序
死锁
两个或多个线程互相需要对方的资源,但无法获取资源,导致其他线程一直无法执行
如何避免死锁
减少锁的使用或者细化锁的粒度,使用定时锁,保证锁的顺序。
什么是AIO BIO NIO
AIO
AIO是一个有效请求一个线程 非堵塞同步通信模式,采用异步通道实现通信,其基于事件和回调机制
BIO
BIO是一个连接一个线程,堵塞同步通信模式,客户端与服务器连接需要三次握手,使用简单但吞吐量少
NIO
NIO是一个请求一个线程,非堵塞同步通信模式,客户端与服务器需channel连接,始终只有一个线程,
并发编程的常见陷阱和解决方案
例如死锁,饥饿,活锁等问题,以及如何使用并发编程最佳避免这些问题
死锁,两个或多个线程互相需要对方的资源,但无法获取资源,导致其他线程一直无法执行,解决办法,减少锁的使用或者细化锁的粒度,使用定时锁,保证锁的顺序。
活锁:两个或多个线程互相干扰对方的进程,解决办法,改善算法
饥饿:某个线程无法获取cpu的时间片,无法进行资源共享,一直进人等待阶段,使用公平说,线程优先等。
JVM
四种引用
强引用
普通对象的引用就是强引用
对象之间存在强引用,JVM宁愿发生OOM,也不会让GC去回收这个对象
软引用
通过SoftReference类创建的软引用
GC不会回收这个对象,只有当发生OOM的时候才会回收存在软引用的对象
弱引用
通过WeakReference类创建的弱引用
如果一个对象只具有软引用,GC的时候就会被回收
虚引用
通过PhantomReference类创建的虚引用
虚引用没有具体的实现,虚引用和引用队列一起使用,主要是为了跟踪对象被垃圾回收的状态。当垃圾被对象回收的时候,对象会被放到队列里。
JVM结构
类装载子系统
负责将类加载到内存,并将其装换为运行时的数据结构
字节码执行引擎
将字节码文件解释成机器码
开启垃圾回收线程
运行时数据区
方法区
存放常量,静态变量,类信息
本地方法栈
存放一些本地方法
堆
存放我们new出来的对象
虚拟机栈(线程栈)
每个线程运行时所需要的内存空间
栈内存溢出情况
栈帧过多,导致栈内存溢出。典型的就是递归调用
栈帧过大
程序计数器
用来记录字节码执行的行号,每个线程私有的。就类似于在上下文切换的时候进行存档,CPU再执行这个线程的时候不需要重头开始,接着上次继续执行就可。
流程图
JIT优化技术
我们都知道JAVA代码是被编译成字节码文件,然后在虚拟机上解释成机器码来运行的。JVM中有一个解释器,就是将字节码文件解释成机器认识的机器码,解释器是一边解释一遍运行的,因此效率很低。
索引HotSpot虚拟机就引出了JIT优化技术。JIT优化技术还是基于解释器来执行的,只不过它在将一些频繁被使用的方法和代码记录成了热点代码,缓存了起来。程序再次运行的时候,这些热点代码会从缓存中查到直接变成机器码来被机器运行,提高了效率。
垃圾回收
标记对象是否是垃圾对象
可达性分析算法
GC Roots根节点 : 线程栈的本地变量,静态变量,本地方法栈的变量
从GC Roots开始根据引用关系进行向下查找标记,标记的就是非垃圾对象,没有被标记的就是垃圾对象。
引用计数法(已被淘汰)
对象头上有一个字段,如果这个对象被引用一次就加一,引用失效就减一。当这个字段为0时,就表示它是垃圾对象。
被淘汰的原因
不能解决对象之间存在互相依赖的问题。 比如A引用B,B也引用了A。除此之外A和B再没有其他引用,但是这两个对象的引用计数一直为1,无法通知GC来对这两个对象进行回收。
垃圾回收的具体算法
标记--清除算法
分为标记和清除两个阶段,标记阶段使用可达性分析算法对对象进行标记,然后清除掉为被标记的垃圾对象
优点: 速度较快
缺点: 会造成不连续的空间,产生空间碎片
复制算法
就是将内存空间分为两块,只使用一块空间,当这一块空间满的时候,把非垃圾对象复制到另外一块空间,把这一块空间里的对象全部清除掉
优点: 简单高效,不会产生空间碎片
缺点: 需要额外的内存空间
标记--整理算法
标记阶段对对象进行标记,清除阶段将被标记的对象全部整理到内存空间的一端,清除掉边界以外的垃圾对象
优点: 不会产生空间碎片
缺点: 速度比较慢
分代收集算法(主要)
是大多数JVM的选择。将内存空间分为年轻代和老年代。年轻代分为Eden区和survivor区,survivor又分为S0区和S1区。在年轻代使用标记复制算法,在老年代使用标记清除或者标记整理算法。在年轻代中Eden区满的时候会对整个年轻代进行一次minor gc,将存活下来的对象放到S0区,并且对象的年龄+1。Eden再满的时候,再进行minor GC,将存活下来的对象放到另一块S1区,把放不下的对象和年龄超过15的对象会放到老年代。当老年代满的时候会使用标记清除或者标记整理算法进行一次full GC,产生STW。
优点:不会产生空间碎片
缺点:需要额外的维护成本
类加载过程
加载
通过类加载器将类加载到内存中,并为之创建一个java.lang.Class对象
链接
验证
检查这个类是否有正确的结构,是否符合JVM的约束
准备
对类的变量分配内存空间,设置默认值
对于final修饰的变量,会在此时就对变量初始化值。
解析
把类的符号引用转为直接引用
初始化
为类的变量初始化值
双亲委派机制
机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终达到顶层的启动类加载器。
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。
优缺点
优点
避免类的重复加载
保护程序安全,防止核心API被随意篡改
缺点
某些场景下会有局限性
如何打破双亲委派机制
自定义类加载器
自定义类加载器加载一个类需要:继承ClassLoader,重写findClass,如果不想打破双亲委派模型,那么只需要重写findClass;如果想打破双亲委派模型,那么就重写整个loadClass方法,设定自己的类加载逻辑
JVM调优
jps:JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。
jstack:Java虚拟机自带的命令行工具,主要用于生成线程的堆栈信息,用于诊断死锁及线程阻塞等问题。
jmap:Java虚拟机自带的命令行工具,可以生成JVM中堆内存的Dump文件,用于分析堆内存的使用情况。排查内存泄漏等问题
stat:Java虚拟机自带的命令行工具,主要用来监控JVM中的类加载、GC、线程等信息。
Arthas:Arthas 是Alibaba开源的Java诊断工具,非常强大,非常推荐
STW
STW是什么
在堆内存中,老年代区间内存满的时候,JVM会由字节码执行引擎开启full GC垃圾回收线程,对整个堆内存使用可达性分析算法进行一次垃圾回收。这个时候会停掉主线程,专心进行GC。程序会卡顿一下,不过这个时间是非常非常短的,JVM调优主要就是通过各种手段来减少STW,让用户能有更好的体验。
为什么要有STW
如果主线程和GC线程同时进行,这个对象前一秒是刚被GC标记为非垃圾对象,下一秒使用完了变成垃圾对象了。这样GC很难结束,让GC对堆再重来一次还是会出现这样的问题,不如让主线程停掉,让GC线程专心工作。GC的时间也是非常非常短的。
偏向锁
轻量级锁
重量级锁
0 条评论
下一页