Java经典面试题
🎄JVM线程私有和共享的区域
JVM线程私有的区域有:虚拟机栈,本地方法栈,程序计数器。
虚拟机栈:主要存储方法,局部变量,运行的数据。
本地方法栈:主要存储本地方法(含有Native关键字的方法)。
程序计数器:存储程序运行位置的字节码行号指示器。
JVM线程共享的区域有:Java堆,元空间
Java堆:存储所有创建的对象,数组等。
元空间:存储虚拟机加载的字节码数据,常量,静态变量,运行时常量池等。
🎇线程上下文切换
线程上下文切换,也就是CPU不再执行当前的线程,而去执行其他的线程。那有哪些原因会导致线程的上下文切换呢?
- 线程的时间片用完
- 垃圾回收(会暂停当前工作的线程,先进行垃圾回收)
- 更高优先级的线程运行
- 线程主动调用了某些方法,如sleep,yeild,wait,join,synchronized,lock等
当发生上下文切换时,操作系统会保存当前线程的状态,恢复另一个线程的状态,此时程序计数器会记住下一条jvm指令的执行地址,同时上文记录,程序计数器是线程私有的。
🍒如何判断对象是否存活
判断对象是否存活有两种方法:引用计数算法和可达性分析算法。
🍸引用计数法
在对象被创建的时候,会在对象头中分配一个空间,即计时器,来保存这个对象被引用的次数。如果这个对象被其他的对象引用,它的引用计数器会+1,如果删除其他对象对这个对象的引用,则它的引用计数会-1,当对象的引用计数为0时,这个对象就会被当成垃圾回收。
优点:
引用计数法实现起来比较简单,判断对象是否存活的效率比较高。
缺点:
无法解决对象之间循环引用的问题,不能检测到环的出现。例如,A和B之间相互引用,此时计数器都会显示为1,此时A和B都无法进行垃圾回收。
🎍可达性分析法
Java虚拟机中的垃圾回收机制都是采用的可达性分析算法来探索存活的对象的。此种方法工作原理是会扫描java堆中的对象,沿着GC Roots对象往下寻找,看看是否能在此引用链中找到该对象,如果找不到的话,证明该对象没用了,表示该对象可以回收。
可达性分析算法最大的优点之一就是解决了对象之间的相互循环依赖的问题,目前和引用计数法比起来没有缺点。
🍖JVM中的垃圾回收算法
对于新生代和老年代的对象,在JVM中会采取不同的垃圾回收算法。年轻代的对象一般都是朝生暮死的,创建之后很快就会被回收,而老年代的对象是需要长期存活的,因此用到的算法大不相同。新生代对应的收集方法为“Minor GC”,老年代对应的收集方法称为“Major GC”,而对于整个堆空间和方法区的回收被称为“Full GC”
🧃标记清除算法
标记清除算法为最基础的垃圾收集算法,即为每个对象都分配一个标记为,这个标记位会记录对象的状态。标记着所要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以标记存活的对象,清理掉未标记的对象。标记清除算法用于老年代的垃圾回收中。
优点: 基于可达性分析算法,实现起来比较简单,后续的算法都是基于这种思想来实现的。
缺点:
影响最大的一点在于,标记清除算法会使内存空间碎片化,即标记并清除垃圾后,会产生很多不连续的内存空间,这将导致较大的对象因为无法找到连续的内存而提前触发一次垃圾回收。如果大部分对象需要回收,就会进行大量的标记和清除操作,存活对象数量多时效率会降低。
🥫复制算法
复制算法被用于新生代的垃圾回收机制中,新生代有三部分,Eden(80%),和两个survivor区(From Survivor 和 To Survivor)。两个Survivor区为容量大小相等的两块内存,每次只使用其中的一块内存,当使用的那块内存用完后,就会将内存中还存活着的对象复制到另一块内存上,然后把使用过的那块内存空间清空。
优点: 实现起来比较简单,效率也比较高,可以保证内存有连续的区域,能够解决标记清除算法导致的内存碎片问题。
缺点:
可分配的内存空间缩小了一半儿,代价比较高,内存空间浪费比较多; 存活的对象比较多的时候使用复制算法将会导致效率降低。
进行标记清除算法时,会导致应用程序挂起(停顿),即stop the world(STW)。
扩展:
90%以上的对象都是朝生暮死的,所以在新生代中,每次为对象分配内存时会使用Eden区和其中的一块Survivor区,当发生垃圾回收时,JVM会将Eden和Survivor中存活的对象都复制到另一块Survivor区域内,之后清理掉Eden区和Survivor区域中的空间。综上所述,建立新对象时,新生代可用内存空间为整个儿新生代容量的90%(80%的Eden区和10%的Survivor区),如果发生了极少部分情况,即多于10%的对象存活下来了,没有被垃圾回收器回收掉,此时JVM会触发空间担保机制,即当Survivor空间不足以容纳一次Minor GC后的存活对象时,就需要依赖老年代进行分配担保。
🥓标记整理算法
标记整理法是对标记清除算法的一个改进。第一个阶段和标记清除算法一样,都是将对象标记为存活和死亡状态,然而在第二阶段,标记清除算法只是将被标记的对象进行清除,标记整理算法会将存活的对象进行整理并且放到另一个端,然后再把所有的对象清除掉。
标记整理算法用于老年代的回收机制中。
优点: 不会像垃圾清除算法那样产生不连续的内存碎片空间
不会像复制算法那样划分两个区域,提高了空间的利用率
缺点:
效率上肯定更慢一些,因为多了一步整理的操作过程。
🍨如何判断变量是否线程安全
对于成员变量和静态变量
- 如果它们没有被共享,则它们是线程安全的;
- 如果它们被共享了,根据它们的状态是否能够改变,又会分两种情况:如果只有读操作,则它们是线程安全的;如果有读写操作,则这段代码是临界区,是需要考虑线程安全的。
对于局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象则未必线程安全。如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,则是需要考虑线程安全的。
🍻最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
思路:该题让求出最长的递增子序列,因此至少需要一次遍历,考虑到代码进行到每一步的状态才可以,所以动态规划法解决此题比较容易。
代码+详解:
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length==0){
return 0; //长度为0直接返回0
}
int [] dp=new int[nums.length];
dp[0]=1; //初始化数组,也可以调用库函数Arrays.fill(dp,1),但是效率会慢些
int result=1;//初始化结果
for(int i=1;i<nums.length;i++){
dp[i]=1;//初始化数组
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i]=Math.max(dp[i],dp[j]+1);//循环更新dp[i]的最大值
}
}
result=Math.max(result,dp[i]);
}
return result;
}
}
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起讨论🍻
希望能和诸佬们一起努力,今后进入到心仪的公司
再次感谢各位小伙伴儿们的支持🤞
- 点赞
- 收藏
- 关注作者
评论(0)