Java面试 -- JVM篇

1、内存模型

这里写图片描述

堆:Java虚拟机管理内存中最大的一块,线程共享区域。所有对象实例和数组都在堆上分配内存空间。

栈:在Hotspot中虚拟机栈和本地方法栈是在一起的。它是线程私有,每个线程都会创建一个虚拟机栈,生命周期与线程相同。每个方法被执行的时候就会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。一个方法执行的过程对应着一个栈帧的入栈到出栈过程。

方法区:用于存储类信息,常量,静态变量等信息,是线程共享区域。

程序计数器:一块较小的内存空间,作用是当前执行字节码的行号指示器。

2、堆的分区

堆中存储对象实例,是垃圾回收的主要区域。为了方便垃圾回收,将堆区域分为新生代和老年代两个区域。

新生代:大量对象(98%)都是朝生夕死,因此在进行垃圾回收的时候采用复制算法进行垃圾回收,因为只需付出商量存货对象的复制成本就可以完成收集。但是由于新生代大量对象都是非存活状态,按照常规复制算法1:1划分内存会造成大量空间的浪费。因此新生代又可以划分为一块较大的Eden区域和两块较小的Survivor空间,每次使用Eden和其中一块Survivor区域。回收时,将存活的对象一次性拷贝到另一块Survivor空间上,再清理掉用过的Eden和Survivor空间。默认Eden区域:Survivor区域=8:1 也就是每次使用新生代容量的90%,只有10%被浪费。但是当Survivor区域不足 以存储回收后存活的对象时,需要老年代进行空间担保,这些对象直接通过分配担保机制进入老年代。

老年代:判断对象是否死亡,至少进行两次标记过程。如果对象在Eden出生并且经过一次gc后仍然存活,并且能够被Survivor容纳的话,将会被移动到Survivor区域,并将其年龄设置为1,如果它还能熬过下一次gc收集,年龄再+1.默认情况下当年龄到达15后,就会晋升到老年代中。老年代采用标记清除和标记整理算法进行垃圾收集。

3、对象的创建

对象创建方法,对象的内存分配,对象的访问定位。

虚拟机遇到一个new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,需要先执行相应的类加载过程。

对象访问定位符:

1.句柄访问方式:java堆中划分一部分空间作为句柄池,栈reference中存储对象的句柄地址,句柄中包含对象实例数据和各自对象具体地址。即对象实 例数据指针指向堆的实例池对象地址、对象类型数据指针指向方法区的对象类型数据。

优势:reference中存在稳定的句柄地址。对象被移动时只改变句柄中实例数据指针。

2.直接指针访问方式: reference中直接存储对象实例地址,对象实例数据中有一小块区域存储对象类型数据指针,同样指向方法区。

优势:节省了一次指针定位的开销,访问速度更快。

详情见:对象创建,内存分配详解

4、GC的判定方法

GC的判定就是判断这个对象是否存活,包含2个判断方式:引用计数与引用链。

1.引用计数:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1。引用失效时,计数器减1 。当计数器为0时,对象就不可能再被使用。

残留问题:循环引用

2.引用链(可达性分析算法):通过Gc-Root的对象为起点,通过这个节点向下搜索,搜索所走过的路径为引用链。当一个对象到Gc-Root没有任何引用链连接时,证明对象不可用。

可作为Gc-Root的对象:

  • 虚拟机栈中的引用对象。
  • 本地方法栈中引用的对象。
  • 方法区类静态属性引用的对象。
  • 方法区中常量引用的对象。

5、GC收集方法

GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方。

1.标记清除算法:首先标记处所有需要回收的对象,在标记完成后统一进行回收。

缺点:标记的过程效率不高、标记清除之后产生大量不连续的内存碎片,当需要申请大块连续内存空间时,无法找到。

2.复制算法:将内存按容量费为大小相等的两块区域,每次只使用其中的一块,当一块内存用完了,就将还存活的对象复制到另一块内存上面,然后吧使用过的那块内存统一清理掉。

