JAVA基本知识
JAVA语言层面
面向对象和面向过程有什么区别?
把问题抽象成实体对象去解决问题
把问题抽象成过程方法去解决问题
对象和对象引用有什么区别?
对象是new出来的,放在堆里面
对象引用放在虚拟机栈里面,有直接引用和句柄引用
对象相等和引用相等的区别?
对象相等是比较对象中的内容是否相等,
引用相等是比较对象的内存地址是否相等,直接==,默认equals等同
类的构造方法有什么作用?
进行类的初始化,无参构造方法默认就有,可以不用申明,构造方法不能被重写,但是可以重载。
面向对象的三大基本特征
对象就是实体,具有继承性、封装性、多态性
继承:Java是单继承的
封装:Java封装的基本单位是类,public、private、protected
多态: 同一个接口能用不同的实例进行操作,弥补单继承的不足
JAVA的基本数据类型
boolean 1B byte 1B char 2B
short 2B
int 4B
long 8B
float 4B
double 8B
JAVA方法的重载和重写
重载:一个程序定义多个名称相同的方法,参数不同
重写:相同的方法名和参数,重新实现该方法
接口和抽象类有什么区别?
这两个都不能被实例化,接口是interface, 抽象类是abstract class 接口中的成员变量只能赋初值(public static final),但是抽象类可以,Java是单继承但是可以实现多接口。
引用拷贝、浅拷贝、深拷贝的区别?
引用拷贝不会创建新的对象,浅拷贝会创建新的对象,但是对象中的引用类型会直接引用原对象的,深拷贝会全部拷贝。
Object类有哪些常见方法?
equals hashcode toString wait nootify等
字符串内存区域详解
String a = new String("ab");
String b = new String("ab");
上面这段代码会创建三个对象,首先在方法区的字符串常量池中创建ab对象,然后在堆中创建ab对象,然后在堆中创建引用对象a指向ab
==和默认的equals
==和默认的equals一样,基础数据类型比较的是值,引用类型比较的是引用指向的地址是否一致,即在上述字符串代码中 比较的是 是堆中的字符串还是方法区中的字符串。a==b返回的应该是false 因为第一句会创建三个对象,第二句会创建两个对象,常量池中第一句创建了就不会再创建了当时堆中会继续创建
为什么重写equals必须重写HashCode?
首先hashcode()方法是object类中的,所以每个对象都是hash值,对于两个相等的对象hash值必须相同,当时对于hash值相等的对象却不一定是同一个对象,因为可能会发生hash冲突 比如hashset存值,先计算hash值再计算equals比较大小,所以重写equals 必须重写hashcode
如何重写equals方法?
public class AsyncTaskRequest {
AsyncFlowClientData taskData;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AsyncTaskRequest)) return false;
AsyncTaskRequest that = (AsyncTaskRequest) o;
return Objects.equals(getTaskData(), that.getTaskData());
}
@Override
public int hashCode() {
return Objects.hash(getTaskData());
}
}
String、StringBuilder、StringBuffer的区别?
String用final修饰 且为私有不可变,常量 线程安全。
StringBuilder和StringBuffer都继承AbstractStringBuilder,有一系列的API 比如apend(),indexof()但是StringBuilder线程不安全,StringBuffer线程安全(Sychronized)
在JDK9之前这三个都是CHAR数组,JDK9之后改成Byte数组,因为有两种编码方案,在一种方案夏byte占的字节更少
字符串的+本质上使用的是什么?
多个字符串的+ 实际上调用的是StringBuilder的append()方法,并返回toString()方法
1.String a = new String("str");
2.String b = new String("ing");
3.String c="str"+"ing";
4.String d="string";
5.String e=a+b;
System.out.println(e==c);//false
System.out.println(e==d);//false
System.out.println(c==d);//true
1和2调用new对象 会在常量池和堆中各创建对象,
3和4由于编译器的优化 所以相等,直接在常量池中创建字符串String
调用对象+ 会调用StringBuilder的append 返回一个堆中的String对象
Java中的反射?
对于任何一个运行中的类,能获取类的方法属性,能获取方法的参数等,比如getMethod(),getClass(),比如Spring中的@Value()
FINAL详解
final修饰类,该类不能被继承
final修饰方法,该方法不能被重写
final修饰属性或变量,值不能改变,但是可以一开始不赋值后来赋值,final修饰引用类型,可以改变。
final 赋值的时候, 字节码层面调用putfield指令后其实是加了写屏障的,其次final能一定程度上避免外星方法逃逸,所以更安全。
final作为成员变量的时候,数字比较小的时候存放在方法区常量池中(static也放在方法区中),数字超过常量池的最大值时,放在堆中;而一般成员变量都是放在堆中。
静态方法为什么不能访问非静态变量?
因为静态修饰的方法是属于类的,在类初始化之后就存在了,而一般成员变量是属于对象的,必须实例化后才会存在,而在类初始化 ,静态方法存在的时候,非静态成员变量还没有存在
什么是可变长参数?
...args 可变长参数只能作为最后一个参数,当时前面可以有任意个数的参数
基本数据类型和包装类
基本数据类型如果不被static修饰 会放在栈中,而包装类放在堆中,基本数据类型没有初值会有默认值,当时包装类没有初值会Null异常
基本数据类型变成包装类 叫装箱
包装类变为基本数据类型叫拆箱
对于Byte short Integer long 是有-128到127的缓存的
所以下面的代码不相等
Integer i1=40;//从缓存中装箱
Integer i2=new Integer(40);//在堆中创建
关于时间的函数
//获取当前时间
LocalDateTime now = LocalDateTime.now();
//修改时间
now.withHour(18).withMinute(12).with(DayOfWeek.TUESDAY);
now.plusWeeks(1);
如何解决浮点数精度丢失的问题?
BigDecimal
如何处理比long还大的数字?
BigInteger()
Java集合类
Java容器结构概览
主要包括:List、Map、set 、Queue这三个都是实现了iterable接口
List主要包括:ArrayList 、vector、LinkedList
set(不可重复单列)主要包括:HashSet、LinkedHashSet、TreeSET
Map(不可重复双列)主要包括:HashMap、HashTable、LinkedHashMap、TreeMap
HashMap添加的原理
1. 添加一个值时首先会计算hash值,然后结合数组长度,得到数组下标
2. 根据数组下标去判断是否需要扩容,扩容阈值是(数组长度*装填因子,数组长度默认为16,装填因子默认为0.75),每次扩容两倍扩容,最大数组节点是64
3. 如果k的hash在map中不存在就插入
4. 如果k的hash存在,就比较equals 如果equals为true 说明已经存在更新就行了,如果equals为false,则插入到链表或者红黑树尾节点处(JDK1.7之前是头插法,JDK1.8之后是尾插法)
5. 当节点数量大于等于64 且链表冲突大于等于8 就转为红黑树
HashMap的get原理
1. 计算hash 根据数组长度得到数组下标
2. 然后去遍历链表或者红黑树,在定位成功的情况下,去调用equals定型,实现查找
为什么HashMap容量一定是2^K
因为(n-1)&hash
HashMap多线程可能会造成死循环的问题?
JDK1.7之前 拉链法处理冲突用的是头插发,可能会造成循环死链,JDK1.8之后采用尾插法避免了这个问题,当时多线程操作一般使用ConcurrentHashMap,这个在JDK1.7之前用的是分段锁,在JDK1.8之后主要用Sychronized+CAS实现。
HashMap和HashTable的区别?
后者线程安全,put方法加了sychronized锁
前者初始大小是16,后者初始大小是11,前者扩容是2n,后者扩容是2n+1
前者允许key为null,后者不允许
ArrayList和Array的区别?
Array既可以存储对象也可以存储基本数类型,ArrayList只能存储对象
Array必须申明的时候指定大小,ArrayList可以不用 并且支持动态扩容
Array只是一个数组没有相关操作API,ArrayList实现iteravble接口有相关操作API
Comparable和Comparator的区别?
Comparable是内部的,接口是CompareTo,Clooections.sort()一个参数,调用的时候不传递
Comparator是外部的,接口是Compare,Clooections.sort()两个参数,调用的时候传递
LinkedHashSet
底层是数组+链表,本质也是hashMap结构,只不过改成了双向链表,让hashset拉链法中的链表都串起来了
ArrayDeque和LinkedList的区别
两者都实现了Dequeue接口,当时前者是基于数组+双指针来实现的,后者是基于链表实现的,所以前者可能要扩容,后者不需要扩容,当时前者的查找效率更高,前者不支持NuLL。
PriorityQueue
基于数组实现的堆,实现了优先级的队列,非线程安全
JAVA多线程并发问题
为什么要并发编程?
- 提高多核cpu利用率
- 方便进行业务拆分,提高响应速度,缩短响应时间,优化用户体验
并发编程的优缺点?
- 优点:速度快
- 缺点:死锁、内存泄漏、会有上下文切换问题、线程安全问题。
并发出现问题的根源是什么?
- 上下文线程切换带来的原子性问题: 加锁,使用sychronized 或者基于cas的reetrantlock锁。
- 缓存导致的可见性问题,使用valotile、sychronized、lock。
- 编译优化后带来的有序性问题。使用volatile
并行和并发有什么区别?
并发实际上是一个时间段内的并行。
什么是JMM?
Java machine memory Java内存模型,从抽象的角度定义了线程和主内存之间的抽象关系,线程之间的共享变量存储在主内存中,同时每个线程都有一个私有的本地内存,本地内存中存储了该线程读取修改共享变量的副本,本地内存是JMM中的一个抽象概念,并不真实存在,包括缓存、写缓冲区等。
sychronized的使用场景?
- 修饰自定义对象
- 修饰方法,给调用该方法的对象加锁
- 修饰静态,给当前类加锁
sychronized关键字的原理
sychronized语义层绑定了monitor监视器,同时sychronized是可重入的锁,所以监视器绑定了计数器,当每次获得锁时,计数器加一。
sychronized锁的升级
开始的时候是偏向锁,每个线程获得锁的时候汇记录threadid ,当threadid与线程id不一致时,升级为轻量级锁,轻量级锁通过cas和对象头markword中的字段的形式实现,同时轻量级锁支持自旋循环,当自旋循环超过一定次数后,会升级为重量级锁,重量级锁就是和monitor绑定。
volatile
jmm中有主内存和工作内存,所以会有可见性的问题,volatile及时更新主内存中的共享变量的值,给值加读写屏障,保证主内存共享变量的一致性,但是无法保证原子性;同时volatile可以解决编译优化带来的有序性问题
volatile和atomicnterger的区别
前者不支持原子性,但后者是cas和volatile共同实现,支持原子性;但是volatile和long、double支持原子性,因为64位的原因。
进程和线程有什么区别?
一般一个应用程序有一个进程,但一个进程有多个线程,进程是os资源的基本单位,线程是执行和调度的基本单位。
线程的状态?
os中线程主要是创建、就绪、运行、阻塞、终止,Java中thread类中的枚举定义了 new 、runnable、 termianted 、waiting block、 time_waitting
创建线程的几种方式?
实现runnbale接口、实现callable接口,继承threa类
线程之间如何通信?
共享变量、消息传递(wait notify park unpark)
interrupted和isInterrupted
interrupt中断并不会影响线程的执行,只是修改中断标志位位true ,interrupted返回中断标志位后会清楚,isInterrupted只会返回。
如何理解threadlocal对象?
threadlocal对象底层实际上是map,key是thread对象、value是值,threadlocal为每个线程都创建了一个值的副本,所以每个线程值都是独立的,但是key是弱引用,在新生代回收算法时一定会被回收,但是value是强引用,所以key为null 但是value不为null就会导致内存溢出,threadlocalmap的remove可以解决。
wait、notify和park、unpark的区别?
wait 、notify必须结合sychronized对象锁一起使用,实现等待,而park和unpark不需要。
可重入锁和非可重入锁的区别?
是否可以同一个对象再次获得锁,sychronized是可重入锁,计数器加一;aqs是可重入锁,state加一。
公平锁和非公平锁?
比如sychronized重量级锁里面,就是非公平锁,因为在等待的队列和刚申请的队列可以同时竞争锁资源,公平锁要保证锁获得的顺序
非公平锁比公平锁性能更好:对于非公平锁可以每次直接通过cas来竞争获取锁,但是公平锁需要排在等待队列中,排队过程需要从运行到等待,进程状态的切换需要从用户态到核心态,这个过程耗时。
AQS原理?
AQS即abstract queued sychronized 队列同步器,是用来构建锁和其他同步工具的基础框架,
主要包含一个int型的state和FIFO的双向链表(包含head tail双节点),
方法包括:三个修改state的方法,五个可重写的方法,九个模版方法
三个方法是:对于state来说,主要有getState()、setState()、comparemAndset()方法,当state=0说明未被占有,当state>0 说明被占有,state>1 发生可重入;
五个方法是:是否独占、独占获取锁、共享获取锁、释放独占锁、释放共享锁
九个模版方法:也包括获取释放等,这九个方法也会调用修改的五个方法。
Reentrantlock数据结构?
首先reentrantlock是基于aqs实现的,所以reentrantlock数据结构和aqs一样都是含有state、双向链表(head、tail)、exclusiveThreadOwner(当前独占线程)四部分组成,会重写五个方法,实现三个state方法。
Reentrantlock如何实现公平锁的
Reentrantlock内部有sync、nofairsync、fairsync三部分组成,nofairsync和fadirsync都是基于sync实现的,而sync是基于aqs实现的,所以对于这三个类来说,底层都是state加一个双向链表,不同的是对于默认的非公平锁,直接调用aqs的tryacquire()方法,而对于公平锁,在获取锁之前会判断将当前节点加入等待队列并判断当前节点之前是否还有节点,来实现公平锁。
Reentrantlock/LocK和Sychronized的区别?
lock可以实现公平锁,sychronized不可以
lock可以使线程在获取的时候允许被打断,比如lockInterruptiedly()
lock的trylock方法可以带时间等待,进行自旋等待
lock解锁位置你可以控制,所以锁的范围可以控制,而sychronized锁不需要手动解锁,加锁和解锁由虚拟机控制
Reentrantlock的条件变量是什么?
reentrantlock条件变量实际也是等待队列,是一个由firstwaiter和lastwaiter组成的双向链表,可以由wait和signal控制,唤醒的条件变量在当前锁被独占的情况下,会把天啊加到AQS的等待队列中去。
ReentrantReadWritelock是什么?
ReentrantReadWritelock 同时包含readlock和writelock,读锁就是共享锁可以被多个线程读,写锁是独占锁,只能被一个线程所持有
StampedLock原理
stamedlock上锁的时候会返回时间戳,解锁的时候把时间戳作为参数传递给解锁函数,同时调用时间戳验证程序来判断是否加锁解锁成功,是一种性能更高的乐观锁。
线程获取了读锁后还能获取写锁吗?
在线程持有读锁的情况下,也可能其他线程也持有读锁,所以获取写锁失败,但是当有持有写锁的时候,一定是独占的,所以可以先获得读锁,再判断读锁和写锁是否是同一个线程。同样读锁不可以升级为写锁,但是写锁可以降级为读锁。
ReentrantReadWriteLock底层是如何实现的?
ReentrantReadWriteLock是基于·()和AQS实现的,所以底层数据结构和AQS一样,但是高16位是读锁,低16位是写锁(高读低写)
ConcurrentHashMap和HashTable的区别?
ConcurrentHashMap和HashMap数据结构类似,JDK1.7之前是数组+链表,在jdk1.8之后是数组+链表转红黑树,而HashTable用的是数组+链表(拉链法控制冲突)
1.7之前ConcurrentHashMap是数组+链表的形式,采用分段锁,每次只会锁部分数据,访问不同数据段的数据,不会有并发问题,底层数据结构是segment+hashEntry 并发规模是取决于segement个数,最大是16。
1.8之后ConcurrentHashMap是数组节点+链表转红黑树的形式(数组节点大于等于64,链表大于等于8),采用sychronized+cas方式实现的,只有在当前链表节点和红黑树节点处采用sychronized重量级锁,其他采用cas,并发度是节点个数。
HashTable采用数组+链表的形式,整个hashtable区域使用sychronized 修饰put一个锁。
ConcurrentHashMap和中的put方法
如果未初始化,初始化node节点为16
如果超过当前容量,则当前线程加入到扩容过程中
如果当前桶为空,则直接插入
如果当前桶不为空,添加segement锁,往链表或者红黑树中添加元素
ConcurrentHashMap的get方法
get不会有并发性问题,因为并发读容易出问题的是可见性的问题,而get中的共享变量都加了voaltile
ConcurrentHashMap的的key和value是否可以为null
ConcurrentHashMap的key和value不可以为null ,避免出现二义性的问题,当时HashMap的key和value可以为null,因为在HahsMap中可以通过cotainsKey来判断。
简单介绍一下BlockingQueue?
BlockingQueue 是支持两个附加操作(插入和移除)的队列,如果队列满了就阻塞插入,如果队列为空,就等待队列为非空,底层采用condition实现生产者消费者的模式
你了解的阻塞队列有哪些?
单数组有界:ArrayListBlockingQueue
单链表有界无界:LinkedBlockingQueue(可以指定初始大小,也可以不指定初始大小)LinkedTransferQueue(可以持续生产,但会内存溢出)
双链表有界无界:LinkedBlockingDequeue
优先级升序无界:priotyBlockingQueue
优先级延迟升序无界:DelayBlockingQueue
CAS是什么?
CAS是基于CPU指令实现的,compareandset 分为三步骤:读取内存,比较内存,设置内存
使用CAS的地方?
自旋锁的实现,Java中的原子类(比如atomicInteger基于cas和volatile),ConcurrentHashMap1.8之后基于sychronized和cas来实现
CAS会有哪些问题?
ABA问题: 原来是A,变成了B,又变成了A,会以为没有发生变化
CAS是实现的原子性,只能保证单个变量的原子性,不能保证多个变量的原子性
一般是将CAS放到循环中,在竞争激烈的情况下,CPU利用率会变高
如何解决ABA问题?
通过乐观锁方式来实现,加入版本号,比如AtomicStampedReference,既比较引用也比较标志
等待多个线程执行完之后的的并发工具类
CountDownLatch 只能用一次, 倒数计数器,等待多个线程执行完成之后一个线程执行,clatch.countdown(),偏向于任务数,计数器为01的时候就不能使用了。
CyclicBarrier 也是倒数计时器,clb.await();当时可以循环使用,通过rest()清零,偏向于线程数。
Semaphore
信号量,不同于操作系统中的信号量,Java中的信号量不表示共享资源个数,用来限制线程并发个数的,也是基于abstract queued synchronizeor来实现的,使用head tail来维护AQS双向链表,用currentOwneThread来记录当前线程,用state来记录可用线程数,当state>0时调用cas方法执行,当state<0时,进入阻塞队列,不调用cas方法。
常见的API包括:semaphore=new Semaphore() ; semaphore.acquire() ; semaphore.release()
Exchange()
用来两个线程之间传递参数,入参是传给对方的参数,出参是对方传过来的参数。
为什么要使用线程池?
可以进行线程管理,降低资源消耗,利用已有线程,提高相应速度。
线程池的核心参数有哪些?
主要是构造方法里面的那些参数 :
核心线程池大小:corePoolSize
最大线程数:maximumPoolSize
空闲线程保活时间:keepAliveTime
保活策略:handler
时间单位:unit
创建线程的工程类:ThreadFactory
阻塞队列:WorkQueue
线程池有哪些拒绝策略?
除非线程池有空位或者被停止,不然使用执行自己的线程去执行(和线程里的没关系),callerrunpolicy
静默提交,不采用任何策略:discardPolicy
抛弃存在时间最短的任务:discardOldestPolicy
直接抛异常:Abortpolcy
自定义拒绝策略:实现RejectedExeutionHandler
有哪些线程池?
newFixedThreadPool(): 定长线程,阻塞队列无界,阻塞队列可能会造成内存溢出
newScheduledThreadPool()定长线程,阻塞队列无界,可以执行定时任务,类似于Java中的timer
newSingleThreadPool()单线程的线程池,并发度低,可以使用FIFO,LTFO等
newCachedThreadPool():可缓存的线程,当线程池超过使用需要的时候,就会动态回收,但是无限增长的线程数可能会造成堆的内处理溢出
Executor和Executors的区别?
Executors是执行线程的工具类
Executor是执行线程的接口,ExecutorService实现了Executor接口并且进行了扩展
线程池有哪些状态?
running
shutdown:不会接受新任务,但是会处理等待队列中的任务
stop:三不:不再接受新任务,不再处理等待队列中的任务,中断当前任务
tidying:线程数为0,即将进行终止
Terminated:终止状态
线程池的execute和submit有什么区别?
两个都是开启线程池中的任务
execute()只能执行runable任务,但是submit()可以执行callable()任务并返回future对象,从而进行异常处理
线程池的执行步骤?
判断核心线程有没有满,没有满就创建,满了就去判断工作队列有没有满,没有满加入工作队列,工作队列满了就去判断是否所有的线程都处于工作状态,或者是否有应急线程,没有就调用拒绝策略。
如何合理分配线程池大小?
等待时间高就要多线程,cpu计算时间高就可以少一点线程,所以大量CPU的线程池设计的和CPU核心数差不多就行,大量IO计算的就多分配,实际还需要根据监控情况来
线程池如何动态地去修改?
提供了一些setter方法 可以修改线程池的线程大小,拒绝策略,空闲线程时间等,也可以放到统一的配置中心中去,比如nacos中的bootstrap.yml配置文件。
JAVA Virtual Memory(JAVA虚拟机问题)
数据区是什么?
虚拟机在执行Java程序的时候,会将内存区域划分为若干数据区,其中主要包括5个,线程私有的数据区包括:程序计数器、本地方法栈、虚拟机栈,线程公有的包括:方法区和堆。
程序计数器是什么?
程序计数器是一片较小的内存空间,可以看作当前线程执行字节码的行号指示器,通过程序计数器来跳转到下一条需要执行的指令位置。
虚拟机栈是什么?
每个线程创建的时候都会开辟一个虚拟机栈空间,虚拟机栈和线程的生命周期是相同的,虚拟机调用方法的时候就会生成一个栈帧,栈帧中包括:局部变量表、动态链接、操作栈、出口等信息,方法的执行到完成就是入栈和出栈的过程,当申请栈空间不足时会抛出栈空间异常,当栈空间可扩展但内存不足时会抛出内存异常。
本地方法栈是什么?
本地方法栈和虚拟机栈类似,不过本地方法是调用本地方法时才会用到,调用本地方法时,虚拟机栈保持不变,动态链接并调用本地方法。HotSpot虚拟机不区分本地方法栈和虚拟机栈。
堆的作用是什么?
Java中几乎所有的对象实例都放在堆中,堆在逻辑上是连续的,在物理存储上可以不连续,可以通过xms设置堆大小。
方法区的作用是什么?
方法区主要存储被虚拟机加载的类信息、静态变量、常量、编译时产生的缓存数据等信息,hotspot虚拟机在jdk6之前还会将方法区会有方法区的永久代,在jdk8之后永久代变成了本地内存中的元空间。
运行时常量池是什么?
运行时常量池是方法区的一部分,所以首先它出错的时候会报内存异常,其次它主要用于存储编译产生的字面常量,符号引用以及符号引用翻译后的直接引用等,不同于字节码的文件常量池,它具有动态性既可以编译时进行存储,也可以运行时进行存储,,比如String的intern。
直接内存是什么?
直接内存是基于通道和缓存的IO,可以通过本地方法来分配堆外内存,在堆中通过directByteBuffer来引用使用,避免了数据在Java堆和Native堆内存来回复制,提高性能。
内存溢出和内存泄露的区别
- 内存溢出指申请内存时内存空间不足导致的异常。
- 内存泄露指的是已经申请内存,却无法释放内存导致的异常。
栈溢出的原因是什么?
- 当申请的栈深度大于栈空间时,会抛出StackOverFlow异常。
- 如果虚拟机栈支动态扩容,那么可能会报OutOfMemory异常,但是HotSpot虚拟机不支持动态扩展,所以只会在申请时报这个异常,运行时不会。
运行时常量池溢出的原因是什么?
String的intern方法就可能导致运行时常量池溢出,因为String的intern方法是本地方法,返回String字符串的对象引用,如果运行时常量池里没有 就新建并返回,在jdk6之前是放在运行时常量池的永久代里面的,如死循环就会溢出,jdk7之后字符串放在堆里面(jdk7之后intern方法也会指向堆中的实例),所以不会有这个问题。
方法区溢出的原因是什么?
方法区主要存储被虚拟机加载的类信息、静态变量、常量、编译时产生的缓存数据等信息。所以只要不断产生大量的类,方法去就会溢出,比如反射或者CGLib运行时会产生大量的类,像spring框架,增强的类越多就需要越大的方法区区保证产生的新类能加载进内存。
Java对象的引用方式有哪些?
java的方法栈主要通过reference引用堆里面的对象,一般具体引用形式由虚拟机决定,主要包括直接指针和句柄两种方式,直接指针也是hotspot虚拟机的主要方式,直接指针:只需一次定位,速度快;句柄实现:会在堆里面开辟句柄池句柄存放了类地址信息和实例数据,优点是reference指针修改少稳定,gc回收的时候只需要改变句柄里面的实例数据指针。
Java的引用有哪些类型?
按照强度主要分为:强引用、软引用、弱引用、虚引用。
强引用就是常见的new对象,内存回收即使崩溃也不会回收。
软引用内存溢出前就会回收掉。
弱引用只能撑到下次新生代回收之前。
虚引用只要回收对象就会被放到引用队列中。
Java引用如何判断为是否是垃圾?
- 计数器,引用计数器为0就是垃圾,但一般不使用,因为无法解决循环引用的问题。
- 进行可达性分析,从GCroots开始到对象之间是否有引用链,如果没有引用链,就是垃圾。
哪些可以作为GCRoots的对象?
- 虚拟机栈中引用的对象,比如局部变量。
- 方法区的常量和静态引用的对象,比如字符串常量池
- 本地方法栈中native方法引用的对象
- 虚拟机的内部引用,比如class对象
- 反应虚拟机内部情况的对象,比如本地代码缓存等。
- 被同步锁synchronized持有的对象
有哪些GC垃圾回收算法?
-
标记清除
先进行可达性分析,从GCROOTS去找没有引用链的对象,将它标记,并清除
效率低、会产生大量内存碎片。
-
标记复制
主要用于新生代(少量对象存活),将内存分为两半,每次只有一半,当一半使用完,就将非垃圾复制到另一半,再将已使用的清理掉
一次只能用一般内存
hotspot将新生代分为较大的eden和较小的两块survivor,每次将已用的一块survivor和eden复制到未使用的survivor上,每次可使用的新生代空间为90%左右。
-
标记整理
主要用于老年代(大量对象存活),标记后,整理到一起,然后清除垃圾。
有哪些垃圾回收器?
- 基于单线程、标记复制的serial回收器,主要用于新生代。
- 多线程版本的parnew回收器,主要用于新生代,可以和cms联合使用
- 和parnew类似的多线程版本的吞吐量优先的parallel scavenge 回收器,复制算法,主要用于新生代。
- serial old 标记整理,老年代,单线程
- parallel old 标记整理,老年代,多线程。
- cms基于标记清除算法:初始标记 cgroots对象,并发标记垃圾,重新标记垃圾,并发清除。(初始标记和重新标记需要stop the world)
- g1 基于局部设计思路,不在以代为回收标准,垃圾最多的区域作为划分标准,分为:初始标记、并发标记、最终标记、筛选回收(对每个region进行用户期望排序,制定回收计划)
Java中的内存分配策略和计划
- 对象优先分配在eden中
- 大对象和长期存活的对象进入老年代
- 动态判定对象年龄
- 老年代空间要大于新生代空间
JAVA编译和执行过程
.java文件,编译成字节码(.class)文件,字节码文件装入内存。
.class文件 由JVM虚拟机执行引擎来完成的,JVM负责将.class文件转为目标机器码,并解释执行。
源码编译阶段
类加载阶段:加载classLoader及其子类来完成
类执行阶段:JVM是基于堆栈的虚拟机
什么是类加载?
JVM把描述类的信息从class文件加载进内存,并进行验证,准备,解析,初始化的过程叫类加载。验证、准备、解析又被称为连接,Java的加在,连接,初始化都是在运行时完成的,并且加载、验证、准备、初始化的顺序是不变的。
类加载的过程?
-
加载:
- 将全限定类名转为二进制字节流
- 将字节流转中的静态数据结构转为方法区中运行时的数据结构
- 在内存中生成class实例
-
连接
- 验证:文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备:类静态变量初始化
- 解析:将常量池中符号引用变成直接引用
-
初始化:执行类中编写的代码
Java虚拟机中有哪些类加载器?
主要包括:启动类加载器、平台类加载器,应用类加载器
什么是双亲委派加载模型?
一个类收到启动加载请求后,首先它自己不会加载这个类,而是把加载请求传递给父加载器,所以最后所有的加载请求都会传递给最上层的启动类加载器优先加载,只有父加载器无法完成加载请求时,子加载器才会进行加载。
如何判断类的唯一性?
在Java虚拟机中,由类加载器和类文件两个共同确定类的唯一性。