金三银四面试季节——Java 核心面试技术点-《JVM篇》

描述一下 JVM 的内存区域

  • 程序计数器(PC,Program Counter Register)。在 JVM 规范中,每一个线程都有它本身的程序计数器,而且任什么时候间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,若是是在执行本地方法,则是未指定值(undefined)。
  • Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每一个线程在建立时都会建立一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,一般叫做当前帧,方法所在的类叫做当前类。若是在该方法中调用了其余方法,对应的新的栈帧会被建立出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操做只有两个,就是对栈帧的压栈和出栈。栈帧中存储着局部变量表、操做数(operand)栈、动态连接、方法正常退出或者异常退出的定义等。
  • 堆(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎全部建立的Java 对象实例都是被直接分配在堆上。堆被全部的线程共享,在虚拟机启动时,咱们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。理所固然,堆也是垃圾收集器重点照顾的区域,因此堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
  • 方法区(Method Area)。这也是全部线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。因为早期的 Hotspot JVM 实现,不少人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增长了元数据区(Metaspace)。
  • 运行时常量池(Run-Time Constant Pool),这是方法区的一部分。若是仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各类信息,还有一项信息就是常量池。Java 的常量池能够存放各类常量信息,不管是编译期生成的各类字面量,仍是须要在运行时决定的符号引用,因此它比通常语言的符号表存储的信息更加宽泛。
  • 本地方法栈(Native Method Stack)。它和 Java 虚拟机栈是很是类似的,支持对本地方法的调用,也是每一个线程都会建立一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一起区域,这彻底取决于技术实现的决定,并未在规范中强制。

形成OOM的缘由有哪几种?

  • 堆内存不足是最多见的 OOM 缘由之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,缘由可能千奇百怪,例如,可能存在内存泄漏问题;也颇有可能就是堆的大小不合理,好比咱们要处理比较可观的数据量,可是没有显式指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,致使堆积起来,内存没法释放等。
  • 虚拟机栈和本地方法栈,这里要稍微复杂一点。若是咱们写一段程序不断的进行递归调用,并且没有退出条件,就会致使不断地进行压栈。相似这种状况,JVM 实际会抛出StackOverFlowError;固然,若是 JVM 试图去扩展栈空间的的时候失败,则会抛出OutOfMemoryError。
  • 对于老版本的 Oracle JDK,由于永久代的大小是有限的,而且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再须要的类型)很是不积极,因此当咱们不断添加新类型的时候,永久代出现OutOfMemoryError 也很是多见,尤为是在运行时存在大量动态类型生成的场合;相似 Intern 字符串缓存占用太多空间,也会致使 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGenspace

GC 算法

  • 复制(Copying)算法,我前面讲到的新生代 GC,基本都是基于复制算法,将活着的对象复制到 to 区域,拷贝过程当中将对象顺序放置,就能够避免内存碎片化。这么作的代价是,既然要进行复制,既要提早预留内存空间,有必定的浪费;另外,对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 须要维护 region 之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
  • 标记 - 清除(Mark-Sweep)算法,首先进行标记工做,标识出全部要回收的对象,而后进行清除。这么作除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就致使其不适合特别大的堆;不然,一旦出现 Full GC,暂停时间可能根本没法接受。
  • 标记 - 整理(Mark-Compact),相似于标记 - 清除,但为避免内存碎片化,它会在清理过程当中将对象移动,以确保移动后的对象占用连续的内存空间。

G1 垃圾回收器采用的是什么垃圾回收算法?

从 GC 算法的角度,G1 选择的是复合算法,能够简化理解为:java

  • 在新生代,G1 采用的仍然是并行的复制算法,因此一样会发生 Stop-The-World 的暂停。
  • 在老年代,大部分状况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,而且不是总体性的整理,而是增量进行的。

GC 调优思路

