1. 内存模型以及分区,需要详细到每个区放什么

JVM分为堆区和栈区,还有方法区,初始化的对象放在堆中,引用放在栈中,class类信息、常量池

(static常量和static变量)等放在方法区。

  • 方法区:主要是存储类信息,常量池,编译后的代码(字节码)等数据

  • 堆:初始化的对象,成员变量(非static的变量),所有的对象的实例和数组都要在堆上分配

  • 栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出

口等信息,局部变量表存放的是八大基础类型加上一个应用类型,所以还是一个指向地址的指针

  • 本地方法栈:主要为native方法服务(java调用非java代码的接口)

  • 程序计数器:记录当前程序执行的行号

2. 堆里面的分区:Eden,survivor(from+to),老年代,各自的特点

堆里面分为新生代和老年代(java1.8取消了永久代,采用了metaspace(元空间)),新生代包括

Eden+survivor区。survivor区分为from区和to区,使用2个Survivor区,始终保持有一个空的Survivor区,

可以避免内存碎片化。。Eden : from : to = 8 : 1 : 1。

当Eden区和from区满时,触发minor gc,先将存活的对象复制到to区,然后清空Eden区和from区,再

颠倒 From Survivor 和 To Survivor 的逻辑关系:from变to,to变from。重复此过程。

JVM会判断之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,若大于则进行full GC

(即回收所有区域),若小于,则还需要查看一个参数HandlePromotionFailure,即是否允许担保失败,

因为实际上进入老年代的对象大小在GC前是未知的,这也是为什么采用之前晋升的平均值来进行判断担

保,也就是说只是一种预测,并不能代表真实就是有这么多对象晋升,所以若不允许担保失败,即保守的

人为一定会有超过剩余老年代区域的对象存入,则还是进行Full GC,否则,进行Minor GC。

附:

  1. 对象在to区每熬过一次GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15),就会晋升到老

年代。XX:MaxTenuringThreshold=数字 参数可以设置对象在经过多少次GC后会被放入老年代

  1. 大对象直接进入老年代。 XX:PretenureSizeThreshold参数可以设置多大的对象可以直接进入老年代内存

区域

  1. 动态年龄。就是在survivor区有根据单个对象独占百分之50以上内存且年龄最大(不一定达到默认15

岁)就移动到老年代

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

  • 对象创建方法

进行类加载检查。当遇到一个new指令,首先检查这个指令的参数是否能在常量池中定位到一个

类的符号引用,并且检查这个符号引用代表的类是否已被加载、连接和初始化过。如果没有,那

必须先执行相应的类的加载过程。

  • 对象内存分配

有两种常见的分配方式,一是指针碰撞,二是空闲列表,分别针对连续分配内存和不连续的,有

空隙的,取决于虚拟机是否会压缩整理。内存分配的大小是在类加载完成之后就已经确定的,但

是分配的时候修改指针的指向位置应该是线程安全的(栈上的Reference),第一种方式就保证原

子性;第二种是给每个线程分配自己的一小块内存,成为本地线程分配缓冲(TLAB),哪个线程要

分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。

  • 对象内存布局:

    • 对象头:对象头包含两个部分的信息,第一部分是对象自身的运行时数据,如哈希码、GC分代年

龄、持有的锁等等;第二部分是类型指针,指向它的类元数据的指针,通过这个虚拟机来确定这

个对象是哪个类的实例。

  • 实例数据:对象真正存储的数据,就是程序代码中定义的字段内容。

  • 对齐填充:用于使对象的开头必须是8字节的整数倍,无特殊意义。

  • 对象访问定位:

对于这句代码:

Object objectRef = new Object();

Object objectRef 这部分将会反映到Java栈的本地变量中,作为一个reference类型数据出现。而

“new Object()”这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化

内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定。另外,

在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地

址信息,这些数据类型存储在方法区中。

有两种基本的定位方式:

第一:句柄访问(间接)

在Java堆中划分一块内存作为句柄池(即一个句柄列表),reference中存储的就是对象在句柄池

中的地址,得到了句柄池的地址就可以知道对象的实例数据和类型数据的位置。

第二:直接指针访问(直接)

reference中存储的直接就是对象的实例数据的地址,而实例数据中自己有一个指针存储对象类型

数据的地址(方法区中),不需要reference来存储。

4. GC的两种判定方法

  • 引用计数法:

在JDK1.2之前使用。对象再被创建时,对象头里会存储引用计数器,对象被引用,计数器+1;引

用失效,计数器 -1;GC时会回收计数器为0的对象,无法解决对象互相循环引用。

  • 引用链法:

通过一种GC ROOT的对象(虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属

性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即一般说的Native方法)引用的对

象)来判断,如果有一条链能够到达GC ROOT就说明,对象还在被引用,不能到达GC ROOT就说

明对象已经不再被引用,可以回收。

5. SafePoint 是什么

