当前位置: 首页 > news >正文

三五互联网站建设怎么样公司网上推广平台

三五互联网站建设怎么样,公司网上推广平台,vi设计方案,wordpress怎么播放视频播放器JVM是Java高级部分#xff0c;深入理解程序的运行及原理#xff0c;面试中也问的比较多。 JVM是Java程序运行的虚拟机环境#xff0c;实现了“一次编写#xff0c;到处运行”。它负责将字节码解释或编译为机器码#xff0c;管理内存和资源#xff0c;并提供运行时环境深入理解程序的运行及原理面试中也问的比较多。 JVM是Java程序运行的虚拟机环境实现了“一次编写到处运行”。它负责将字节码解释或编译为机器码管理内存和资源并提供运行时环境使其不会被不同的底层操作系统和环境影响。 JVM是支持Java成为跨平台语言的基础Java代码在很多底层针对不同操作系统做了处理屏蔽了不同操作系统和处理器CPU的差异才使得其能够在多个平台运行。 先把大部分知识点理解记熟了再研究更深的。其中内存结构和垃圾回收很重要必问。 一核心结构 首先JVM与JRE、JDK的关系也会经常被问到。使用Java时要先安装JDK所以可以记住JDK是包装的最完整的然后再往前面推。 操作系统 JVM虚拟机 JREJVM基础类库 JDKJRE编译工具 JVM主要由以下四大模块构成类加载器、运行时数据区、执行引擎、本地方法接口。其中每个模块又会有自己的处理和知识点。 下面就按照顺序描述各个模块的知识点和相关问题。 二类加载器 类加载器ClassLoader是JVM的核心组件之一负责在运行时动态加载类。 我们编写的Java文件会存储为二进制字节码等待被使用。之后通过类加载器将二进制字节码编译为机器码生成完整的类并提供给运行时数据区。 1类加载机制 一个类完整的生命周期会经过五个阶段加载、链接、初始化、使用和卸载被垃圾回收。构成了类对象从无到有以及最后结束的一个过程。 加载、连接、初始化称为类加载机制链接又分为验证、准备、解析三个阶段。 1加载 简单来说就是将类的二进制字节流.class文件转换为方法区的数据结构并生成 Class 对象。 方法区就是用来存储类的数据、方法、结构等加载就是将类数据放入方法区。方法区只是Java的一个概念在不同厂商不同版本有相应的实现HotSpot Java 8使用元空间实现。具体在内存结构中详解。 注意即时编译的热点代码不在这个阶段进入方法区。 类加载底层涉及到两个概念instanceKlass、_java_mirror。两者均在加载阶段完成且是后续阶段链接、初始化的基础。 所以两者的创建都属于类加载机制。但两者的角色和存储位置不同分别对应 JVM 内部元数据和 Java 层对象。 常见问题Class对象在初始化阶段生成 Class 对象在加载阶段就已生成仅生成对象但类的静态变量赋值初始化在‌初始化阶段‌执行 重要在加载阶段JVM会将Class文件加载到内存并生成对应的Class对象。这时候就会创建运行时常量池作为方法区的一部分。 运行时常量池此时包含Class文件常量池中的‌原始数据‌字面量、符号引用等但符号引用如类、方法、字段的引用‌尚未解析为直接引用。 instanceKlass HotSpot JVM内部用C结构 instanceKlass 描述类的‌元数据‌属于JVM内部的‌元数据存储‌用于‌字节码执行‌。数据会放入方法区所以instanceKlass也属于‌方法区的实现细节 它的作用是存储类的‌完整结构信息‌例如 _super父类的instanceKlass指针_methods类的方法列表_constants运行时常量池区别于Class文件中的常量池_class_loader加载该类的类加载器引用以及其他。 特点instanceKlass实例由JVM直接分配在‌元空间Metaspace‌中属于‌非堆内存‌。 _java_mirror _java_mirror是instanceKlass在Java堆中的‌镜像对象‌对应Java层的java.lang.Class实例。所以其就是我们平时的Class对象。 作用为Java提供反射入口Class.forName存储类的动态信息如静态变量以及作为JVM内部元数据instanceKlass与Java应用层的桥梁属于应用层操作。 _java_mirror对象分配在‌Java堆‌中由垃圾回收器管理。 关系 二者有紧密联系当调用class.getName时会先访问Class实例然后通过JNI调用到instanceKlass中的元数据最后返回类名。 步骤 1‌JVM 解析 .class 文件生成 instanceKlass元空间。 ‌步骤 2‌根据 instanceKlass 的信息创建 _java_mirror堆。 ‌步骤 3‌instanceKlass 与 _java_mirror 建立双向指针关联。 instanceKlass内部有指针指向_java_mirror。 _java_mirror即Class对象通过Klass*指针关联到instanceKlass在JVM源码中通过oopDesc结构实现。 所以二者是有一个双向绑定的关系这是重点。 2链接 总体来说就是确保字节码符合JVM规范以及进行默认值处理。其中又分为验证、准备、解析三个阶段。 验证 验证文件格式是否编写正确数据是否正确是否符合规范等。具体有文件格式验证、元数据验证、字节码验证以及符号引用验证等。 说白了就是文件不能有问题数据不能有问题为了保证程序的安全性。 另外验证阶段‌不修改运行时常量池‌仅检查其内容的合法性。 准备 准备阶段就是为静态类变量分配内存并设置默认值。会根据是否final编译期常量直接赋指定的值。 1static 变量的赋值仅针对于 static 修饰的静态基本数据类型static修饰的变量称为类变量非static修饰的称为实例变量在此阶段不处理而是在对象实例化时分配内存及赋值。 2final 如果是不加final的类变量基本数据类型会在准备阶段赋各自默认值在初始化阶段赋实际值 如果是加了final的类变量且值为编译期常量值在编译器就确认了则会在准备阶段直接赋实际值。如果是运行期才能确定的值则也同普通类变量准备阶段赋默认值。 static final int c new Random().nextInt()。 //赋默认值0 String字符串是特殊的引用类型当String变量被 static final 修饰时且赋值为字面量编译器常量则也会直接赋值。其他情况下非static非final均为引用类型默认为null。具体原理可在方法区详解涉及常量池。 3基本类型 准备阶段仅针对于静态八大基本类型赋值如果是引用对象会默认为null不涉及准备阶段统一在对象实例化时处理。 详细理解准备阶段赋值有助于优化代码例如设置编译器常量减少初始化开销。以及排查类加载问题等。 解析 解析阶段就是将常量池中的符号引用转换为直接引用。符号引用就是一组符号来描述目标直接引用就是直接指向目标的指针可以用来定位到目标对象。 会将运行时常量池中的‌符号引用‌如java/lang/Object‌解析为直接引用‌如内存地址或句柄且缓存到运行时常量池中避免重复解析。 这期间运行时常量池会查询字符串常量池是否存在字符串如果有则直接引用否则创建并引用。 使用 classLoader.loadClass() 获取Class类对象时不会触发解析和初始化。 3初始化 初始化是类加载的最后一个环节但是注意区别于之前的环节初始化阶段是严格按需触发的。只有在首次主动使用时才会触发不会因类被加载而自动执行即初始化是惰性的。 初始化赋值 在Java中对类变量static修饰进行初始化赋值有两种方式 声明类变量时指定初始值使用静态代码块为类变量指定初始值 不加static的变量称为实例变量只有在类实例化时才会分配空间并赋值。 注意在使用静态代码块方式时必须把静态变量定义在静态代码块的前面因为两者是按照代码顺序执行的顺序不一致可能导致问题空指针。 如果静态代码块中引用了其他类例如通过静态方法调用被引用的类必须已经完成初始化否则可能导致递归初始化问题。 实现原理 编译器会将所有‌静态变量的赋值操作‌和‌静态代码块‌按代码顺序合并成一个名为clinit的类构造器方法。当触发类初始化时会执行类构造器clinit方法为静态变量赋实际值。原始构造的内容也会存在但会在最后。 静态代码块中如果抛出未捕获的异常会导致类初始化失败后续对该类的任何使用都会抛出ExceptionInInitializerError。 会引发初始化行为 可将其归纳为主动使用类的场景就会触发类的初始化。 会初始化场景场景描述new关键字通过new创建对象时类必须初始化。访问类的静态成员访问类需计算的静态变量非编译期常量或访问类的静态方法会导致初始化。反射获取类对象调用 Class.forName() 默认会触发实例化可通过传参设置为不初始化。类继承关系子类初始化时父类若没有初始化过会先初始化父类。接口默认方法如果接口的实现类初始化且接口包含默认方法则接口会初始化。包含main方法的类在程序启动时JVM会初始化包含main方法的类主启动类。动态语言支持做了解 通过MethodHandle访问静态成员‌获取类的静态字段或方法句柄时触发初始化。 MethodHandles.Lookup lookup MethodHandles.lookup(); lookup.findStatic(MyClass.class, staticMethod, MethodType.methodType(void.class)); 静态变量赋值包装类型时底层会自动装箱所以会导致初始化。 以上是会初始化的场景问到了大致说几个就可以了。另外还有不会触发初始化的场景。 不会触发初始化 不会初始化场景场景描述访问类的编译时常量‌访问 static final 且值在编译期确定准备阶段赋值。‌数组类型声明MyClass[] arr new MyClass;数组由JVM动态生成。反射获取类对象使用不会触发初始化的反射获取类对象如.class以及forName设置不初始化classLoad等。‌父类已初始化若父类已初始化子类引用父类的静态字段不会触发子类初始化。集合声明类有点牵强做一个了解。类作为集合的泛型被声明时除非主动实例化类对象否则不会初始化泛型擦除。 注意上述反射只是声明时不会初始化会在第一次使用该类class对象时触发类的初始化。 初始化的唯一性 JVM会通过锁和状态标记确保‌类初始化仅执行一次‌无论触发操作如何重复或并发。 1同步锁clinit方法线程安全 当多个线程同时尝试初始化一个类时JVM会通过隐式锁确保只有一个线程执行clinit方法其他线程阻塞等待唤醒后根据状态会跳过初始化。 2类标记状态 JVM为每个类维护一个状态如uninitialized、initializing、initialized。 一旦类完成初始化后续操作直接跳过clinit方法。 具体使用时才初始化类减轻了程序启动时的开销避免了启动程序时大批量初始化类的情况提高了程序性能。 4使用及卸载 使用很好理解类初始化之后就可以进行使用了可以对其属性和方法进行操作。 卸载JVM中的卸载指的是从JVM中移除Class对象、字节码和静态变量等卸载并不常见。 因为通常只有 ClassLoader 被回收后类才有可能被卸载。如果一个类是由系统类加载器加载的那么它可能很难被卸载因为系统类加载器通常不会回收通常与JVM生命周期一致。所以一般只有自定义类加载器加载器实例不再被引用时它加载的类才有可能被卸载。 类卸载条件 ‌所有实例被回收‌该类及其子类的所有实例都已被垃圾回收。‌类加载器被回收‌加载该类的 ClassLoader 实例已被回收。无活跃引用‌该类的 Class 对象如 MyClass.class没有被任何地方强引用例如反射、静态变量等。 注意仅仅垃圾回收类的实例 不意味着类本身被卸载类卸载必须满足上述条件。  重新创建类 类卸载后可以再次重新创建类并使用。但这一行为已经不是简单的初始化了而是类从字节码开始重新走一遍生命周期。 具体为尝试使用对象时调用静态方法实例化等会由类加载器重新加载字节码可能需要新的类加载器随后重新触发 clinit 方法重新初始化类。 所以重新加载是类卸载后的新生命周期开始。 public class Test {public static void main(String[] args) throws Exception {// 使用自定义类加载器加载类ClassLoader loader new CustomClassLoader();Class? clazz loader.loadClass(MyClass);// 触发初始化clazz.getMethod(init).invoke(null);// 清除引用触发卸载clazz null;loader null;// 强制触发GC仅示例实际生产环境慎用System.gc();Thread.sleep(1000);// 再次加载需要新的ClassLoaderloader new CustomClassLoader();clazz loader.loadClass(MyClass);// 会再次触发初始化clazz.getMethod(init).invoke(null);} }class CustomClassLoader extends ClassLoader {// 实现加载类的逻辑例如从字节码文件读取 }5枚举 在Java中枚举的本质是一个‌继承自 java.lang.Enum 的类其成员变量枚举实例会被隐式声明为 public static final且由 ‌JVM 保证全局唯一性‌。 枚举的加载遵循 Java 类加载机制实例会在 cinit 方法中被静态初始化。JVM会保证cinit的线程安全无需额外同步机制。 枚举的构造器是私有的由 JVM 强制限制通过反射调用 Constructor.newInstance() 时会抛出 IllegalArgumentException。 其内部实例也是单例的跟随初始化创建。 2类加载器 JVM中有三类核心类加载器形成‌双亲委派模型‌的层次结构。提供三个类加载器的原因是单一职责分别负责不同的区域。按照由大到小的顺序。 类加载器描述‌Bootstrap ClassLoader启动类加载器 由C实现是JVM的一部分无法直接访问。 加载JAVA_HOME/lib目录的核心类库如rt.jar。 处于类加载器层次顶端无父加载器。 Extension ClassLoader扩展类加载器 在Java中实现Java类sun.misc.Launcher$ExtClassLoader。 加载JAVA_HOME/lib/ext目录或java.ext.dirs系统变量指定的类库。 父加载器‌为 Bootstrap ClassLoader。 ‌Application ClassLoader应用程序类加载器 在Java中实现Java类sun.misc.Launcher$AppClassLoader。 加载用户类路径ClassPath下的类主要加载我们写的类。 父加载器‌为 Extension ClassLoader。 自定义类加载器 自定义路径父加载器‌为‌Application ClassLoader。 其中启动类加载器为最顶级加载器如果尝试通过 getClassLoader() 获取类加载器时会打印null。 因为启动类加载器由JVM内部的C代码实现没有对应的 ClassLoader 对象因此返回null并且这也是Java设计的一种规范以此作为启动类加载器标识。 其他的类加载器可以返回Java实例但注意扩展类加载器的getParent()也会返回null同上。 类的命名空间由类加载器包名类名共同确定唯一类。 不同类加载器加载的同一个类JVM视为不同类。 1自定义类加载器 如果有特殊场景需求时例如需要加载非classpath中的路径文件、想通过接口来实现、同时加载相同的类时则可以考虑创建并使用自定义类加载器。 步骤继承ClassLoad类并重写findClass()方法读取类字节码调用defineClass()生成Class对象遵循双亲委派机制在使用时通过该自定义类加载器 loadClass 方法获取类对象。 public class CustomClassLoader extends ClassLoader {Overrideprotected Class? findClass(String name) throws ClassNotFoundException {byte[] bytes loadClassFromDisk(name); // 自定义加载逻辑return defineClass(name, bytes, 0, bytes.length);} }自定义类加载器由JVM垃圾回收管理并且其加载的类在一定条件下会被JVM卸载。 自定义类加载器需谨慎管理生命周期避免内存泄漏如Metaspace/PermGen溢出。 2双亲委派模型 首先术语双亲是指的appClassLoader的上面两个父类所以这样描述。 双亲委派模型是指类加载请求会首先委派给父加载器并且一层层往上父加载器无法完成时子加载器才尝试加载。 优点 唯一性保证类的一致性同一个类由同一个类加载加载。 安全性避免重复加载确保核心类安全如String类防止开发者对Java程序类进行篡改。 一般我们说的都是Java 8的双亲委派其实在Java 9的双亲委派有一些变化做了相应的优化。 JDK 9引入了模块化概念大体就是将不同的包指定为一个模块然后指定该模块的类加载器。当需要加载类时委派到平台类加载器会将其直接派给指定的类加载器处理。 会避免无用的委派优化了类加载性能提高程序启动速度。 打破双亲委派模型 有些场景可能需要由子加载器优先加载不遵循双亲委派机制该行为称为打破双亲委派。 常见的有 TomcatWeb服务器需要保证每个Web应用使用独立的类加载器加载各自独立的类即使同名加载WEB-INF下的类。 SPIService Provider Interface‌如JDBC驱动加载使用线程上下文类加载器Thread Context ClassLoader加载厂商实现。 ‌OSGi模块化‌每个Bundle有自己的类加载器形成网状依赖关系。 线程上下文类加载器 也属于打破双亲委派模型其作用是解决父加载器需访问子加载器资源的场景如JDBC加载第三方驱动。 在线程启动时会默认把应用程序类加载器放入线程加载器使用getContentClassLoader可获取当前线程类加载器。 可通过 Thread.currentThread().setContextClassLoader()设置线程类加载器。 使用 ServiceLoader.load() 通过上下文类加载器加载服务实现。 3类加载器源码 在 loadClass的源码中通过方法 findLoaderClass 判断是否加载过如果有则从缓存中直接取不会重新加载。 如果这个类没被加载过返回null。则标识需要加载类此时会判断一个parent对象就是我们的类加载器。 如果parent对象不为空则递归调用 loadClass 方法递归时也会判断各个类加载是否加载过。直到类加载器为顶级bootstrap时parent才会为空并进入方法尝试获取加载类缓存。 如果顶级加载器找不到类会向下继续找如果所有类加载器都找不到类会抛出找不到类异常。 protected Class? loadClass(String name, boolean resolve) throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded 类是否已加载过如果存在直接使用Class? c findLoadedClass(name);if (c null) {long t0 System.nanoTime();try {if (parent ! null) { //类加载器不为空递归向上找c parent.loadClass(name, false);} else {c findBootstrapClassOrNull(name); //只有顶级类加载器时才会为空并委派顶级父加载器处理}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader }......}if (resolve) {resolveClass(c); //如果所有类加载器都无法加载类会抛出找不到类异常}return c;} } 三运行时数据区 运行时数据区主要由五大部分组成方法区、堆、虚拟机栈、本地方法栈、程序计数器讲解时也可以按照这个顺序。 这五个部分方法区、堆、栈会涉及内存溢出堆是垃圾回收的主要区域方法区的回收条件严格且效率低虚拟机找的内存管理不依赖于垃圾回收而是通过栈帧的弹出释放内存。 栈内存的释放是确定性的‌方法结束即释放而堆和方法区的回收是非确定性的由 GC 策略决定。 另外方法区和堆是线程共享的所以堆内存的共享变量要考虑线程安全问题。 1方法区 方法区是一个规范用于存储‌类信息、常量、静态变量、即时编译器JIT编译后的代码‌等数据。它是所有线程共享的内存区域生命周期与 JVM 一致。 注意方法区作为规范只定义了上述逻辑概念没有规定具体实现方法不同的JVM可以用不同的数据结构实现如Open J9我们常说的就是 HotSpot所以在描述方法区时一定要指定JVM。 1内存结构 方法区是一个规范在Hotspot虚拟机中Java 8对其方法区的实现做了调整和优化。 1.7及之前 方法区的实现是永久代PermGen其位于堆内存中大小固定可以通过参数 -XX:PermSize 和 -XX:MaxPermSize 配置。 缺点受固定内存限制灵活性不高易因类加载过多或常量池过大导致 PermGen OOM。 1.8及以后 1.8修改为元空间Metaspace实现元空间使用本地内存默认不限制大小但会受操作系统物理内存限制。 优势为降低 OOM 风险内存分配更灵活没有永久代的内存焦虑问题可设置元空间内存上限避免耗尽系统内存根据类加载的需求动态扩展减少内存浪费。 2核心作用 方法区的特点是线程安全的。当方法区无法满足内存分配需求时会抛出OutOfMemoryError内存溢出。Java 8之后使用系统内存很难溢出可以通过设置较小参数来模拟类加载过多OOM的场景。 1类元数据存储 在上面解释类加载过程时提过Hotspot 方法区底层就是使用 instanceKlass 存储类的元数据类名、字段、方法、类加载器等。 与堆中的 Class 对象相互关联并直接分配在本地内存中而非堆内存。 静态变量 类级别的变量static 修饰直接存储在方法区中。注意如果是静态引用对象则方法区仅存储引用地址实际对象实例存在堆内存中。 JIT热点代码 JIT 编译后的热点代码会存储在方法区的“代码缓存”Code Cache中‌。这里简单描述下具体可以在执行引擎-JIT详细描述触发条件。 代码缓存是 JVM 为存储‌即时编译器JIT生成的本地机器码‌而预留的内存区域会由JVM单独分配通常也会在本地内存。 垃圾回收 方法区也有垃圾回收机制但是回收条件较为严格需满足类卸载条件引出类加载机制。主要用于回收废弃常量和无用的类不可过度依赖容易发生内存泄漏和内存溢出问题。 这里可能会有问题描述下方法区的‌OutOfMemoryError原因和处理 原因 未合理配置元空间大小。项目中可能存在大量动态生成的类如 CGLib、反射、JSP。类加载器未卸载如频繁热部署应用或创建大量自定义类加载器。 处理 调整 -XX:MaxMetaspaceSize元空间内存大小。如果设置默认需考虑物理内存压力。减少动态类生成如缓存代理对象以及减少反射动态获取考虑缓存反射类等。检查代码去除或减少无用的类加载器无用的类等避免内存泄漏。 3核心参数 描述针对于方法区的参数设置以及元空间垃圾回收和扩容流程。 元空间参数描述-XX:MetaspaceSize 初始的高水位线注意不是初始化这么大初始分配的内存可能较小然后根据需要动态调整。 当元空间使用量达到此值时触发Full GC尝试回收无用元数据。若回收后仍不足则扩容。 -XX:MaxMetaspaceSize 元空间的最大上限默认无限制受限于系统内存。建议生产环境设置此值防止内存耗尽。 如果达到了MaxMetaspaceSize的限制就无法继续扩容导致OOM错误。 -XX:MinMetaspaceFreeRatio触发元空间扩容的最小空闲比例默认 40%注意设置值时无需百分号。-XX:MaxMetaspaceFreeRatio触发元空间缩容的最大空闲比例默认 70%内存退回给操作系统。 通过参数可以控制元空间的垃圾回收频率以及扩容的触发时机。 1扩容触发 扩容针对的是设置了初始初始高水位线的场景不设置默认为系统最大内存生产建议设置避免宕机。 扩容机制元空间由多个内存块组成每个块分配给特定的类加载器。当加载新类时JVM从当前块分配内存。当触发扩容时JVM会向操作系统申请新的内存块每次扩容的大小由JVM内部策略决定通常逐步增加。达到上限时不会触发扩容而是触发GC。 上述的两个参数都可以用来控制扩容初始高水位线、最小空闲比例。 最小空闲比例当元空间的空闲比例低于这个阈值时会触发元空间扩容不会直接触发GC。只会在扩容失败时达到最大上限才会触发GC尝试回收数据。初始高水位线当元空间使用量达到该值时触发Full GC尝试回收元数据若GC后内存还不足则扩容。当两个参数同时设置了且条件同时满足时。JVM会优先处理【最小空闲比例】的扩容需求扩容成功后直接分配扩容失败则GC。后续会检查【初始高水位线】在首次使用量达到时如果上个参数已扩容则此时不会触发GC而是将高水位线自动更新为扩容后的容量。如果上个参数未扩容则按原逻辑触发GC再尝试扩容。 所以两个参数对于扩容和GC顺序不同不会产生冲突‌能够协同确保内存分配的效率与稳定性从性能方面看肯定是先扩容好因为GC成本太高。 优化初始值避免初始值过小导致频繁 GC。 优化最小空闲比例如果元空间增长过快降低该值以减少扩容频率如果内存充足提高该值以保留更多空闲内存减少GC风险。 对于缩容元空间将内存释放给操作系统的条件较严格通常需满足最大空闲比例且依赖不同的JVM实现。 2垃圾回收触发 如果元空间的使用达到了设置的最大阈值分配新内存失败时会触发Full GC回收方法区不再使用的元数据清除满足类卸载条件的数据条件较为严格无用类无法被回收即为内存泄漏。 Full GC表示全局垃圾回收暂停所有线程STW。这里的Full GC与堆内存的Full GC是同一个过程只是触发的条件不同。都是由垃圾回收器执行。 因此元空间触发的Full GC实际上会触发整个堆的回收而堆触发的Full GC同样可能影响元空间。 如果垃圾回收后内存还是不够则会抛出内存溢出程序终止。 4运行时常量池 运行时常量池其实也是方法区的核心部分比较重要这里单独描述。在描述之前需要先理解三个术语以及他们的关系。 1常量池 常量池指的是Class文件结构里的一部分里面存放了各种类编译时期生成的各种字面量和符号引用如类接口名、字段、方法等信息。 这部分信息是在编译时生成的存在于Class文件中。每个类都有自己的常量池。 2运行时常量池 运行时常量池是‌方法区的一部分‌每个类或接口在JVM中加载后其Class文件中的常量池会被解析并加载到运行时常量池。其跟随Hotspot方法区实现1.8之前在永久代1.8及之后在元空间。 运行时常量池创建发生在类的加载阶段符号引用替换发生在解析阶段。 每个类或接口都有自己的运行时常量池同Class常量池。 在加载时运行时常量池存的是符号引用在解析阶段会替换为字符串常量池的引用。 两者关系Class文件中的常量池是运行时常量池的“静态快照”运行时常量池是其在JVM中的运行时形态。 3字符串常量池 可称为String Pool / String Table字符串常量池是JVM中‌全局共享的字符串缓存池‌用于存储字符串对象的引用避免重复创建相同字符串。 在Java中字符串是不可变的所以JVM为了优化会有一个全局的字符串常量池。当用双引号直接创建字符串时JVM会检查字符串常量池中是否存在该字符串如果有就直接返回引用否则创建并放入池中。 在Java 7之前位于永久代方法区Java 7及之后被移到堆内存Heap。 字符串常量池是一个JVM内部实现的哈希表结构不是集合框架中的在Java7存放在永久代时大小固定不可扩容易导致内存问题。Java 7及之后移动到堆内存后大小可通过 JVM 参数调整‌例如 -XX:StringTableSizeN默认 60013。 字符串常量池是惰性加载按需加载即用到时才加载。实际字符串对象的创建和入池操作发生在‌首次主动使用该字面量时‌如赋值、方法调用。 如果字符串相加的值在编译期能够确认则会进行编译期优化从串池中获取。所以有些面试题会考察字符串相加判断根据其是否从串池获取以及存储位置是否一样判断。 与运行时常量池关系运行时常量池可能包含字符串常量池中的符号引用。 比如在类加载阶段Class创建并加载到运行时常量池后。 随后会在解析符号阶段查询字符串常量池如果存在则引用指向已有的对象否则创建新的字符串对象并放入字符串常量池。 所以运行时常量池中的字符串实际上是引用到字符串常量池中的对象。 5String.intern() String提供的一个方法调用 intern() 时JVM 会检查字符串常量池StringTable中是否存在与当前字符串内容相同的对象如果有则直接返回如果不存在会将当前字符串对象的引用添加到字符串常量池‌并返回该引用。 intern() 直接操作的是 ‌字符串常量池StringTable‌而非运行时常量池。运行时常量池中的符号引用在类加载时已被解析为字符串常量池中的对象引用不会修改其内容。 伴随着Java版本对方法区的调整intern方法对串池的处理也不同妥协所以经常会有字符串比对的相关问题。 Java 6 如果一个堆内存字符串对象调用intern方法会判断字符串常量池是否存在。 如果不存在则会将堆中的字符串内容拷贝到永久代‌生成一个新的字符串对象加入池中并返回永久代的引用。如果存在则直接返回永久代的引用。 结果堆中的对象和池中的对象是‌两个独立的对象‌地址不同。会导致判断失败。 String s1 new String(abc); // 堆中对象 String s2 s1.intern(); // 永久代对象拷贝生成 System.out.println(s1 s2); // false地址不同Java 7 从 Java 7 开始字符串常量池被移至堆内存与普通对象共存。 所以对象调用intern方法时如果不存在则会将将堆中该字符串对象的引用直接加入池中无需拷贝如果存在则直接返回池中的引用。 结果池中存储的是‌堆中对象的引用‌地址相同。 String s1 new String(abc); // 堆中对象 String s2 s1.intern(); // 池中引用指向堆中的 s1 对象 System.out.println(s1 s2); // true地址相同但是不能确保一定返回堆中的引用具体要看 intern 执行的时机。 如果字符串常量池中已有相同内容的字符串例如通过字面量 abc 提前加载则 intern() 直接返回池中引用与调用时机无关。此时再和堆内存对象判断地址会比较不成功。 String s0 abc; // 池中已加载 abc String s1 new String(abc); String s2 s1.intern(); System.out.println(s2 s0); // trues2 指向池中已有的 abc System.out.println(s1 s2); // falses1 是堆中的新对象equals() 与 的区别‌ 这里就更加清晰两者的区别了很经典的面试题以及建议字符串用equlse判断值的原因。 无论 Java 6 还是 Java 7只要字符串内容相同equals() 始终会返回 true判断内容相等。 但 的结果取决于对象地址 Java 6 中intern() 后的对象地址与堆对象地址不同拷贝到永久代会比较失败。 Java 7 中intern() 后的对象地址可能与堆对象地址相同引用复用会比对成功但是需要注意 intern 方法的执行时机串池中没有提前字面量加载。 2堆内存 堆内存是JVM管理的内存区域中最重要的一部分用于存储对象实例和数组。它是所有线程共享的内存空间也是垃圾回收的主要区域。 堆内存存储的数据 new 创建的对象和数组。存储类级别的静态实例对象存储实例变量的值基本类型或堆对象引用。Java 7及之后存储字符串常量池作为全局字符串缓冲池。存储线程私有的分配缓冲区JVM 为每个线程在堆内存中分配一小块私有区域TLAB用于快速分配对象避免多线程竞争。存储逃逸分析优化失败的对象JVM 的‌逃逸分析‌会尝试将未逃逸的对象分配在栈上栈上分配但若分析失败或未启用优化对象仍分配在堆中栈章节中详细描述。垃圾回收相关的元数据如标记信息分代年龄等数据。 静态变量不在堆中的原因 静态变量属于类由方法区管理与堆内存隔离。这种设计避免了静态变量被垃圾回收除非类卸载同时减少堆内存压力。 1内存结构 为了优化垃圾回收效率Java将堆内存划分为不同区域称之为不同的代且随着JDK版本做了相关优化。  上图是Java 8堆内存详细的划分在之前还会存在一个永久代内存区域。所以堆内存最大的改动就是在Java 8对方法区的实现。 新生代 新生代用于存放新创建的对象其中‌又分为了 Eden 区‌和 ‌Survivor 区‌存储不同时期对象。新生代垃圾回收称为 Minor GC理解为代价小一点的GC。 Eden区对象首次分配的区域。在Minor GC时会被清空用于分配新对象。 Survivor区‌存储并处理垃圾回收时的对象其中又分为 Survivor From 和 Survivor To 区。进入From区和To区的对象会在两者之间移动不会回到Eden区。直到年龄足够晋升到老年代或被回收。 这里涉及到垃圾回收算法的标记整理用来清除无用对象保留存活对象具体可以在GC篇描述。 老年代 用来存放长期存活的对象如直接晋升老年代或经过多次 Minor GC 后依然存活的晋升对象。老年代的垃圾回收称为 Migor GCFull GC表示全局垃圾回收一般会认为是老年代垃圾回收导致的全局回收会造成STD速度较慢代价比较大具体可以在GC篇详细描述。 老年代就是一块完整的内存区域没有更具体的区域划分Java 8。 永久代 这里就能理解为什么Java 8之前的方法区叫永久代因为1.8版本前方法区实现在堆中所以也遵循了堆中的分代规则起了永久代的名字并划分一块内存区域内存固定容易造成OOM。 1.8及之后版本将永久代从堆内存移除改为元空间使用本地内存避免了永久代导致的内存溢出问题。 Java 9 回答问题时可以提一下在Java 9中G1默认称为垃圾回收器G1 将堆划分为多个大小相等的 ‌Region‌区域不再严格物理分代。 逻辑上我们还会称为分代但物理上是动态 Region 分配。其优势是提供更可控的停顿时间适合大内存应用。 2核心参数 通过 JVM 参数可以配置堆内存的大小和分代比例从而进行我们常说的JVM调优和GC调优以及内存溢出等相关问题的故障排查。 堆内存的相关参数大致分为四个部分堆内存基础参数、分代内存参数、高级调优参数。 使用的话就是加在程序的启动参数中中途加入不会生效程序需重启。 堆内存基础设置 -Xms初始堆大小建议与 -Xmx 相同避免堆动态扩容导致的性能波动。 -Xmx最大堆大小堆内存上限超过会触发 OutOfMemoryError。 -XX:UseCompressedOops启用压缩指针默认为开启可优化对象头大小节省内存。替换为减号即为关闭。 使用以上参数可简单设置堆内存大小结合本地内存容量减少内存不足的问题提高系统稳定性。 -Xmx 至少为系统可用内存的 1/4但不超过 80%避免系统崩溃。 分代内存参数年轻代/老年代 分代参数描述-XX:NewSize年轻代初始大小设置年轻代的初始值。-XX:MaxNewSize年轻代最大大小结合堆初始/最大内存使用。-XX:OldSize老年代初始大小结合堆初始/最大内存使用。-XX:NewRatio 老年代与年轻代的比例默认值 JDK8 为 2。 使用-XX:NewRatio3表示老年代:年轻代3:1。 -XX:SurvivorRatio Eden 区与 Survivor 区的比例默认为8。 使用-XX:SurvivorRatio8表示Eden区和survivor区From和To区比例8:1:1。 -XX:MaxTenuringThreshold 对象晋升老年代的年龄阈值默认15设置0表示直接进入年代。 使用-XX:MaxTenuringThreshold15。 一般情况下直接增大堆内存可以简单快速的解决内存问题但如果有内存紧缺的场景例如项目体量过大、机器本地内存限制等则只能最大程度的分配堆内存中的分代内存。 新生代内存不足频繁触发垃圾回收时可考虑提高新生代大小或减小晋升阈值让对象进入老年代。 老年代内存不足可考虑提高老年代比例。或提高晋升阈值使对象保留在新生代但要注意可能需要提高Survivor区的比例因为进入Survivor区的对象不会回到Eden区。 高级调优参数 高级参数描述-XX:UseTLAB 启用线程本地分配缓冲默认开启可加速对象分配减少竞争。 -XX:TLABSize设置 TLAB 大小每个线程在堆内存分配的一小块区域使用时根据线程数调整优化。-XX:AlwaysPreTouch启动时预分配物理内存避免运行时内存分配延迟但会延长启动时间。-XX:EliminateAllocations启用逃逸分析优化自动将未逃逸对象分配在栈上默认开启。 生产环境中一般是多个参数结合使用根据场景选择构建高吞吐量、低延迟、大内存的的程序提高系统性能减少内存问题。 3内存溢出 当发生内存泄漏或堆内存不足时设置较小或对象较多会触发垃圾回收机制当回收后内存还不够时就会抛出内存溢出OutOfMemoryErrorOOM程序终止。 堆内存时垃圾回收的主要区域一般也称Full GC为堆内存不足虽然方法区也可引发Full GC但和堆相比概率较低。 常见的堆内存溢出原因有内存泄漏、堆内存设置较小。 优化策略是排查代码有没有内存泄漏问题设置较大堆内存优化对象生命周期调整分代内存结构使用工具分析堆内存使用情况VisualVM / JConsole/图形化查看堆内存使用。 内存泄漏和内存溢出区别 总结内存泄漏是程序中的错误导致对象无法被回收可用内存逐渐减少而内存溢出是程序需要的内存超过了可用的内存可能由泄漏引起也可能是其他原因。 两者的关系是内存泄漏可能导致内存溢出但溢出不一定由泄漏引起也可能是正常的内存不足。 内存泄漏 对象已经不再被程序使用但由于错误被意外保留在内存中导致垃圾回收器GC无法回收它们。例如静态集合长期持有对象引用、ThreadLocal的引用、资源泄漏连接池或IO未关闭、类无法卸载、Hash值改变等。 处理使用工具如MAT、VisualVM分析堆转储找到未被回收的对象引用链删除或优化代码。注意内存泄漏不是单纯加内存能解决的问题的源头还是代码问题。 内存溢出 申请内存时JVM的堆或方法区没有足够空间分配对象就会抛出OOM程序终止。可能的原因为内存泄漏长期累计、JVM方法区或堆内存设置较小、程序体积或数据量过大等。 处理检查是否存在泄漏设置JVM参数调整检查程序逻辑是否处理了大数据场景修改或优化代码。 两者在生产环境都是比较严重的问题内存泄漏是温水煮青蛙加内存也只是缓解一时需要解决代码的源头问题不好排查内存溢出会使程序终止可能造成业务操作中断、用户数据丢失等问题很严重。 3虚拟机栈 虚拟机栈是Java虚拟机内存模型运行时的重要部分用于支持方法调用和执行过程中的数据管理栈内存是线程私有的不涉及垃圾回收的区域。 线程私有每个线程在创建时都会分配一个独立的虚拟机栈生命周期与线程相同。 每个栈只能有一个活动栈帧对应着当前正在执行的那个方法。 1内存结构 栈帧每个方法的执行对应一个栈帧的入栈方法开始和出栈方法结束栈帧中存储方法的局部变量、操作数栈、动态链接、返回地址等信息。 后进先出LIFO‌方法调用链的栈帧按调用顺序压入和弹出。 栈帧内包含几个重要部分主要用来存储局部变量以及对数据进行操作。 全局变量需要考虑线程安全。看一个栈帧内的变量是否线程安全可以看其是否被其他方法引用或当作返回值被引用逃离方法。 局部变量表存储方法参数和局部变量包括基本数据类型、对象引用和返回地址。以‌变量槽Slot‌为最小单位32位类型如int占1个槽64位类型如long、double占2个槽对象通常占用一个槽具体由JVM实现。局部变量表大小在编译期确定不会在运行时改变。 操作数栈执行字节码指令时的工作区用于存放计算中间结果和调用参数。提供给编译器通过指令获取计算。操作数栈大小也是编译时确定写入方法的Code属性中。 动态链接将符号引用转换为直接引用方法在内存中的实际地址。每个栈帧持有指向运行时常量池的引用支持动态绑定。 方法返回地址记录方法正常退出或异常中断后应返回的位置。 其他还有例如多线程中存储对象头进行锁获取等。 在方法中发生未捕获的异常时栈帧会被弹出直到找到合适的异常处理器。可能涉及多个栈帧的销毁同时异常信息会被传递到调用者栈帧中。 2栈内存溢出 当线程请求的栈深度超过虚拟机允许的最大值时会抛出StackOverflowError例如无限递归或设置了较小的栈内存。 当线程扩展栈时无法申请足够内存时可能会抛出OutOfMemoryError不过通常栈溢出更常见于StackOverflowError。 可以通过-Xss设置栈大小如-Xss1M默认值依赖JVM实现和操作系统。 异常抛出时JVM通过栈帧中的信息生成堆栈轨迹可使用JConsole、VisualVM或jstack命令查看线程栈信息。 4程序计数器 程序计数器是用来保存当前线程‌正在执行的字节码指令地址是线程私有的且唯一不会内存溢出的区。 ‌线程私有‌每个线程独立拥有一个程序计数器互不影响。 部分JVM实现可能将程序计数器映射到CPU的寄存器以提升执行效率。 简单来说就是记住下一条JVM指令的执行地址提供并指导执行引擎完成计算。是虚拟机栈和执行引擎之间的桥梁。 5本地方法栈 Java虚拟机用于管理本地方法的调用而本地方法栈就用来管理本地方法的调用即代码中使用native修饰的方法底层可能是C或C编写的本地接口。 需注意不是所有的JVM都支持本地方法本地方法栈也是线程私有的。 常见的有例如Object类的clone方法就是调用本地方法。 对一个运行中的Java程序而言当其某个线程调用本地方法时它就进入了一个全新的不受虚拟机管理的模块本地方法可以通过本地方法接口来访问虚拟机中的运行时数据区以及其他数据。 四执行引擎 执行引擎时负责将JVM中字节码转换为底层操作系统能够执行的机器指令并实际运行程序。 执行引擎主要有三个功能点解释器、即时编译器、垃圾回收器。 1解释器 逐条读取并解释字节码指令将字节码解释为机器码即使下次读取是相同的仍会重复解释。 优点是启动速度快内存占用较低缺点是执行效率较低每次运行会重复解释。 2即时编译器 一般称为JIT其功能是将热点代码编译为本地机器码后续执行直接运行机器码。 优点是显著提升长期运行程序的性能如服务器应用。 1编译器实现 ‌C1编译器Client Compiler‌轻量级编译器优化启动速度适用于客户端程序。‌C2编译器Server Compiler‌深度优化编译器牺牲编译时间换取更高的执行效率。‌分层编译Tiered Compilation‌JDK 7默认启用结合C1和C2的优势先由C1编译再对热点代码由C2深度优化。 热点代码探测 可通过-XX:CompileThreshold默认 10000当方法调用次数超过阈值时会触发编译。 此时JIT 编译器如 C1/C2将字节码编译为机器码存入代码缓存。 编译优化策略 方法内联将小方法调用替换为方法体代码减少调用开销例如A(1*1)并可结合常量折叠进一步优化。 逃逸分析判断对象是否逃逸出方法或线程决定是否进行栈上分配或锁消除。 2代码缓存 代码缓存是 JVM 为存储‌即时编译器JIT生成的本地机器码‌而预留的内存区域。 在 HotSpot 中代码缓存是方法区的一部分由JVM单独分配通常也在本地内存。 可通过 -XX:ReservedCodeCacheSize 和 -XX:InitialCodeCacheSize 参数配置大小。 当代码缓存占满时JIT 编译会停止导致程序退化为解释执行性能下降。可通过添加参数 -XX:UseCodeCacheFlushing 允许在缓存不足时回收无用代码如已卸载类的方法代码。 后续调用可直接执行代码缓存中的机器码大幅提升性能。 3其他描述 JVM基于栈‌字节码指令通过操作数栈进行运算指令更紧凑跨平台性好但效率略低。 物理CPU基于寄存器‌直接操作寄存器效率更高但依赖硬件架构不符合Java跨平台运行。 垃圾回收器属于内存管理模块但执行引擎需要与GC协作在安全点Safepoint暂停线程以执行垃圾回收。 Graal编译器‌JDK 10引入的可插拔编译器支持更多激进优化未来可能替代C2。 ‌AOT编译JDK 9通过jaotc工具将字节码预先编译为机器码减少启动时间。 五垃圾回收 JVM的垃圾回收是自动管理内存的核心机制负责回收不再使用的对象以释放内存避免内存泄漏。吸取了C的经验改为自动管理避免手动管理垃圾回收导致的一些问题。 主要从几个方面描述回收条件、回收算法、垃圾回收器、回收操作。 1回收条件 回收算法的目的是判断对象是否可进行回收并自动进行垃圾回收释放资源节省内存空间。 1引用计数法 引用计数法并未在Java中使用而是作为一个概念理解在其他语言中有使用到。 原理每个对象维护一个计数器记录被引用的次数。当引用计数归零时对象被回收。 缺点两个对象互相引用引用计数无法归零导致内存泄漏另外需要频繁更新计数器并且在多线程环境下还需要考虑线程安全问题造成较大性能开销。 2可达性分析算法 Java中使用的是根搜索算法也称为可达性分析算法。 原理从GC Roots出发遍历对象引用链。若对象不可达例如将属性或对象赋值为null则标记为可回收。 GC Root主要有四种对象线程栈帧中的局部变量或参数、引用类型静态变量、字符串常量池引用、同步锁持有的对象。 在可达和不可达的状态中间还有一个可复活状态对象被标记为可回收但在finalize()方法中重新与引用链关联。 3引用类型 JVM提供了四种引用类型用来在发生垃圾回收时判断特定的对象是否可被回收释放内存。 ‌强引用 平时用到的所有引用都属于强引用例如new对象对象间赋值等。 对象只要被一个强引用关联GC 永远不会回收。强引用链断裂时可被回收。 ‌软引用Soft 适用于内存敏感型缓存通过 SoftReference 类包装使用。 当没有强引用直接引用时且即使发生垃圾回收内存也不够时回收软引用对象。 弱引用Weak 适用于临时缓存、监听器注册通过WeakReference 类包装使用。 当没有强引用直接引用时无论内存是否充足发生GC就会被回收。 ‌虚引用Phantom 适用于精准控制资源释放必须与 ReferenceQueue 结合使用当对象被回收时虚引用会被加入队列。 虚引用对象无法通过 get() 获取实际对象仅用于跟踪对象被回收的时机。 软弱引用还可以搭配队列使用因为其本身也是对象会占用空间。使用队列后当软弱引用的对象被回收后会进入引用队列进一步处理。 // 创建软引用对象 SoftReferencebyte[] softRef new SoftReference(new byte[1024 * 1024]); // 尝试获取对象 byte[] data softRef.get(); // 若内存充足返回字节数组若内存不足返回 null// 创建弱引用对象 WeakReferenceObject weakRef new WeakReference(new Object()); // 手动触发 GC仅示例实际开发中避免调用 System.gc() System.gc(); // 检查对象是否存活 if (weakRef.get() null) {System.out.println(对象已被回收); }// 创建引用队列 ReferenceQueueObject queue new ReferenceQueue(); // 创建虚引用对象 PhantomReferenceObject phantomRef new PhantomReference(new Object(), queue); // 检查对象是否被回收 Reference? ref queue.poll(); // 若对象被回收返回虚引用对象 if (ref ! null) {System.out.println(对象已被回收可执行清理操作); } 2回收算法 堆内存是垃圾回收的主要区域JVM提供了不同的算法用于堆内存的垃圾回收。 为什么标记存活对象而不是标记失效对象 效率问题‌垃圾回收的目的是回收内存而 JVM 中大部分对象是“朝生夕死”的。直接标记存活对象可以避免遍历大量无效对象提高效率。安全性问题‌如果误删存活对象会导致程序崩溃因此必须明确标记存活对象后再清理未标记的部分。 注意对于标记对象网上有的说是标记失效对象这个一定要明确是标记存活对象。在《深入理解Java虚拟机》的书中确实描述标记需要回收的对象但实际实现是标记存活对象反向推导的。 1标记清除 分为两个步骤核心思想是通过“标记存活对象”和“清除未标记对象”两个阶段来回收内存。 1沿GC Root引用链标记所有存活对象。若需并发标记如CMS收集器需通过“三色标记”等机制处理应用线程与GC线程的并发修改。 2线性遍历整个堆内存识别未标记的对象。将未标记对象占用的内存标记为空闲供后续分配使用。 优点是速度快只需做一个标记且实现逻辑简单无需移动对象适合处理存活率高的老年代对象。 缺点是清除后会产生大量不连续的内存碎片可能导致大对象分配失败会造成两次停顿降低响应时间清除阶段需遍历整个堆时间复杂度为O(n)。 2标记整理 是对于标记清除算法产生内存碎片的优化方案。 其步骤为标记存活对象后将其移动到内存一端清除边界外的空间。解决了内存碎片问题。 缺点是效率低、速度慢整理需要移动对象造成开销且对象中的局部/引用变量也需要改变引用地址。 同标记清除适用于老年代对象存活率高的场景。 3复制算法 将内存分为两块区域称为From区和To区To区域始终保持空区域状态。核心思想是通过空间划分‌和对象复制‌来高效管理内存避免内存碎片化问题。 步骤从GC Root根对象触发标记所有可达对象。将From区的可达对象按顺序复制到To区随后直接清空From区的所有对象。最后From区和To区互换角色为下一次回收做准备。 优点由于保持顺序分配所以没有内存碎片问题。 缺点传统复制算法需要预留 50% 的闲置内存From 和 To 区各占一半利用率较低。 4分代回收机制 上面都是JVM提供的三种回收算法但实际上虚拟机不会单纯使用某种具体的算法在Hotspot虚拟机中是采用三种算法结合的方式称为分代机制。 具体可以参考堆内存篇结构描述。新生代老年代采用不同算法新生代使用复制算法并优化了传统复制算法的双倍内存问题。老年代采用标记清除或标记整理算法。 具体流程为 创建对象时优先分配在 ‌Eden 区‌当触发 Minor GC 时将 ‌Eden From Survivor 中的存活对象‌ 标记并复制到 ‌To Survivor‌年龄1清空无用对象。当存活对象年龄达到阈值默认 15后晋升到老年代。 3垃圾收集器 JVM提供了多种垃圾收集器适用于不同场景可以从两个方面对其进行分类。 1从设计目标区分 单线程型串行执行如 Serial仅用于特定场景小内存或客户端应用。并行-吞吐量优先型 如 Parallel Scavenge目标是最大化应用运行时间占比GC时间占比最小化。 可理解为处理效率高。适用于多线程、注重整体吞吐量的场景。 并发-低延迟型 如 CMS、G1、ZGC目标是减少单次GC停顿时间STW时间短。 最小化单次停顿时间适用于大堆内存和对延迟敏感的场景如Web服务。 简单来说类似微服务的CAP理论是性能和响应速度的选择。 2根据作用区域和算法可分为两种收集器分代收集器和全堆收集器。 垃圾回收器并行并行指的是多个垃圾回收线程一起执行不能有用户线程。 垃圾回收器并发用户线程和垃圾回收线程一起执行能提高响应速度。 1Serial Serial是串行的垃圾回收器属于分代收集器类型。单线程串行工作通过 -XX:UseSerialGC 参数显式使用。 针对新生代和老年代新生代采用复制算法老年代采用标记-整理算法。 GC时触发STW暂停所有用户线程执行垃圾回收线程。 优点为内存占用低无多线程开销。但其只适用于客户端应用或小内存应用不适合生产高并发场景。 2Parallel Scavenge JDK8默认收集器 Parallel Scavenge是吞吐量优先的垃圾回收器属于分代收集器类型多线程并行工作可通过两个参数开启。 -XX:UseParallelGC表示新生代的垃圾回收器算法是复制多线程并行。 -XX:UseParallelOldGC表示老年代的垃圾回收器算法是标记-整理。 这两个开关在1.8中是默认开启的且只要有一个开启另一个也会开启。 ‌核心特点是多线程并行回收‌注重最大化吞吐量单位时间处理请求数。垃圾回收线程数取决于CPU个数会执行多个垃圾回收线程尽快处理。适用于后台计算密集型任务。 提供了核心参数用于动态调整 -XX:ParallelGCThreads4 # 并行GC线程数默认与CPU核数相同 -XX:GCTimeRatio99 # GC时间与总时间占比1/(199)1% -XX:MaxGCPauseMillis200 # 目标最大GC停顿时间毫秒-XX:GCTimeRatio99注意垃圾回收时间不能超过工作总时间的百分之一否则考虑加大堆内存。并且时间占比参数和最大停顿时间互相冲突需根据场景具体选择。 -XX:GCTimeRatio99表示吞吐量堆越大则吞吐越大清理的资源多所需时间会增加。 -XX:MaxGCPauseMillisms表示时间设置时间越少则表示堆越小吞吐量越小时间缩短。 3ParNew parNew是Serial收集器的多线程版本属于分代收集器类型多线程并行工作通过参数 -XX:UseParNewGC 启用。 需注意只针对新生代使用复制算法多线程并行处理。 需要和CMS搭配使用在JDK9后逐渐被G1替代。 4CMS CMSConcurrent Mark-Sweep是并发低延迟的垃圾回收器属于分代收集器多线程并发执行通过参数 -XX:UseConcMarkSweepGC 启用。 注意JDK9后标记为废弃DeprecatedJDK14中移除。 需注意只针对老年代使用标记-清除算法。CMS只能与ParNew或Serial配合。 特点是采用‌并发标记和清除‌减少STW时间。适用于对延迟敏感的服务如Web应用。 缺点是受标记清除算法影响存在内存碎片问题可能会触发Full GC进行压缩。由于内存是不连续的所以对象在分配内存时会采用空闲列表方式。 工作流程 ‌初始标记‌STW标记GC Roots直接关联的对象。‌并发标记‌遍历对象图无停顿用户和垃圾回收线程同时运行。‌重新标记‌STW修正并发标记期间的变动最后标记未避免期间再变动会STW。‌并发清除‌回收垃圾对象无停顿和用户线程同时运行。 并发标记阶段使用三色标记通过写屏障处理并发修改。用户线程只会在打初始标记时短暂阻塞。 优化参数 -XX:CMSInitiatingOccupancyFraction75 # 老年代使用率触发CMS的阈值% -XX:UseCMSCompactAtFullCollection # Full GC时压缩内存 -XX:CMSFullGCsBeforeCompaction4 # 每4次Full GC后压缩一次5G1 G1Garbage-First是同时注重吞吐量、低延迟、超大堆内存的垃圾回收器属于全堆收集器混合收集多线程并发执行通过参数 -XX:UseG1GC 启用。 G1是在Java 7引入的Java 8需手动开启在JDK9作为默认收集器。 结合了‌标记-整理算法‌和‌分区回收策略‌旨在平衡‌吞吐量‌和‌停顿时间‌尤其适合现代多核处理器和大内存应用场景。 每个Region的内存较少且可以单独回收所以它可以采用标记整理方式避免内存碎片。并且内存规整后对象在 Region 分配内存时会使用指针碰撞的方式最大限度使用空间。 核心思想是将堆划分为多个等大的Region默认2048个每个region都可以是以下类型之一在GC过程中动态变化无需固定分区比例。 ‌Eden Region‌存放新对象。 ‌Survivor Region‌存放存活对象Minor GC后晋升。 ‌Old Region‌存放长期存活对象。 ‌Humongous Region‌存储超大对象大小 ≥ Region的50%。 可通过 -XX:MaxGCPauseMillis 设定目标最大停顿时间默认200msG1会结合历史GC数据动态调整策略动态选择回收价值最高的Region以达成停顿目标。 1分阶段回收 Young GC‌回收Eden和Survivor区类似传统新生代GC。Mixed GC‌同时回收Young和部分Old Region减少老年代碎片。‌Full GC‌后备方案当并发回收速度跟不上对象分配速度时触发需避免。 工作流程 初始标记 通常由新生代触发在STW阶段标记GC Roots直接关联的对象。 耗时极短仅标记根对象。 并发标记 通常发生在老年代当老年代占用堆空间达到阈值时进行并发标记。 也是标记存活对象不会造成STW不会暂停用户线程。通过SATB算法处理漏标记标记期间对象变化。 最终标记 处理剩余的SATB记录修正标记结果相比CMS处理的更少。 会STW耗时较短取决于并发标记期间的对象变动量。 筛选回收 最终标记完成后根据停顿目标选择回收价值高的Region将其存活对象复制到空闲Region复制算法。 也称为混合收集会造成STW耗时主要取决于存活对象数量和region选择策略。 并发标记阶段采用三色标记结合SATBSnapshot-At-The-Beginning算法通过写屏障记录引用变化。  注意并不是操作所有老年代数据复制而是为了达成停顿目标动态选择效率最高的老年代对象处理。如果对象不多又可以满足最大停顿时间那么它会把所有老年代复制。 SATB 目的是解决并发标记期间的对象漏标问题。 在初始化时对整个堆建立快照在并发标记期间对发生修改的对象记录到SATB队列最终标记阶段处理SATB队列确保标记完整性。 ‌Card Table卡表 一种‌底层数据结构‌用于‌粗略跟踪跨代引用‌。早期的跨代引用优化处理。 将堆内存划分为固定大小的‌卡页‌每个卡页对应卡表中的一个‌条目。当老年代对象引用新生代对象时标记对应卡页为‌脏页‌Dirty Card表示需扫描该区域。 在垃圾回收时避免全堆扫描仅扫描脏页以找到跨代引用。 维护成本较低仅标记脏页但扫描脏页时需遍历卡页内所有对象。 Remembered Set记忆集 目的是解决跨Region引用问题避免全堆扫描。 一般称为RSet是一种‌高级抽象结构‌用于‌精确记录跨Region引用‌。基于卡表实现但功能更精确。避免跨Region引用扫描直接读取RSet中的引用扫描效率更高。 所以Rset和卡表是协作关系Rset在其基础上做了升级优化。 实现在G1中每个Region维护一个RSet记录其他Region中指向本Region的‌指针位置‌。 1写屏障。当对象ARegion X引用对象BRegion Y时触发写屏障标记对象A所在卡页为脏页。 2并发优化线程。定期处理脏页解析其中的跨Region引用并将‌具体引用地址‌记录到目标Region的RSet中。 3GC阶段使用Rset。回收Region时直接查询其RSet仅扫描相关卡页中的引用避免遍历全堆。 优点写屏障的轻量化仅标记卡表保证应用线程性能Refinement线程的并发处理避免GC停顿过长。 场景 适用于堆内存较大4G建议6G要求低延迟业务场景页面实时系统以及对象生命周期分布不均匀场景。 不适用于堆内存较小2G堆内存小使用Serial或Parallel更高效不适用超大堆内存≥16GB。 6ZGC ZGCZ Garbage Collector是并发低延迟的垃圾回收器属于全堆收集器多线程并发执行通过参数 -XX:UseZGC 启用。 在JDK 11引入初始版本JDK11至JDK14使用需解锁实验室选项开启JDK15正式启用。 ZGC的设计目标是低延迟、高吞吐量、停顿时间与堆大小无关、支持动态调整堆大小。其专为超大堆内存设计核心理念是全程并发。 算法是染色指针 读屏障。并发阶段采用三色标记结合染色指针Colored Pointers通过读屏障处理并发标记。 染色指针 核心特性之一在指针中嵌入元数据‌元空间与对象地址分离‌。 通过指针标记对象状态存活、可回收等‌无需对象头‌。可快速实现并发标记与转移无需STW暂停。 读屏障 当应用线程从堆中读取对象引用时触发。 检查指针的染色位若对象正在被转移触发‌自愈‌机制自动更新引用到新地址。以保证应用线程始终访问有效对象‌无需暂停‌。 内存多重映射 原理是将同一物理内存映射到多个虚拟地址空间如Marked0和Marked1视图。 目的是支持染色指针快速切换标记状态无需复制内存。 适用于堆内存 ≥ 8GB最佳实践为16GB、要求‌极致低延迟‌金融、大规模微服务、堆内存动态变化频繁等场景。 优点ZGC通过‌染色指针‌和‌全程并发‌设计在超大堆场景下实现了‌亚毫秒级停顿‌是Java应对现代高并发、低延迟需求的终极方案。 缺点 ‌内存开销‌染色指针和元数据占用约3%~5%额外内存。 ‌CPU开销‌读屏障和并发处理需更多CPU资源建议多核环境。 7Shenandoah Shenandoah读音深圳do啊是并发低延迟垃圾回收器属于全堆收集器多线程并发执行通过参数 -XX:UseShenandoahGC 启用。 算法是并发复制 读屏障‌。在JDK12中正式引入JDK15优化性能。 与ZGC类似但实现方式不同无染色指针依赖Brooks指针。并发标记与整理均依赖三色标记使用读/写屏障实现高并发。 同样适合大堆内存、低延迟需求。有两个关键技术。 并发压缩在对象存活时移动并更新引用。 Brooks指针‌每个对象头存储转发指针指向复制后的新地址。 在堆内存 ≥16GB时可选择ZGC或Shenandoah追求亚毫秒级停顿。如果时 TB 级别的堆内存优先使用JDK 15版本的ZGC生产已验证。 4垃圾回收 垃圾回收具体处理及GC调优。 1Minor GC 新生代Eden区空间不足时触发。 当new一个新对象默认会放到 Eden 区Eden区内存不足时触发垃圾回收。 不同的垃圾回收器有不同处理以Java 8的Par为例第一次GC会标记Eden存活对象复制到To区且对象寿命加1。第二次GC会标记Eden区和From区存活对象复制到To区。对象寿命达到阈值后晋升至老年代。 频繁发生Minor GC时可能是新生代内存设置较小或有无用对象无法回收。 2Mijor GC 老年代内存不足时处理对整个堆进行回收通常伴随长时间STW通常也称其为Full GC。 以Java 8的Par处理器为例会使用标记-整理算法清除无用对象GC后内存还不足时抛出内存溢出程序终止。 频繁Mijor GC时可能是老年代分配过小或整个堆内存较小。 问题排查创建了很多大对象全部晋升到了老年代发生了内存泄漏问题代码bug等手动gc。 3Full GC 一般情况下指的就是Mijor GC老年代垃圾回收但不全面因为方法区也会导致Full GC。 方法区动态加在类过多或内存泄漏触发的Full GC和堆内存触发的是同一个都是由垃圾回收器执行全面的垃圾回收包含方法区和堆内存。 通常会伴随长时间STW是比较严重的问题。不同的垃圾回收器会有不同的优化方案。 4GC调优 GC调优时首先检查导致GC的问题以及想要实现的目标高吞吐量或低延迟等。 调优方法大致有垃圾回收器选型、内存、锁竞争、CPU占用、响应时间等。 使用 jstat、jmap、jconsole、VisualVM 等工具分析内存和 GC 日志。 1新生代调优 新生代的对象特点是内存分配廉价对象操作频繁大部分对象是用完就消除。 Minor GC时间短所以一般调优会从新生代开始。 新生代内存不能太大或太小取一个中间的合理值。 太小会导致创建对象内存不够用频繁minor gc太大会导致内存空闲老年代内存紧张从而导致Full GC更严重。 建议设置 25 新生代 50 的堆内存总量足够对象的日常创建销毁。 并发场景下考虑最大并发量时占用的内存是否会超过新生代内存如果不超过则可能不会或较少的触发垃圾回收。 新生代中的幸存区需要能够保留当前活跃对象及需要晋升的对象。如果幸存区内存过小JVM会动态调整晋升阈值可能会导致部分不活跃对象提前晋升到老年代。 对象提前晋升到老年代后只有等到Full GC的时候才能被回收等于是延长了存活短对象的生存周期从而占用内存空间。 另一方面有时候可能又需要将一些对象提前晋升老年代如果有大量存活对象未被晋升那么会留在幸存区中不停的复制移动浪费系统性能。 所以实际中还是根据业务场景项目体量具体考虑优化方案。 2老年代调优 一般不涉及老年代调优理论上是越大越好取决于系统内存。 如果频繁Full GC的话考虑增加老年代内存或减少对象晋升搭配新生代以及排查内存泄漏问题。或考虑使用并发吞吐量或低延迟垃圾回收器。 六面试问题 结合上述所有JVM结构等知识进一步扩展的JVM面试相关问题大致回答清晰即可。 1对象创建过程 通过 new 创建一个对象JVM都做了什么 1首先会进入类的加载过程通过类加载器及双亲委派模型尝试从缓存中常量池中获取已加载过的类如果找不到则重新加载并实例化类在加载阶段分配堆内存空间并生成初始Class对象。 2获取到已经加载过的类之后会在堆内存分配空间。根据垃圾回收机制不同有两种内存分配方式指针碰撞serial、ParNew、G1和空闲列表CMS。 3分配内存后会将对象存入新生代的Eden区这个过程如果是多线程并发情况下可能会发生JVM内存的抢占另一个面试题可引出。 4对象存入Eden区之后会进入类加载的准备阶段为类对象设置默认值可引出final修饰。 5对象头设置对象头包括GC的分代年龄、锁升级标识、HashCode。 6最后触发类的初始化执行 cinit 方法随后执行 init 方法。init也是自动生成的一个方法用来对非静态变量赋值在cinit之后执行。 2对象内存分配方式 注意不要讲成对象的内存布局尽量精准回答。 类加载阶段获取到缓存中的类之后会在堆中分配内存。根据内存是否规整区分为两种分配方式。 堆内存是否规整是由垃圾收集器是否带有压缩整理功能决定的。 1内存规整 堆内存规整的情况下已使用的内存在一边未使用内存在一边。对应标记-整理算法。 该场景下使用指针碰撞方式分配内存。 已使用和未使用的内存中间存放一个分界指示器分配内存时指针会向未使用内存移动待分配对象大小的位数用来存放对象。 这种方式内存使用充分但是开销较大在垃圾回收后需要整理压缩内存便于后续使用。  2内存不规整 内存不规整时JVM内部维护了一个记录可用内存块的列表。在分配对象时找一块足够容纳待分配对象的空间划分给对象实例。 该方式称为空闲列表方式对应标记清除-方式。 该方式性能较好垃圾回收后无需整理。但可能导致内存浪费使用不充分大内存存小对象。 3内存抢占问题 多线程并发环境下无论通过哪个内存分配方式多个对象可能会指向同一块内存地址即发生内存抢占问题。 JVM针对内存线程安全问题提供了两种解决方式CAS 和 TLAB这两种方式可以共同使用协作处理。 TLAB 线程本地分配缓冲区JDK 8默认开启。默认占Eden区的1%可通过参数调整。 每个线程在堆内存中预先分配一小块内存当线程要分配内存时优先在本地缓冲区分配。本地缓冲区用完之后会重新申请新的缓冲区。 CAS 当本地缓冲区分配失败或对象大小超过设置的缓冲区大小时会通过CAS分配内存。性能相比TLAB较差。 4对象的内存布局 一个对象在JVM中是如何存储的或者如何计算对象大小。 在Hotspot虚拟机中对象在Java内存中的存储布局可分为三块三块数据相加就是对象的大小。 1对象头区域 Mark Word锁升级的状态、对象的HashCode、GC的分代年龄、是否偏向锁和锁标志位。 类型指针存在方法区的 KlassInstance 中通过类型指针可以获取到类的信息并实例化。 数组长度只有数组对象才会有。 2实例数据区域 实例数据是指我们代码中定义的字段内容例如属性、字段等。 3对齐填充区域 添加占位符起占位作用保证对象的大小必须是8字节的整数倍因为Hotspot要求对象的起始地址必须是8字节的整数倍且对象头部分正好是8字节的倍数。 如果不对齐填充会导致对象头中有空位从而导致其他对象可能存进来数据错乱。并且8的倍数也会提高运行速度。 5三色标记算法 在传统的GC过程中通过可达性分析算法标记存活对象再执行垃圾回收此时程序会STW暂停用户线程只执行垃圾回收线程效率低且用户体验不好。 三色标记算法是在原有的垃圾回收器上升级将STW变为并发标记减少停顿时间。程序一边运行一边标记垃圾。并且做了优化避免重复扫描对象提升标记阶段的效率。 在传统标记中对象引用不会发生改变不会有问题但是在并发标记时对象间的引用可能发生改变可能会出现错标和漏标的情况。 用到三色标记的垃圾回收器CMS、G1、ZGC、Shenandoah。 1三色 三色标记算法也是根据可达性分析从GC Roots开始进行遍历访问只是在遍历对象过程中做了优化按照【是否检查过】这个条件将对象标记成三种颜色。 白色该对象没有被标记表示垃圾对象。灰色该对象已经被标记过了但该对象下的属性如A引用B没有全被标记完GC会继续向下寻找垃圾。黑色该对象已被标记过且该对象下所有属性也都被标记过了。 可以理解为三种颜色是三个集合分别将对象放入不同集合。初始都是白色发生垃圾回收且找完之后就会清空白色。灰色可以理解为中转站最后结束时灰色一定是空的。 2浮动垃圾 在一个GC的并发标记过程中一个对象已经被标记为黑色或灰色但并发修改导致变成了垃圾白色。但此时不会对标记过的对象重新扫描所以不会发现。 那么此时这个对象即不会被清除也不会被重新找到就称为浮动垃圾。 浮动垃圾对系统的影响不大在下一次GC时会被同样处理。 3对象漏标问题 简答来说就是需要用的对象被回收。也是由于并发导致可使用写屏障技术记录引用变化解决。 在一个GC的并发标记的过程中一个业务线程将一个白色对象引用断开置为垃圾同时有一个黑色对象引用这个对象新增引用这两个顺序可以互换也可以先引用再断开。 此时GC Roots不会再去黑色节点下面找但又因为其是白色标记。所以会导致出现既被需要使用又会被垃圾回收。导致系统出现问题。 CMS和G1垃圾回收器都针对该问题做了应对CMS增加引用环节确认标记G1增加SATB算法确认并发期间修改的标记。 6垃圾回收器选择 问到项目线上使用什么垃圾回收器可以先大概描述下常见的回收器然后说有缺陷。 新生代收集器Serial、ParNew、Parallel Scavenge。 老年代收集器Serial Old、CMS、Parallel Old。 全堆收集器G1、ZGC、Shenandoah。 在实际使用时会根据项目的JDK版本以及业务的类型综合考虑垃圾回收器。 JDK8常用的有两种组合ParNew CMSParallel Scavenge Parallel Old。这两种组合的新生代处理基本一样但是对于老年代CMS组合使用标记清除方式性能及响应速度会更好一点。 所以如果我的项目是ToB的或者堆内存是4G以下的可以选择JDK8默认的Parallel组合。 如果项目是ToC的用户或者堆内存是4-8G更大一点可以选择CMS方式。 然后G1是JDK 9的默认回收器Java 8需要显式开启使用。G1回收器对于内存大且需要更快的响应速度时是很好的选择。 最后对于ZGC、Shenandoah这两个垃圾回收器要确认自己的项目体积内存已达到十几G或T级别并且追求毫秒级的停顿时可以考虑。一般用到这两个回收器的都是中大型项目了如果体量下用了可能反而不如其他回收器。 7逃逸分析 还有另一种提问方式对象一定分配在堆中吗这个一定要回答不一定然后描述逃逸分析。 逃逸分析 编译期间JIT做的优化功能用于减少堆内存分配压力。 简单来说就是在方法中创建的对象其指针有可能被返回或者被全局引用然后被其他方法或线程引用这种现象就称为引用的逃逸。 JVM 的‌逃逸分析‌会尝试将未逃逸的对象分配在栈上即方法中创建的对象没有传递出去。 但是如果创建的对象过大使得无法分配在栈上时栈内存不够会正常存储到堆中。 好处 栈上分配。未逃逸的对象会随着栈帧出栈而销毁减轻垃圾回收的压力。 同步消除。如果确定一个变量不会方法逃逸那么它在多线程环境下是安全的不需要考虑变量的线程安全问题避免同步开销。 标量替换。未方法逃逸的变量栈可以使用内存碎片进行存储将其变量恢复为原始类型访问提升速度和性能。
http://www.pierceye.com/news/205313/