从性能的角度看,一般关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput),大多数状况下调优会侧重于其中一个或者两个方面的目标,不多有状况能够兼顾三个不同的角度。固然,除了上面一般的三个方面,也可能须要考虑其余 GC 相关的场景,例如,OOM 也可能与不合理的 GC 相关参数有关;或者,应用启动速度方面的需求,GC 也会是个考虑的方面。 基本的调优思路能够总结为:程序员

  • 理解应用需求和问题,肯定调优目标。假设,咱们开发了一个应用服务,但发现偶尔会出现性能抖动,出现较长的服务停顿。评估用户可接受的响应时间和业务量,将目标简化为,但愿 GC 暂停尽量控制在 200ms 之内,而且保证必定标准的吞吐量。
  • 掌握 JVM 和 GC 的状态,定位具体的问题,肯定真的有 GC 调优的必要。具体有不少方法,好比,经过 jstat 等工具查看 GC 等相关状态,能够开启 GC 日志,或者是利用操做系统提供的诊断工具等。例如,经过追踪 GC 日志,就能够查找是不是 GC 在特定时间发生了长时间的暂停,进而致使了应用响应不及时。
  • 选择的 GC 类型是否符合咱们的应用特征,若是是,具体问题表如今哪里,是 Minor GC 过长,仍是 Mixed GC 等出现异常停顿状况;若是不是,考虑切换到什么类型,如 CMS 和 G1 都是更侧重于低延迟的 GC 选项。

经过分析肯定具体调整的参数或者软硬件配置。验证是否达到调优目标,若是达到目标,便可以考虑结束调优;不然,重复完成分析、调整、验证这 个过程。面试

