Java面试指导
我们是做就业服务的工作室,没有任何培训机构性质!!
主做Java、python、c++,前端vue,react等,
全国各地,简历包装,投递邀约,视频面试,技术面试包通过,离职背调等,
通过正常上班,不拿offer不收费,
不要浪费投递简历的机会和面试机会,
如果已经在职的话,并且不满意目前薪资也可以联系我们。关注公众号回复“就业”即可。

Java面试指导
我们是做就业服务的工作室,没有任何培训机构性质!!
主做Java、python、c++,前端vue,react等,
全国各地,简历包装,投递邀约,视频面试,技术面试包通过,离职背调等,
通过正常上班,不拿offer不收费,
不要浪费投递简历的机会和面试机会,
如果已经在职的话,并且不满意目前薪资也可以联系我们。关注公众号回复“就业”即可。

Java基础
面向对象
什么是⾯向对象?对⽐⾯向过程,是两种不同的处理问题的⻆度,⾯向过程更注重事情的每⼀个步骤及 顺序,⾯向对象更注重事情有哪些参与者(对象)、及各⾃需要什么,⽐如洗⾐机洗⾐服:
⾯向过程:会将任务拆解成⼀系列的步骤:打开洗⾐机—–>放⾐服—–>放洗⾐粉—–>清 洗—–>烘⼲
⾯向对象:会拆出⼈和洗⾐机两个对象:
a. ⼈:打开洗⾐机 放⾐服 放洗⾐粉
b. 洗⾐机:清洗 烘⼲
从以上例⼦能看出,⾯向过程⽐较直接⾼效,⽽⾯向对象更易于复⽤、扩展和维护。
封装:封装的意义,在于明确标识出允许外部使⽤的所有成员函数和数据项,内部细节对外部调⽤透明,外部调⽤⽆需修改或者关⼼内部实现
继承:继承基类的⽅法,并做出⾃⼰的改变和/或扩展,⼦类共性的⽅法或者属性直接使⽤⽗类的,⽽不 需要⾃⼰再定义,只需扩展⾃⼰个性化的
多态:基于对象所属类的不同,外部对同⼀个⽅法的调⽤,实际执⾏的逻辑不同
JDK、JRE、JVM之间的区别
JDK:Java Develpment Kit java 开发⼯具
JRE:Java Runtime Environment java运⾏时环境
JVM:java Virtual Machine java 虚拟机
==和equals⽅法之前的区别
==:对⽐的是栈中的值,基本数据类型是变量值,引⽤类型是堆中内存对象的地址
equals:object中默认也是采⽤==⽐较,通常会重写
Object
1 | public boolean equals(Object obj) { |
String
1 | public boolean equals(Object anObject) { |
上述代码可以看出,String类中被复写的equals()⽅法其实是⽐较两个字符串的内容。
1 | public class StringDemo { |
hashCode()与equals()之间的关系
HashCode介绍:hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
以“HashSet如何检查重复”为例子来说明为什么要有hashCode
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的nashcode值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode(0的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
final关键字的作⽤是什么?
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类覆盖,但是可以重载
图灵学院周瑜
修饰变量:表示变量一旦被赋值就不可以更改它的值。
修饰成员变量:
- 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
修饰局部变量:
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,
即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码
中对final变量赋初值(仅一次)
1 | public class FinalVar { |
修饰基本类型数据和引用类型数据:
- 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
- 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
1 | public class FinalReferenceTest{ |
为什么局部内部类和匿名内部类只能访问局部final变量?
编译之后会⽣成两个class⽂件,Test.class Test1.class
1 | public class Test |
首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着
方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有
没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解
决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以
访问它,实际访问的是局部变量的”copy”。这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修
改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量
和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
String、StringBuffer、StringBuilder的区别
- String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
- StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下String Builders效率会更高
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
接⼝和抽象类的区别
- 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
- 抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是is a的关系,比如:BMW is a Car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系。比如:Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也
是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功
能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计
阶段会降低难度
List和Set的区别
- List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
- Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素
ArrayList和LinkedList区别
- 首先,他们的底层数据结构不同,ArrayList)底层是基于数组实现的,LinkedList底层是基于链表实现的
- 由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
- 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使⽤
HashMap和HashTable有什么区别?其底层实现是什么?
区别:
- HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
- HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现,jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部
类Node节点存在
- 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
- 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
- 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
- key为null,存在下标0的位置
谈谈ConcurrentHashMap的扩容机制
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相对于一个小型的HashMap
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
- 先生成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1.8版本的ConcurrentHashMap不再基于Segment实现
当某个线程进行put时,如果发现ConcurrentHashMap.正在进行扩容那么该线程一起进行扩容
如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
ConcurrentHashMap是支持多个线程同时扩容的
扩容之前也先生成一个新的数组
在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化(底层)?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
- 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入ky和vaue时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
- 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源
说⼀下HashMap的Put⽅法
先说HashMap的Put方法的大体流程:
- 根据Ky通过哈希算法与与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry:对象,JDK1.8中是Node对象)并放入该位置
- 如果数组下标位置元素不为空,则要分情况讨论
a.如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
b.如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
i. 如果是红黑树Node,则将key和vaue封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
ii. 如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链 表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表 的节点个数,如果大于等于8,那么则会将该链表转成红黑树
iii. 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法
泛型中extends和super的区别
- extends T>表示包括T在内的任何T的子类
- <?super T>表示包括T在内的任何T的父类
深拷⻉和浅拷⻉
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实
例对象的引用。
- 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
- 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
HashMap的扩容机制原理
1.7版本
- 先生成新数组
- 遍历老数组中的每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
- 将元素添加到新数组中去
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
- 先生成新数组
- 遍历老数组中的每个位置上的链表或红黑树
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
a.统计每个下标位置的元素个数
b.如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
c.如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
CopyOnWriteArrayList的底层原理是怎样的
- 首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
- 并且,写操作会加锁,防止出现并发写入丢失数据的问题
- 写操作结束之后会把原数组指向新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
什么是字节码?采⽤字节码的好处是什么?
Java中的编译器和解释器: Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码—>编译器—->jvm可执行的Java字节码(即虚拟指令)—->jvm—->jvm中解释器——->机器可执行的二进制机器码——>程序运行。
采用字节码的好处: Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java中的异常体系是怎样的
- Java中的所有异常都来自顶级父类Throwable。
- Throwable下有两个子类Exception和Eror。
- Ero是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
- Exception不会导致程序停止,又分为两个部分RunTime Exception:运行时异常和CheckedException检查异常。
- RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
Java中有哪些类加载器
JDK⾃带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。
- BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%Iib下的jar包和class文件。
- ExtClassLoader是AppClassLoaderl的父类加载器,负责加载%JAVA_HOME%/Iib/ext文件夹下的jar包和class类。
- AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。
说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是BootstrapClassLoader
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会 先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤ BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果 BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到, 那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰ 进⾏加载
GC如何判断对象可以被回收
- 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引⽤计数法,可能会出现A 引⽤了 B,B ⼜引⽤了 A,这时候就算他们都不再使⽤了,但因为相互引 ⽤ 计数器=1 永远⽆法被回收。
GC Roots的对象有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNl(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是⽴即死亡的,对象拥有⼀次⾃我拯救的机会。对象被系统宣告死亡⾄ 少要经历两次标记过程:第⼀次是经过可达性分析发现没有与GC Roots相连接的引⽤链,第⼆次是在 由虚拟机⾃动建⽴的Finalize()队列中判断是否需要执⾏finalize()⽅法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize⽅法,若未覆盖,则直接将其回 收。否则,若对象未执⾏过finalize⽅法,将其放⼊F-Queue队列,由⼀低优先级线程执⾏该队列中对 象的finalize⽅法。执⾏finalize⽅法完毕后,GC会再次判断该对象是否可达,若不可达,则进⾏回收, 否则,对象“复活”
每个对象只能触发⼀次finalize()⽅法
由于finalize()⽅法运⾏代价⾼昂,不确定性⼤,⽆法保证各个对象的调⽤顺序,不推荐⼤家使⽤,建议 遗忘它。
JVM中哪些是线程共享区
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的
你们项⽬如何排查JVM问题
对于还在正常运⾏的系统:
- 可以使用jmap来查看JVM中各个区域的使用情况
- 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
- 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
- 通过各个命令的结果,或者jvisualvm等工具来进行分析
- 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
- 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发⽣了OOM的系统:
- ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件( XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
- 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
- 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
- 然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
⼀个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- 用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。
- JVM要实例化一个对象,首先要在堆当中先创建一个对象。->半初始化状态
- 对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。->多大年龄才会移入老年代?年龄最大15,超过一定年龄后,对象转入老年代。
- 当方法执行结束后,栈中的指针会先移除掉。
- 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。
怎么确定⼀个对象到底是不是垃圾?
- 引⽤计数: 这种⽅式是给堆内存当中的每个对象记录⼀个引⽤个数。引⽤个数为0的就认为是垃 圾。这是早期JDK中使⽤的⽅式。引⽤计数⽆法解决循环引⽤的问题。
- 根可达算法: 这种⽅式是在内存中,从引⽤根对象向下⼀直找引⽤,找不到的对象就是垃圾。
JVM有哪些垃圾回收算法?
- MarkSweep 标记清除算法:这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶 段:直接将垃圾内存回收。这种算法是⽐较简单的,但是有个很严重的问题,就是会产⽣⼤量的内 存碎⽚。
- Copying 拷⻉算法:为了解决标记清除算法的内存碎⽚问题,就产⽣了拷⻉算法。拷⻉算法将内存 分为⼤⼩相等的两半,每次只使⽤其中⼀半。垃圾回收时,将当前这⼀块的存活对象全部拷⻉到另 ⼀半,然后当前这⼀半内存就可以直接清除。这种算法没有内存碎⽚,但是他的问题就在于浪费空 间。⽽且,他的效率跟存货对象的个数有关。
- MarkCompack 标记压缩算法:为了解决拷⻉算法的缺陷,就提出了标记压缩算法。这种算法在标 记阶段跟标记清除算法是⼀样的,但是在完成标记之后,不是直接清理垃圾内存,⽽是将存活对象 往⼀端移动,然后将端边界以外的所有内存直接清除。
这三种算法各有利弊,各⾃有各⾃的适合场景。
什么是STW?
STW: Stop-The-World,是在垃圾回收算法执⾏过程当中,需要将JVM内存冻结的⼀种状态。在STW 状态下,JAVA的所有线程都是停⽌执⾏的-GC线程除外,native⽅法可以执⾏,但是,不能与JVM交 互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
JVM有哪些垃圾回收器?
- 新⽣代收集器:
Serial
ParNew
Parallel Scavenge
- 老年代收集器:
CMS
Serial Old
Parallel Old
- 整堆收集器:
G1
垃圾回收分为哪些阶段
GC分为四个阶段:
- 第一:初始标记标记出GCRoot]直接引用的对象。STW
- 第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。
- 第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个OI区,而只需要遍历第二步标记出来的Region。
- 第四:重新标记:跟CMS中的重新标记过程是差不多的。
- 第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。
什么是三⾊标记?
三⾊标记:是⼀种逻辑上的抽象。将每个内存对象分成三种颜⾊:
- ⿊⾊:表示⾃⼰和成员变量都已经标记完毕。
- 灰⾊:⾃⼰标记完了,但是成员变量还没有完全标记完。
- ⽩⾊:⾃⼰未标记完。
JVM参数有哪些?
JVM参数⼤致可以分为三类:
- 标注指令: -开头,这些是所有的HotSpot都⽀持的参数。可以⽤java -help 打印出来。
- ⾮标准指令: -X开头,这些指令通常是跟特定的HotSpot版本对应的。可以⽤java -X 打印出来。
- 不稳定参数: -XX 开头,这⼀类参数是跟特定HotSpot版本对应的,并且变化⾮常⼤。详细的⽂档 资料⾮常少。在JDK1.8版本下,有⼏个常⽤的不稳定指令:
java -XX:+PrintCommandLineFlags : 查看当前命令的不稳定指令。
java -XX:+PrintFlagsInitial : 查看所有不稳定指令的默认值。
java -XX:+PrintFlagsFinal: 查看所有不稳定指令最终⽣效的实际值。