相关文章:

  • 龙华网站建设-信科网络电子商务网站建设和技术现状
  • 网站备案有效期wordpress 评论图片
  • 搭建网站需要哪些步骤wordpress 主题使用
  • 网站怎么发布做微商天眼官方网站
  • qq群网站制作异常网站服务器失去响应
  • aspnet网站开发模板紫光华宇拼音输入法官方下载
  • 东莞网站设计价格wordpress的配置dns
  • 韶关网站建设公司电子商务网站建设考试重点
  • 网站左侧 导航小红书广告投放平台
  • 资阳住房和城乡建设厅网站重庆建设网站建站
  • 网站制作厂家电话多少女生学网络工程难吗
  • 网站建设要经历哪些步骤?网站建设岗位周计划
  • 贵阳网站制作工具福步外贸论坛网首页
  • 网站大全app下载任务发布平台
  • 专业商城网站建设哪家便宜河南做外贸网站的公司
  • seo博客网站东莞网络推广运营企业
  • 定制网站建设公司哪家好嘉兴网站建设多少时间
  • 快三竞猜网站建设wordpress 整站打包
  • 珠海好的网站制作平台微信音乐音频怎么关闭
  • asp.net 网站计数器响应式设计
  • 2017做那些网站致富小程序商城哪个平台好
  • 织梦制作网站如何上线做网站 当站长
  • 如何知道一个网站是用什么做的树莓派搭建wordpress
  • 怎么制作网站登录电子商务网上购物网站建设规划
  • 大连外贸网站制作做文案公众号策划兼职网站
  • 400网站建设推广通王网站内容管理系统
  • 上海专业网站制作开发wordpress 一级目录下
  • 要查询一个网站在什么公司做的推广怎么查济南集团网站建设报价
  • 手机静态网站建设课程设计报告形象型网站
  • 网站建设接单渠道百度网站内容