如何提升JVM的性能?

  1. 新对象预留在年轻代 经过设置一个较大的年轻代预留新对象,设置合理的 Survivor 区而且提供 Survivor 区的使用率,能够将年轻对象保存在年轻代。算法

  2. 大对象进入年老代 使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值spring

  3. 设置对象进入年老代的年龄 这个阈值的最大值能够经过参数-XX:MaxTenuringThreshold 来设置,默认值是 15编程

  4. 稳定的 Java 堆 得到一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 同样。缓存

  5. 增大吞吐量提高系统性能 –Xmx380m –Xms3800m:设置 Java 堆的最大值和初始值。通常状况下,为了不堆内存的频繁震荡,致使系统性能降低,咱们的作法是设置最大堆等于最小堆。假设这里把最小堆减小为最大堆的一半,即 1900m,那么 JVM 会尽量在 1900MB 堆空间中运行,若是这样,发生 GC 的可能性就会比较高; -Xss128k:减小线程栈的大小,这样可使剩余的系统内存支持更多的线程; -Xmn2g:设置年轻代区域大小为 2GB; –XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,能够尽量地减小 GC 时间。 –XX:ParallelGC-Threads:设置用于垃圾回收的线程数,一般状况下,能够设置和 CPU 数量相等。但在 CPU 数量比较多的状况下,设置相对较小的数值也是合理的; –XX:+UseParallelOldGC:设置年老代使用并行回收收集器。安全

  6. 尝试使用大的内存分页 –XX:+LargePageSizeInBytes:设置大页的大小。 内存分页 (Paging) 是在使用 MMU 的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页 (page) 和页帧 (page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使 OS 能支持非连续性的内存分配。性能优化

  7. 使用非占有的垃圾回收器 为下降应用软件的垃圾回收时的停顿,首先考虑的是使用关注系统停顿的 CMS 回收器,其次,为了减小 Full GC 次数,应尽量将对象预留在年轻代。bash

system.gc() 的做用是什么?

gc()函数的做用只是提醒虚拟机:程序员但愿进行一次垃圾回收。可是它不能保证垃圾回收必定会进行,并且具体何时进行是取决于具体的虚拟机的,不一样的虚拟机有不一样的对策。

Parallel GC、CMS GC、ZGC、Azul Pauseless GC最主要的不一样是?背后的原理也请简单描述下?

Parallel GC的Young区采用的是Mark-Copy算法,Old区采用的是Mark-Sweep-Compact来实现,Parallel执行,因此决定了Parallel GC在执行YGC、FGC时都会Stop-The-World,但完成GC的速度也会比较快。 CMS GC的Young区采用的也是Mark-Copy,Old区采用的是Concurrent Mark-Sweep,因此决定了CMS GC在对old区回收时形成的STW时间会更短,避免对应用产生太大的时延影响。 G1 GC采用了Garbage First算法,比较复杂,实现的好呢,理论上是会比CMS GC能够更高效,同时对应用的影响也很小。 ZGC、Azul Pauseless GC采用的算法很不同,尤为是Pauseless GC,其中的很重要的一个技巧是经过增长Read Barrier来更好的识别对GC而言最关键的references变化的状况。

何时执行ygc,fullgc?

当young gen中的eden区分配满的时候触发young gc,当年老代内存不足时,将执行Major GC,也叫 Full GC。

gc()函数的做用只是提醒虚拟机:程序员但愿进行一次垃圾回收。可是它不能保证垃圾回收必定会进行,并且具体何时进行是取决于具体的虚拟机的,不一样的虚拟机有不一样的对策。

强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?

不一样的引用类型,主要体现的是对象不一样的可达性(reachable)状态和对垃圾收集的影响。

所谓强引用("Strong" Reference),就是咱们最多见的普通对象引用,只要还有强引用指向一个对象,就能代表对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,若是没有其余的引用关系,只要超过了引用的做用域或者显式地将相应(强)引用赋值为 null,就是能够被垃圾收集的了,固然具体回收时机仍是要看垃圾收集策略。

软引用(SoftReference),是一种相对强引用弱化一些的引用,可让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出OutOfMemoryError 以前,清理软引用指向的对象。软引用一般用来实现内存敏感的缓存,若是还有空闲内存,就能够暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

SoftReference 在“弱引用WeakReference”中属于最强的引用。SoftReference 所指向的对象,当没有强引用指向它时,会在内存中停留一段的时间,垃圾回收器会根据 JVM 内存的使用状况(内存的紧缺程度)以及 SoftReference 的 get() 方法的调用状况来决定是否对其进行回收。

对于幻象引用(PhantomReference ),有时候也翻译成虚引用,你不能经过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 之后,作某些事情的机制,好比,一般用来作所谓的 Post-Mortem 清理机制,如 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的建立和销毁。

Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try {
    // Remove 是一个阻塞方法,能够指定 timeout,或者选择一直阻塞
    Reference<Object> ref = refQueue.remove(1000L);
    if (ref != null) {
        // do something
    }
} catch (InterruptedException e) {
    // Handle it
}
复制代码

JVM类加载过程

通常来讲,咱们把 Java 的类加载过程分为三个主要步骤:加载、连接、初始化。 首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 承认的数据结构(Class 对象),这里的数据源多是各类各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;若是输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,咱们能够自定义类加载器,去实现本身的类加载过程。

第二阶段是连接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程当中。这里可进一步细分为三个步骤:

  • 验证(Verification),这是虚拟机安全的重要保障,JVM 须要核验字节信息是符合 Java 虚拟机规范的,不然就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
  • 准备(Preparation),建立类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所须要的内存空间,不会去执行更进一步的 JVM 指令。
  • 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。

最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动做,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

什么是双亲委派模型?

简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,不然尽量将这个任务代理给当前加载器的父加载器去作。使用委派模型的目的是避免重复加载 Java 类型。

类加载器的类型

  • 启动类加载器(Bootstrap Class-Loader),加载 jre/lib 下面的 jar 文件,如 rt.jar。它是个超级公民,即便是在开启了 Security Manager 的时候,JDK 仍赋予了它加载的程序 AllPermission。
  • 扩展类加载器(Extension or Ext Class-Loader),负责加载咱们放到 jre/lib/ext/ 目录下面的 jar 包,这就是所谓的 extension 机制。该目录也能够经过设置 “java.ext.dirs”来覆盖。
  • 应用类加载器(Application or App Class-Loader),就是加载咱们最熟悉的 classpath

上下文类加载器

Java 提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是做为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码常常须要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)**来加载的。引导类加载器是没法找到 SPI 的实现类的,由于依照双亲委派模型,BootstrapClassloader没法委派AppClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,能够在执行线程中抛弃双亲委派加载链模式,使程序能够逆向使用类加载器。

