大纲
- 虚拟机栈和本地方法栈的区别?
- 垃圾回收算法知道哪些,CMS 说一下,并发标记阶段处理速度慢的原因可能是什么。怎么进行优化?
- java虚拟机有哪些分区?
- 请简单描述一下类的加载过程?
- 还有CMS采用哪种回收算法?使用CMS怎样解决内存碎片的问题呢?
- 如何判断对象已死?
- 介绍一下引用?
- 发生Young GC的时候需要扫描老年代的对象吗?
虚拟机栈和本地方法栈的区别?
简单的来说,虚拟机栈是为虚拟机执行字节码指令(java方法)服务,而本地方法栈是为了虚拟机执行本地native方法而服务。
垃圾回收算法知道哪些,CMS 说一下,并发标记阶段处理速度慢的原因可能是什么。怎么进行优化?
垃圾回收算法分别有:标记-清除法、标记-复制法和标记-整理法
- 标记-清除法:第一步先标记出所有要回收的对象(存活的对象),第二步回收所有被标记(未被标记)的对象。
缺点:
- 会随着对象的增多而导致标记和清除所用的时间也线性增多。
- 会在内存中产生大量的不连续的内存碎片
- 标记-复制法:此算法解决了标记-清除法在有大量可回收对象的情况下效率低的问题。它把内存一分为二,然后平时先使用其中的一半,在进行回收的时候,把存活的对象移动到另一半还没使用的内存中,然后直接清理掉之前那半内存(要回收对象存在的内存区域)。
缺点:显而易见,把可用内存缩小为原来的一半,存在较大的空间浪费。 - 标记-整理法:先把所有存活对象移动到一端,然后把边界以外的内存都清理掉。
缺点:对于存活对象很多的老年代,会导致过多的对象移动。
CMS收集器是一种追求最短回收停顿时间的收集器。它的工作大致分为下面四个步骤
- 初始标记:单独的垃圾回收线程标记所有的GC Roots能直接关联的对象,,速度很快
- 并发标记:遍历整个对象图进行标记,垃圾回收线程与用户线程并发,不需要停顿用户线程
- 重新标记:并发标记是用户线程可能导致标记产生变动,所以需要重新调整
- 并发清理:回收垃圾,不停顿用户线程
并发阶段处理慢的可能原有是,因为用户线程和垃圾回收线程并发执行,有可能存在线程上下文切换等带来的性能消耗,如果要提高垃圾回收的速度,可能要停掉部分的用户线程。
java虚拟机有哪些分区?
如下图
请简单描述一下类的加载过程?
- 加载:在加载阶段,JVM通过一个类的全限定名称来获取二进制字节流,最后在内存里生成一个代表该类的Class对象。加载阶段是程序员最能掌控的一个阶段,因为并没有限定要通过何种途径来获取二进制流,所以我们可以通过自定义的类加载器,通过网络字节流传输等多种途径去获取二进制流。
- **验证:**加载与连接是交叉进行的,比如某些验证字节码文件格式的操作。验证阶段主要是确保Class文件里面的信息符合规范,不会影响到虚拟机自身的安全
- **准备:**这个阶段主要为类变量(静态变量)分配内存并初始化赋值。注意,这些类变量使用的内存在方法取,而方法区只是一个逻辑上的说法,在jdk7的方法区表现为永久代,而在jdk8使用了元空间的概念,所以类变量是随着Class对象放在堆里面。
public static int a = 199;
//在准备阶段,赋给a的初始值是0而不是199
//只有当存放在<clinit>()的putstatic指令被执行后才会被赋值为199,而<clinit>类构造方法被执行是在初始化阶段才被执行
public static final int b = 199;
//而类变量b是由final修饰的,所以它的值199被存放于ConstantValue属性(被其对应的字段表引用)中,而ConstantValue会指向它对应的常量池之中的字面量,所以在准备阶段就直接对b赋值
- 解析:解析阶段就是把符号引用转换为直接引用,符号就是用任意的字面量来描述引用的目标,而直接引用是可以直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。直接引用直接对应着虚拟机的真实内存布局。符号引用就是class文件常量池中的CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等常量。
- 初始化:初始化阶段就是执行类构造器()的过程,而()是javac编译是自动生成的,它搜集了类中对类变量赋值的动作和static{}静态代码块的语句,如果一个普通的类没有静态变量赋值动作也没有静态代码块,()是不存在的,java虚拟机会保证在子类的()方法执行前先执行父类的()方法,而无需显式调用。
还有CMS采用哪种回收算法?使用CMS怎样解决内存碎片的问题呢?
CMS在并发清理阶段是基于标记-清除算法,如果空间导致空间碎片过多,导致无法找到足够大的内存来分配对象,就会提前触发Full GC (同时清理年轻代和永久代)进行碎片整理。
如何判断对象已死?
- 引用计数法:如果一个对象被引用,那么它的引用计数器则加一,取消引用则减一;若一个对象的引用计数器值为0,则对象已死,但引用计数器无法解决对象相互相互引用的问题,因为这样导致它们的引用计数器都不为1,所以不能进行垃圾回收(垃圾引用垃圾居然变成不是垃圾)
- 可达性分析:主流的java虚拟机都通过这种方法进行分析。这个方法主要通过一系列的GC Root,而GC Root分别引用了不同对象,而那些对象又有可能引用了别的对象,从而这些对象一起形成了图,如果某一对象不在这个图里,换句话说这个对象对于GC Root来说是不可达的,就可以判定对象是垃圾。固定的GC Root可以为一下几种:
- 虚拟机栈引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- java虚拟机内部的引用,例如Class对象、异常对象等
- 被同步锁(synchronized)持有的对象
介绍一下引用?
- 强引用
类似于 Object obj = new Object()的引用,java虚拟机永远不会回收被强引用的对象。 - 软引用
被软引用的对象会在内存溢出之前,被回收。
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str);
- 弱引用
被弱引用的对象会在垃圾回收发生时被回收,而不管内存是否足够。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;
System.gc();
System.out.println(weakReference.get());
//null
- 虚引用
虚引用的作用仅仅是对象被回收时收到一个系统的通知,不能通过虚引用来获取实例。虚引用一定要配合引用队列使用,在虚引用的对象被回收时,虚拟机会将引用放入引用队列,这相当于一种系统通知。
String str2 = new String("哈哈哈哈");
ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>(str2,stringReferenceQueue);
str2 = null;
System.gc();
//强制执行所有失去引用的对象的finalize()方法
System.runFinalization();
System.out.println(stringReferenceQueue.poll() == phantomReference);
//true
发生Young GC的时候需要扫描老年代的对象吗?
不需要。为了解决存在跨代引用时把整个老年代都加进GC Roots扫描范围,垃圾收集器在新生代建立了名为记忆集的数据结构。这个记忆集通常用卡表的方式去实现,卡表其实是一个字节数组(CARD_TABLE),而每一个元素都对应标识了一个内存块,这个内存块叫做卡页,每一个卡页中储存着多个对象,如果卡页中有对象存在跨代引用,则把卡表数组对应的元素的值标记为1,则在下次扫描中就能轻易得出哪些卡页中包含跨代指针,则把这些对象一并加入到GC Roots中一起扫描。