缺点:每次只能使用总内存容量的一半。在对象存活较多的情况下会进行大量复制操作,效率底下。

3.标记整理算法:和标记清除算法一样,先对死亡对象进行标记,然后将存活对象向一端移动,然后直接清理掉边界以外的内存。

4.分代收集算法:根据对象存活周期的不同,将内存划分为新生代和老年代,新生代使用复制算法,老年代使用标记清除或标记整理算法进行垃圾收集。

6、GC收集器

GC收集器有哪些?CMS收集器与G1收集器的特点。

1.Serial:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

特点:CPU利用率最高,停顿时间即用户等待时间比较长。

2.Parallel:采用多线程来通过扫描并压缩

特点:停顿时间短,回收效率高,对吞吐量要求高。

3.CMS收集器:采用“标记-清除”算法实现,使用多线程的算法去扫描堆,对发现未使用的对象进行回收。

4:G1:堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。

特点:支持很大的堆,高吞吐量、支持多CPU和垃圾回收线程、在主线程暂停的情况下,使用并行收集、在主线程运行的情况下,使用并发收集

7、Minor GC与Full GC

Minor GC与Full GC分别在什么时候发生?

Minor GC: 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。 
特点:当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor G。内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。 
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。 
质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

Major GC:清理永久代

Full GC:清理整个堆空间—包括年轻代和永久代

Major GC / Full GC:老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

8、双亲委派模型

JDK 默认提供了几种ClassLoader:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

  1. Bootstrp loader 
    Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。

  2. ExtClassLoader 
    Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrploader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.LauncherExtClassLoader,ExtClassLoader主要加载

  3. AppClassLoaderBootstrploader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是sun.misc.LauncherAppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。

综上所述,它们之间的关系可以通过下图形象的描述:

这里写图片描述

Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 
    每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

说到这里大家可能会想,Java为什么要采用这样的委托机制?

理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”, 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的ClassLoader加载了此类, 那么在JVM中它是不同的类。

明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp 类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。

如何自定义ClassLoader 
Java除了上面所说的默认提供的classloader以外,它还容许应用程序可以自定义classloader,那么要想自定义classloader我们需要通过继承java.lang.ClassLoader来实现,接下来我们就来看看再自定义Classloader的时候,我们需要注意的几个重要的方法:

1.loadClass 方法

loadClass method declare
public Class<?> loadClass(String name)  throws ClassNotFoundException

上面是loadClass方法的原型声明,上面所说的双亲委托机制的实现其实就实在此方法中实现的。下面我们就来看看此方法的代码来看看它到底如何实现双亲委托的。

loadClass method implement
public Class<?> loadClass(String name) throws ClassNotFoundException
 {  
return loadClass(name, false);
}

从上面可以看出loadClass方法调用了loadcClass(name,false)方法,那么接下来我们再来看看另外一个loadClass方法的实现。

Class loadClass(String name, boolean resolve)

protected synchronized Class<?> loadClass(String name, boolean resolve) 
 throws ClassNotFoundException   
 {  // First, check if the class has already been loaded  Class c = findLoadedClass(name);
//检查class是否已经被加载过了  if (c == null)
 {     
 try {      
if (parent != null) {         
 c = parent.loadClass(name, false); //如果没有被加载,且指定了父类加载器,则委托父加载器加载。    
  } else {        
  c = findBootstrapClass0(name);//如果没有父类加载器,则委托bootstrap加载器加载      } 
     } catch (ClassNotFoundException e) {         
 // If still not found, then invoke findClass in order          
// to find the class.         
 c = findClass(name);//如果父类加载没有加载到,则通过自己的findClass来加载。      } 
 } 
 if (resolve) 
{     
 resolveClass(c); 
 }  
return c;
}

9、类加载过程

类的加载分为五个过程:加载、验证、准备、解析、初始化。

详情请参看:类加载的五个过程详解

转载地址:2017最新Java基础知识面试笔试整理–jvm基础篇


阅读更多
个人分类: 面试
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