safepoint就是一个安全点,所有的线程执行到安全点的时候就会去检查是否需要执行safepoint操作,

如果需要执行,那么所有的线程都将会等待,直到所有的线程进入safepoint。

然后JVM执行相应的操作之后,所有的线程再恢复执行。

safepiont出现的位置主要在:

  1. 循环的末尾(防止大循环的时候一直不进入safepoint,而其它线程在等待它进入safepoint)

  2. 方法返回前

  3. 调用方法的call之后

  4. 抛出异常的位置

6. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?

  • 标记清除:

原理:分为标记和清除两个阶段:首先标记出所有的需要回收的对象,在标记完成以后统一回收

所有被标记的对象。

特点:(1)效率问题,标记和清除的效率都不高;(2)空间的问题,标记清除以后会产生大量

不连续的空间碎片,空间碎片太多可能会导致程序运行过程需要分配较大的对象时候,无法找到足够

连续内存而不得不提前触发一次垃圾收集。

地方 :适合在老年代进行垃圾回收,比如CMS收集器就是采用该算法进行回收的。

  • 标记整理:

原理:分为标记和整理两个阶段:首先标记出所有需要回收的对象,让所有存活的对象都向一端

移动,然后直接清理掉端边界以外的内存。

特点:不会产生空间碎片,但是整理会花一定的时间。

地方:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc的) gc和Serial old收

集器就是采用该算法进行回收的。

  • 复制算法:

原理:它先将可用的内存按容量划分为大小相同的两块,每次只是用其中的一块。当这块内存用

完了,就将还存活着的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉。

特点:没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小位原来的一

半。

地方:适合新生代区进行垃圾回收。serial new,parallel new和parallel scanvage收集器,就是采

用该算法进行回收的。复制算法改进思路:由于新生代都是朝生夕死的,所以不需要1:1划分内存空

间,可以将内存划分为一块较大的Eden和两块较小的Suvivor空间。每次使用Eden和其中一块

Survivor。当回收的时候,将Eden和Survivor中还活着的对象一次性地复制到另一块Survivor空间上,

最后清理掉Eden和刚才使用过的Suevivor空间。其中Eden和Suevivor的大小比例是8:1。缺点是需要

老年代进行分配担保,如果第二块的Survovor空间不够的时候,需要对老年代进行垃圾回收,然后存

储新生代的对象,这些新生代当然会直接进入来老年代。

  • 优化思路:

分代收集算法原理:根据对象存活的周期的不同将内存划分为几块,然后再选择合适的收集算

法。一般是把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。

在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为

对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回

收。

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



并行收集器:串行收集器使用一个单独的线程进行收集,GC 时服务有停顿时间

串行收集器:次要回收中使用多线程来执行 CMS 收集器是基于“标记—清除”算法

实现的,经过多次标记才会被清除

G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)

上来看是基于“复制”算法实现的

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

新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC

9. 几种常用的内存调试工具:jmap、jstack、jconsole、jhat



jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息

mat(eclipse 的也要了解一下)

10. 简述 java 垃圾回收机制?


在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在

JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚

拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将

它们添加到要回收的集合中,进行回收。

11. java 类加载过程?

java 类加载需要经历一下 5 个过程:

加载

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化

为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象。

验证

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保

证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)

  • 是否所有方法都遵守访问控制关键字的限定

  • 方法调用的参数个数和类型是否正确

  • 确保变量在使用之前被正确初始化了

  • 检查变量是否被赋予恰当类型的值

准备

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默

认初始值,如 0、0L、null、false 等)

也就是说,假如有这样一段代码:

  1. public String chenmo = "沉默";
  2. public static String wanger = "王二";
  3. public static final String cmower = "沉默王二";

chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null

需要注意的是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以

cmower 在准备阶段的值为“沉默王二”而不是 null

解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

  • 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关

信息。

  • 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的

  • ;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直

接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就

是直接引用。

初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

总结

类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够

被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过

程中进行卸载。如果想要了解Java类整个生命周期的话,可以自行上网查阅相关资料,这里不再多做赘述。

在面试过程中类加载过程虽然是一个老生常谈的问题,但是往往从这个问题还可以衍生出很多其他重要的知

识点,已经罗列在下文中,如果大家感兴趣的话,可以自行学习,小编也会在之后的文章中,对其中的一些

问题进行解答和总结。

相关扩展知识点:

  • Java虚拟机的基本机构?
  • 什么是类加载器?
  • 简单谈一下类加载的双亲委托机制?
  • 普通Java类的类加载过程和Tomcat的类加载过程是否一样?区别在哪?
  • 简单谈一下Java堆的垃圾回收机制?

12. 类加载器双亲委派模型机制?



当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类

去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

13. 什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接

引用。

  1. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的

实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  1. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)

来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过

ClassLoader.getSystemClassLoader()来获取它。

  1. 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现