ServiceLoader 的加载代码:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
复制代码

ContextClassLoader默认存放了AppClassLoader的引用,因为它是在运行时被放在了线程中,因此无论当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何须要的时候均可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成须要的操做。

自定义类加载器

自定义类加载器,常见的场景有:

  • 实现相似进程内隔离,类加载器实际上用做不同的命名空间,以提供相似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,若是分别被不同的容器加载,就能够互不干扰。这个方面的集大成者是Java EE和OSGI、JPMS等框架。
  • 应用须要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。
  • 须要本身操纵字节码,动态修改或者生成类型

从本地路径 load class 的例子:

public class CustomClassLoader extends ClassLoader {
 
    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }
 
    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                fileName.replace('.', File.separatorChar) + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }
}
复制代码

动态代理的原理?

反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。经过反射咱们能够直接操做类或者对象,好比获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至能够运行时修改类定义。 动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,不少场景都是利用相似机制作到的,好比用来包装 RPC 调用、面向切面的编程(AOP)。 实现动态代理的方式不少,好比 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其余的实现方式,好比利用传说中更高性能的字节码操做机制,相似 ASM、cglib(基于 ASM)、Javassist 等。

如何使用JDK动态代理?

public class MyDynamicProxy {
    public static  void main (String[] args) {
        HelloImpl hello = new HelloImpl();
        MyInvocationHandler handler = new MyInvocationHandler(hello);
        // 构造代码实例
        Hello proxyHello = (Hello) Proxy.newProxyInstance(HelloImpl.class.getClassLoader(), HelloImpl.class.getInterfaces(), handler);
        // 调用代理方法
        proxyHello.sayHello();
    }
}
interface Hello {
    void sayHello();
}
class HelloImpl implements  Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}
class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("Invoking sayHello");
        Object result = method.invoke(target, args);
        return result;
    }
}
复制代码

动态代理:JDK动态代理和CGLIB代理的区别?

JDK动态代理只能对实现了接口的类生成代理,而不能针对类,CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法(继承)。

JDK Proxy 的优点:

  1. 最小化依赖关系,减小依赖意味着简化开发和维护,JDK 自己的支持,可能比 cglib 更加可靠。
  2. 平滑进行 JDK 版本升级,而字节码类库一般须要进行更新以保证在新版 Java 上可以使用。
  3. 代码实现简单。

基于相似 cglib 框架的优点:

  1. 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,相似 cglib 动态代理就没有这种限制。
  2. 只操做咱们关心的类,而没必要为其余相关类增长工做量。
  3. 高性能。

Spring在选择用JDK仍是CGLiB的依据是什么?

(1)当Bean实现接口时,Spring就会用JDK的动态代理 (2)当Bean没有实现接口时,Spring使用CGlib是实现 (3)能够强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)

CGlib比JDK快?

(1)使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,比使用Java反射效率要高。惟一须要注意的是,CGLib不能对声明为final的方法进行代理,由于CGLib原理是动态生成被代理类的子类。可是JDK也在升级,开始引入不少字节码技术来实现部分动态代理的功能,因此在某些测试下不必定是CGLib更快。

Java 中操做字节码的技术

ASM、Javassist、CGLib、Byte Budy。


你们若是想获取更多的面试资料与架构知识,你们能够加个人程序员交流群: 833145934,群内每晚都会有阿里技术大牛讲解的最新Java架构技术。并会录制录播视频分享在群公告中,做为给广大朋友的加群的福利——分布式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高并发、高可用架构)/微服务(Spring Boot、Spring Cloud)/源码(Spring、Mybatis)/性能优化(JVM、TomCat、MySQL)【加群备注好消息领取最新面试资料】 
相关文章
相关标签/搜索