JVM的内存模型介绍一下根据 JDK 8 规范JVM运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存属于操作系统的本地内存也是可以直接操作的。JVM的内存结构主要分为以下几个部分程序计数器可以看作是当前线程所执行的字节码的行号指示器用于存储当前线程正在执行的 Java 方法的JVM 指令地址。如果线程执行的是 Native 方法计数器值为 undefined(未定义一一因为 native 方法由本地代码实现不再对应字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域生命周期与线程相同。Java虚拟机栈每个线程都有自己独立的Java虚拟机栈生命周期与线程相同。每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会拋出 StackOverflowError 和 OutOfMemoryError 异常。本地方法栈与Java 虚拟机栈类似主要为虚拟机使用到的Native方法服务在HotSpot虚拟机中和Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧同样可能出现StackOverflowError 和OutOfMemoryError两种错误。Java 堆是VM中最大的一块内存区域被所有线程共享在虚拟机启动时创建用于存放对象实例。从内存回收角度堆被划分为新生代和老年代新生代又分为Eden 区和两个Survivor区From Survivor和 To Survivor。如果在堆中没有内存完成实例分配并且堆也无法扩展时会拋出OutOfMemoryError异常。方法区元空间)在JDK1.8及以后的版本中方法区被元空间取代使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分但有“非堆”的别名。方法区可以选择不实现垃圾收集内存不足时会抛出OutOfMemoryError 异常。运行时常量池是方法区的一部分用于存放编译期生成的各种字面量和符号引用具有动态性运行时也可将新的常量放入池中。当无法申请到足够内存时会抛出OutOfMemoryError 异常。直接内存不属于JVM 运行时数据区的一部分通过NIO 类引入是一种堆外内存可以显著提高I/O性能。直接内存的使用受到本机总内存的限制若分配不当可能导致OutOfMemoryError 异常。JVM内存模型里的堆和栈有什么区别用途栈主要用于存储局部变量、方法调用的参数、方法返回地址以及一些临时数据。每当一个方法被调用一个栈帧stack frame就会在栈中创建用于存储该方法的信息当方法执行完毕栈帧也会被移除。堆用于存储对象的实例包括类的实例和数组。当你使用new关键字创建一个对象时对象的实例就会在堆上分配空间。生命周期栈中的数据具有确定的生命周期当一个方法调用结束时其对应的栈帧就会被销毁栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定其对应的栈帧就会被销毁栈中存储的局部变量也会随之消失。堆中的对象生命周期不确定存取速度栈的存取速度通常比堆快因为栈遵循先进后出的原则操作简单快速。堆的存取速度相对较慢因为对象在堆上的分配和回收需要更多的时间而且垃圾回收机制的运行也会影响性能。存储空间栈的空间相对较小每个线程一个单线程栈大小可由-Xss参数配置由JVM 管理。当栈溢出时通常是因为递归过深或局部变量过大。堆的空间较大动态扩展也由JVM 管理。堆溢出通常是由于创建了太多的大对象或未能及时回收不再使用的对象。可见性栈中的数据对线程是私有的每个线程有自己的栈空间。堆中的数据对线程是共享的所有线程都可以访问堆上的对象。栈中存的到底是指针还是对象栈中存储的不是对象而是对象的引用。堆分为哪几部分呢新生代新生代分为 Eden Space 和 Survivor Space。Eden 区是新生代中最大的区域默认 Eden:S0:S1 8:1:1大多数新创建的对象首先存放在这里。当Eden 区满时会触发一次 Minor GC新生代垃圾回收。在Survivor Spaces中通常分为两个相等大小的区域称为S0Survivor 0和S1Survivor 1。在每次Minor GC后存活下来的对象会被移动到其中一个Survivor空间以继续它们的生命周期。这两个区域轮流充当对象的中转站帮助区分短暂存活的对象和长期存活的对象。老年代:存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长因此Major GC也称为Full GC涉及老年代的垃圾回收发生的频率相对较低但其执行时间通常比Minor GC长。老年代的空间通常比新生代大以存储更多的长期存活对象。元空间:从Java 8开始永久代被元空间取代用于存储类的元数据信息如类的结构信息如字段、方法信息等。元空间并不在Java堆中而是使用本地内存这解决了永久代容易出现的内存溢出问题。大对象在 G1 垃圾收集器中任何超过 Region 一半大小的对象都会被认定为 Humongous Object直接分配在一组连续的 Humongous Region 中这些 Region 在 G1 的逻辑上属于老年代的一部分但有独立的分配策略避免大对象在年轻代频繁被复制移动而带来的开销。传统的分代 GC如 Parallel / CMS中超过-XX:PretenureSizeThreshold的大对象也会直接分配到老年代原因同样是避免在 Eden 和 Survivor 之间反复复制。如果有个大对象一般是在哪个区域大对象通常会直接分配到老年代。新生代主要用于存放生命周期较短的对象并且其内存空间相对较小。如果将大对象分配到新生代可能会很快导致新生代空间不足从而频繁触发 Minor GC。而每次 Minor GC 都需要进行对象的复制和移动操作这会带来一定的性能开销。将大对象直接分配到老年代可以减少新生代的内存压力降低 Minor GC 的频率。大对象通常需要连续的内存空间如果在新生代中频繁分配和回收大对象容易产生内存碎片导致后续分配大对象时可能因为内存不连续而失败。老年代的空间相对较大更适合存储大对象有助于减少内存碎片的产生。程序计数器的作用为什么是私有的Java程序是支持多线程一起运行的多个线程一起运行的时候cpu会有一个调动器组件给它们分配时间片比如说会给线程1分给一个时间片它在时间片内如果它的代码没有执行完它就会把线程1的状态执行一个暂存切换到线程2去执行线程2的代码等线程2的代码执行到了一定程度线程2的时间片用完了再切换回来再继续执行线程1剩余部分的代码。我们考虑一下如果在线程切换的过程中下一条指令执行到哪里了是不是还是会用到我们的程序计数器啊。每个线程都有自己的程序计数器因为它们各自执行的代码的指令地址是不一样的呀所以每个线程都应该有自己的程序计数器。方法区中的方法的执行过程解析方法调用JVM会根据方法的符号引用找到实际的方法地址如果之前没有解析过的话。栈帧创建在调用一个方法前JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。执行方法执行方法内的字节码指令涉及的操作可能包括局部变量的读写、操作数栈的操作、跳转控制、对象创建、方法调用等。返回处理方法执行完毕后可能会返回一个结果给调用者并清理当前栈帧恢复调用者的执行环境。方法区中还有哪些东西用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。String保存在哪里呢字符串字面量保存在字符串常量池中字符串常量池在 JDK 6 及之前位于方法区永久代中自 JDK 7 起已经移到了堆中。String s new String(abc) 执行过程中分别对应哪些内存区域如果 abc 这个字符串常量之前不存在则创建两个对象常量池里的 abc new 出来的实例如果 abc 这个字符串常量已经存在则只会创建一个对象new 出来的实例。引用类型有哪些有什么区别强引用指的就是代码中普遍存在的赋值方式比如A a new A()这种。只要强引用还存在变量未离开作用域、也没有被显式置 nullGC 就不会回收该对象。软引用可以用SoftReference来描述指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。弱引用可以用WeakReference来描述他的强度比软引用更低一点弱引用的对象下一次GC的时候一定会被回收而不管内存是否足够。虚引用也被称作幻影引用是最弱的引用关系可以用PhantomReference来描述他必须和ReferenceQueue一起使用同样的当发生GC的时候虚引用也会被回收。可以用虚引用来管理堆外内存。内存泄漏和内存溢出的理解内存泄露内存泄漏是指程序在运行过程中不再使用的对象仍然被引用而无法被垃圾收集器回收从而导致可用内存逐渐减少。内存泄露常见原因静态集合使用静态数据结构如HashMap或ArrayList存储对象且未清理。事件监听未取消对事件源的监听导致对象持续被引用。线程未停止的线程可能持有对象引用无法被回收。内存溢出内存溢出是指Java虚拟机JVM在申请内存时无法找到足够的内存最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。内存溢出常见原因大量对象创建程序中不断创建大量对象超出JVM堆的限制。持久引用大型数据结构如缓存、集合等长时间持有对象引用导致内存累积。线程过多每个线程都需要独立的栈空间线程数过多时申请栈内存失败可能抛出OutOfMemoryError: unable to create new native thread注意深度递归触发的是StackOverflowError并不属于 OOM二者是不同的 Error。遇到过堆溢出的情况吗如何解决堆溢出通常发生在程序持续创建对象且无法被 GC 及时回收的场景下。首先需要定位原因一般分两步捕获内存快照通过 JVM 参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath./heapdump.hprof让程序在发生 OOM 时自动生成堆快照文件。分析快照文件使用 MATMemory Analyzer Tool或 JProfiler 等工具分析快照重点看哪些对象占用了大量内存、是否存在内存泄漏如对象长期被无用引用持有无法回收。常见的解决思路根据原因不同而不同如果是内存泄漏比如静态集合无意识地缓存了大量对象、长生命周期对象持有短生命周期对象的引用如单例类持有业务对象等。这时候需要梳理对象引用链找到未释放的根源比如清理静态集合中不再使用的元素、解除不必要的对象关联。如果是内存不足即程序确实需要大量内存如处理大文件、加载大量数据到内存但当前堆配置太小。这种情况下可以通过调整 JVM 参数扩大堆内存比如-Xms2g -Xmx4g初始堆 2G最大堆 4G但需注意不能超过物理内存限制避免频繁 swap。另外从代码层面优化也很重要比如避免一次性加载全部数据改用分批处理、使用缓存时设置合理的过期策略、及时释放资源如 IO 流、数据库连接等从源头减少内存占用。栈溢出的情况呢栈溢出主要发生在 Java 虚拟机栈或本地方法栈的内存空间被耗尽时通常与方法调用的深度直接相关。从触发原因来看最常见的场景是无限递归调用。因为 Java 方法调用时会在栈中创建栈帧存储局部变量、操作数栈、方法返回地址等每递归一次就会新增一个栈帧。如果递归没有正确的终止条件栈帧会不断累积最终超过虚拟机栈的最大容量导致栈溢出。另一种情况是单个方法的栈帧过大。如果一个方法定义了大量局部变量或者局部变量占用内存过大比如大数组单个栈帧就会占用较多栈空间可能在调用层级不深时就耗尽栈内存。解决栈溢出的思路主要有排查递归逻辑检查是否存在无限递归或递归层级过深的问题添加正确的终止条件或减少递归深度。必要时可将递归改写为迭代如用循环替代因为迭代不会持续创建新栈帧。调整栈内存大小通过 JVM 参数-Xss如-Xss256k增大栈内存容量。但这种方式要谨慎栈内存过大会导致线程可创建数量减少总内存固定时单个线程栈越大能创建的线程数越少。优化方法栈帧减少方法内局部变量的数量避免在方法中创建过大的对象或数组将大对象的创建移到堆中通过 new 关键字降低单个栈帧的内存占用。有具体的内存泄漏和内存溢出的例子么请举例及解决方案?1、静态属性导致内存泄露如何优化第一进来减少静态变量第二如果使用单例尽量采用懒加载。2、未关闭的资源如何优化第一始终记得在finally中进行资源的关闭第二关闭连接的自身代码不能发生异常第三Java7以上版本可使用try-with-resources代码方式进行资源关闭。3、使用ThreadLocal如何解决此问题第一使用ThreadLocal提供的remove方法可对当前线程中的value值进行移除第二不要使用ThreadLocal.set(null) 的方式清除value它实际上并没有清除值而是查找与当前线程关联的Map并将键值对分别设置为当前线程和null。第三最好将ThreadLocal视为需要在finally块中关闭的资源以确保即使在发生异常的情况下也始终关闭该资源。对象的生命周期对象的生命周期包括创建、使用和销毁三个阶段创建对象通过关键字new在堆内存中被实例化构造函数被调用对象的内存空间被分配。使用对象被引用并执行相应的操作可以通过引用访问对象的属性和方法在程序运行过程中被不断使用。销毁当对象不再被引用时通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象释放对象占用的内存空间完成对象的销毁过程。类加载器有哪些双亲委派模型的作用保证类的唯一性通过委托机制确保了所有加载请求都会传递到启动类加载器避免了不同类加载器重复加载相同类的情况保证了Java核心类库的统一性也防止了用户自定义类覆盖核心类库的可能。保证安全性由于Java核心库被启动类加载器加载而启动类加载器只加载信任的类路径中的类这样可以防止不可信的类假冒核心类增强了系统的安全性。例如恶意代码无法自定义一个java.lang.System类并加载到 JVM 中因为这个请求会被委托给启动类加载器而启动类加载器只会加载标准的 Java 库中的类。支持隔离和层次划分双亲委派模型支持不同层次的类加载器服务于不同的类加载需求如应用程序类加载器加载用户代码扩展类加载器加载扩展框架启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制保证了各个层级类加载器的职责清晰也便于维护和扩展。简化了加载流程通过委派大部分类能够被正确的类加载器加载减少了每个加载器需要处理的类的数量简化了类的加载过程提高了加载效率。讲一下类的加载和双亲委派原则我们把 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 类型。标记清除算法的缺点是什么主要缺点有两个一个是效率问题标记和清除过程的效率都不高另外一个是空间问题标记清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。GC只会对堆进行GC吗JVM 的垃圾回收器不仅仅会对堆进行垃圾回收它还会对方法区进行垃圾回收。堆Heap堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上因为大多数对象都会被分配在堆上而垃圾回收的重点通常也是回收堆中不再被引用的对象以释放内存空间。方法区Method Area方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法区中的垃圾回收与堆有所不同但是同样存在对不再需要的常量、无用的类信息等进行清理的过程。