一、JVM内存结构
JVM的内存模型(运行时数据区)
JVM运行时数据区包括:
程序计数器:当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器。
Java虚拟机栈:每个线程私有,生命周期与线程相同。每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法返回地址等。局部变量表存放编译期可知的基本数据类型、对象引用和returnAddress类型。
本地方法栈:与虚拟机栈类似,但为Native方法服务。
Java堆:被所有线程共享,在虚拟机启动时创建,用于存放对象实例。是垃圾收集器管理的主要区域。
方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在HotSpot中,方法区也被称为“永久代”(JDK8之前)和“元空间”(JDK8及之后)。
运行时常量池:方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
Java堆的结构?什么是新生代和老年代?
Java堆是垃圾收集的主要区域,从分代回收的角度,分为新生代和老年代。
新生代:新创建的对象首先放在新生代。新生代又分为Eden区和两个Survivor区(通常称为S0和S1,或者from和to)。大多数对象在Eden区中创建,当Eden区满时,触发Minor GC,将存活的对象移动到Survivor区。对象在Survivor区中经过多次GC后,年龄达到阈值(默认15)则进入老年代。
老年代:存放长期存活的对象和大对象(比如大数组)。当老年代空间不足时,触发Major GC(或Full GC)。
另外,还有一部分空间是永久代/元空间,但不在堆内,而是方法区的实现。
二、垃圾回收
如何判断对象是否可以被回收?
答案:有两种算法:
引用计数法:给对象添加一个引用计数器,有引用时加1,引用失效时减1,为0时回收。但无法解决循环引用问题。
可达性分析算法:通过一系列称为“GC Roots”的对象作为起点,向下搜索,走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。GC Roots包括:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象等。
Java中的引用类型有哪些?
答案:四种,从强到弱:
强引用:最常见的引用,只要强引用存在,对象就不会被回收。
软引用:在内存不足时会被回收,适合用于缓存。
弱引用:只能生存到下一次垃圾收集之前,无论内存是否足够,都会回收。
虚引用:最弱的引用,无法通过虚引用获取对象,设置虚引用的唯一目的是为了在这个对象被回收时收到一个系统通知。
请描述垃圾回收算法有哪些?
答案:
标记-清除:先标记出所有需要回收的对象,然后统一回收。缺点:效率问题,空间碎片。
复制算法:将内存分为两块,每次使用一块,当这一块用完了,将存活的对象复制到另一块,然后清除已使用的块。适用于新生代。
标记-整理:标记过程同标记-清除,但后续让所有存活的对象都向一端移动,然后清理掉边界以外的内存。适用于老年代。
分代收集:根据对象存活周期将内存分为新生代和老年代,然后根据各自的特点采用不同的收集算法。
常见的垃圾收集器有哪些?
答案:
Serial:单线程,新生代复制算法,老年代标记-整理,适用于客户端模式。
ParNew:Serial的多线程版本,新生代并行,老年代串行,可与CMS配合。
Parallel Scavenge:吞吐量优先,新生代复制算法,老年代标记-整理。
Serial Old:Serial的老年代版本,标记-整理。
Parallel Old:Parallel Scavenge的老年代版本,标记-整理。
CMS:以最短回收停顿时间为目标,标记-清除算法,过程:初始标记、并发标记、重新标记、并发清除。缺点:对CPU资源敏感,无法处理浮动垃圾,碎片多。
G1:面向服务端,将堆划分为多个Region,可预测的停顿时间,整体标记-整理,局部复制。步骤:初始标记、并发标记、最终标记、筛选回收。
ZGC和Shenandoah:低延迟的垃圾收集器,几乎全并发的收集器。
什么是Minor GC、Major GC、Full GC?
答案:
Minor GC:发生在新生代的垃圾回收,频率高,回收速度快。
Major GC:通常指老年代的GC,但不太明确,有时指Full GC。
Full GC:回收整个堆,包括新生代、老年代、永久代/元空间等,速度慢,应尽量避免。
三、类加载机制
请描述类加载的过程?
答案:类加载分为加载、连接、初始化三个阶段,其中连接又分为验证、准备、解析。
加载:通过类的全限定名获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,在堆中生成一个代表这个类的Class对象。
验证:确保Class文件的字节流信息符合虚拟机要求,包括文件格式、元数据、字节码、符号引用验证。
准备:为类变量(静态变量)分配内存并设置初始值(零值),不包含实例变量。
解析:将常量池中的符号引用替换为直接引用。
初始化:执行类构造器
<clinit>()方法,为类变量赋程序设定的初始值。
什么是双亲委派模型?有什么作用?
答案:类加载器之间的层次关系,除了启动类加载器外,每个类加载器都有父加载器。当一个类加载器收到类加载请求,首先委派给父加载器,只有当父加载器无法完成时,子加载器才尝试加载。
作用:保证Java核心类的安全,避免核心类被篡改;同时避免类的重复加载。
如何打破双亲委派模型?
答案:重写ClassLoader的loadClass方法(不委派给父加载器)即可打破。典型的例子:Tomcat为每个Web应用提供独立的类加载器,优先加载Web应用自己的类,找不到再委派给父加载器。
四、性能调优与工具
常用的JVM性能调优参数有哪些?
答案:
堆内存:
-Xms初始堆大小,-Xmx最大堆大小。新生代:
-Xmn新生代大小,-XX:SurvivorRatioEden和Survivor比例。老年代:
-XX:NewRatio老年代与新生代比例。永久代/元空间:
-XX:PermSize,-XX:MaxPermSize(JDK7及之前);-XX:MetaspaceSize,-XX:MaxMetaspaceSize(JDK8及之后)。GC日志:
-XX:+PrintGCDetails,-XX:+PrintGCDateStamps,-Xloggc:gc.log。其他:
-XX:+UseConcMarkSweepGC指定CMS收集器,-XX:+UseG1GC指定G1收集器。
如何排查内存溢出(OOM)问题?
答案:
首先通过工具(如jmap)生成堆转储文件(heap dump)。
使用分析工具(如MAT, JProfiler, VisualVM)分析dump文件,查看占用内存最大的对象,以及引用链,定位可能的内存泄漏代码。
五、其他
什么是Java内存模型(JMM)?
答案:Java内存模型定义了多线程环境下,共享变量的访问规则。它规定了线程如何与主内存和工作内存交互,以及何时将数据同步回主内存。JMM主要围绕原子性、可见性、有序性来保证线程安全。
原子性:基本数据类型的访问读写是原子性的,但long和double的非原子性协定。synchronized块之间的操作是原子性的。
可见性:一个线程修改了共享变量,其他线程能够立即看到。volatile、synchronized、final保证可见性。
有序性:禁止指令重排序。volatile和synchronized保证有序性。
请解释Java中的逃逸分析?
答案:逃逸分析是一种分析对象作用域的技术,分析对象动态作用域,当一个对象在方法中被定义后,如果被外部方法引用(如作为参数传递到其他方法),则称为方法逃逸;如果被其他线程访问,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程外,则可以为这个变量进行一些优化:
栈上分配:将对象分配在栈上,随着方法结束而销毁,减轻GC压力。
同步消除:如果对象不会逃逸出线程,则同步措施可以去掉。
标量替换:将对象拆散成基本数据类型,直接在栈上分配。
六、实战题
如何优化GC性能?
答案:
根据应用特点选择合适的垃圾收集器。例如,追求低延迟可以用CMS或G1,追求高吞吐量可以用Parallel Scavenge。
调整堆大小,避免过小导致频繁GC,过大导致单次GC时间过长。
调整新生代与老年代的比例,避免对象过早进入老年代。
避免创建大对象,因为大对象会直接进入老年代。
监控GC日志,分析GC频率和耗时,针对性调整。