JVM
-
堆和栈的区别
- 堆 :
- 运行时确定内存大小。
- 由 JVM 管理。
- 是一块一块的内存。
- 线程共享。
- 栈 :
- 编译时即可确定内存大小。
- 线程私有。
- 实现方式采用数据结构中的栈实现,具有先进后出的顺序特点。
- 在分配速度上比堆快,分配一块栈内存不过是简单的移动一个指针。
- 堆 :
-
JVM 主要组成部分
- 组成部分 :
- 类加载器(
ClassLoader
) - 运行时数据区(
Runtime Data Area
) - 执行引擎(
Execution Engine
) - 本地库接口(
Native Interface
)
- 类加载器(
- 作用 :
- 首先通过类加载器(
ClassLoader
)将 Java 代码转为字节码,运行时数据区(Runtime Data Area
)再把字节码加载到内存中。而字节码文件只是 JVM 的一套指令规范,并不能直接交给底层操作系统去执行,因此需要特定命令解析器执行引擎(Execution Engine
),将字节码翻译成底层系统指令,再交给 CPU 执行,而这个过程中需要调用其他语言的本地接口(Native Interface
)来实现整个程序的功能。
- 首先通过类加载器(
- 组成部分 :
-
JVM 内存结构
- 程序计数器(
Program Counter Register
) :- 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己的独立的程序计数器。当线程执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
- Java 虚拟机栈(
Java Virtual Machine Stacks
) :- 每个 Java 线程都有一个私有的 Java 虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
- 本地方法栈(
Native Method Stack
) :- 本地方法栈与 Java 虚拟机栈类似,但它为本地方法(
Native Method
)服务。本地方法是用其他编程语言(如 C、C++)编写的,通过 Java Native Interface(JNI)与 Java 代码进行交互。
- 本地方法栈与 Java 虚拟机栈类似,但它为本地方法(
- Java 堆(
Java Heap
) :- Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代(
Young Generation
)和老年代(Old Generation
)等不同的区域,其中新生代又包括 Eden 空间、Survivor 空间(From 和 To)。
- Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代(
- 方法区(
Method Area
) :- 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区也被称为永久代(
Permanent Generation
),但在较新的 JVM 版本中,永久代被元空间(Metaspace
)所取代。
- 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区也被称为永久代(
- 运行时常量池(
Runtime Constant Pool
) :- 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。与 class 文件中的常量池不同,运行时常量池可以动态改变。
- 字符串常量池 :
- 字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(
String
)专门开辟的一块区域,主要是为了避免字符串的重复创建。
- 字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(
- 直接内存(
Direct Memory
) :- 直接内存不是 JVM 的一部分,但也与内存管理有关。Java NIO 库允许直接分配堆外内存,这些内存不受 Java 堆大小的限制,也不受垃圾回收器管理。直接内存通常是通过
ByteBuffer
类来使用的。
- 直接内存不是 JVM 的一部分,但也与内存管理有关。Java NIO 库允许直接分配堆外内存,这些内存不受 Java 堆大小的限制,也不受垃圾回收器管理。直接内存通常是通过
- 程序计数器(
-
四种引用:强引用、软引用、弱引用、虚引用分别介绍
-
强引用(
Strong Reference
) :- 强引用是 Java 中最常见的引用类型。如果一个对象具有强引用,即使系统内存不足,垃圾回收器也不会回收这个对象,只有在不再有任何强引用指向这个对象时,才会被回收。
-
软引用(
Soft Reference
) :- 软引用用于描述一些还有用但非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾回收器会尝试回收这些对象。软引用通常用于实现内存敏感的缓存,可以在内存不足时释放缓存中的对象。
-
弱引用(
Weak Reference
) :- 弱引用比软引用的生命周期更短暂。如果一个对象只有弱引用指向它,在进行下一次垃圾回收时,不论系统内存是否充足,这些对象都会被回收。弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
-
虚引用(
Phantom Reference
) :- 虚引用是 Java 中最弱的引用类型。如果一个对象只有虚引用指向它,那么无论何时都可能被垃圾回收器回收,但在对象被回收之前,虚引用会被放入一个队列中,供程序员进行处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。
-
示例代码 :
javaObject obj = new Object(); // 强引用 SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用 WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用 ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // 虚引用
-
-
怎么判断对象是否可以被回收
- 引用计数法 :
- 给对象中添加一个引用计数器。每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1。任何时候计数器为 0 的对象就是不可能再被使用的。但是这样有一个问题,不能解决循环引用的问题。
- 可达性分析算法 :
- 这个算法的基本思想是通过一系列的称为 "GC Roots" 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,即证明此对象是不可用的,即可被回收。
- 引用计数法 :
-
垃圾回收算法、垃圾回收机制
- 垃圾回收算法 :
- 标记 - 清除算法 :
- 标记 - 清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点(
GC Roots
),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。 - 适用场景:存活对象较多的情况下比较高效,适用于年老代(即旧生代)。
- 缺点:
- 容易产生内存碎片,当再来一个比较大的对象时,可能会因为找不到足够大的连续空间而提前触发垃圾回收。
- 需要扫描整个空间两次:第一次标记存活对象,第二次清除没有标记的对象。
- 标记 - 清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点(
- 复制算法 :
- 从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到另一块新的内存上,之后将原来那一块内存全部回收掉。
- 现在的商业虚拟机都采用这种收集算法来回收新生代。
- 适用场景:存活对象较少的情况下比较高效。
- 优点:
- 只需要扫描整个空间一次,标记存活对象并复制移动。
- 缺点:
- 需要两块内存空间。
- 需要复制移动对象。
- 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
- 标记 - 整理算法 :
- 标记 - 整理算法是一种老年代的回收算法,它在标记 - 清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
- 分代收集算法 :
- 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记 - 整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(
Tenured Generation
)和新生代(Young Generation
)。在不同年代使用不同的算法,从而使用最合适的算法。新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记 - 清除或者标记 - 整理算法。
- 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记 - 整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(
- 标记 - 清除算法 :
- 垃圾回收机制 :
- 年轻代 :
- 年轻代分为
Eden
区和Survivor
区(两块:From
和To
),且Eden:From:To = 8:1:1
。 - 新产生的对象优先分配在
Eden
区(除非配置了-XX:PretenureSizeThreshold
,大于该值的对象会直接进入老年代)。 - 当
Eden
区满了或放不下了,这时候其中存活的对象会复制到From
区。这里需要注意的是,如果存活下来的对象From
区都放不下,则这些存活下来的对象全部进入老年代。之后Eden
区的内存全部回收掉。 - 之后产生的对象继续分配在
Eden
区,当Eden
区又满了或放不下了,这时候将会把Eden
区和From
区存活下来的对象复制到To
区(同理,如果存活下来的对象To
区都放不下,则这些存活下来的对象全部进入老年代),之后回收掉Eden
区和From
区的所有内存。 - 如上这样,会有许多对象会被复制很多次(每复制一次,对象的年龄就 +1),默认情况下,当对象被复制了 15 次(这个次数可以通过
-XX:MaxTenuringThreshold
来配置),就会进入老年代。 - 当老年代满了或者存放不下将要进入老年代的存活对象的时候,就会发生一次
Full GC
(这也是我们最需要减少的,因为耗时很严重)。
- 年轻代分为
- 垃圾回收的两种类型 :
- Minor GC:对新生代进行回收,不会影响到老年代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
- Full GC :也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于 Full GC 需要对整个堆进行回收,所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。导致 Full GC 的原因包括:老年代被写满、永久代(
Perm
)被写满和System.gc()
被显式调用等。
- 总结 :
- 年轻代:复制算法。所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速地收集掉那些生命周期短的对象。
- 老年代:标记 - 清除或标记 - 整理。在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。
- 年轻代 :
- 垃圾回收算法 :
-
垃圾回收器
- 常见的垃圾收集器 :
- 新生代的收集器 :
- Serial :串行收集器 - 复制算法
Serial
收集器是新生代单线程收集器,优点是简单高效,是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。Serial
收集器依然是虚拟机运行在 Client 模式下默认新生代收集器,对于运行在 Client 模式下的虚拟机来说是一个很好的选择。
- ParNew :并行收集器 - 复制算法
ParNew
收集器是新生代并行收集器,其实就是Serial
收集器的多线程版本。- 除了使用多线程进行垃圾收集之外,其余行为包括
Serial
收集器可用的所有控制参数、收集算法、对象分配规则、收集策略等都与Serial
收集器完全一样。
- Parallel Scavenge :并行收集器 - 复制算法
Parallel Scavenge
收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。- 该收集器的目标是达到一个可控制的吞吐量(
Throughput
)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- Serial :串行收集器 - 复制算法
- 老年代的收集器 :
- Serial Old :串行收集器 - 标记整理算法
Serial Old
是Serial
收集器的老年代版本,它同样是一个单线程的串行收集器,使用标记整理算法。- 这个收集器的主要意义也在于给 Client 模式下的虚拟机使用。如果在 Server 模式下,主要有两大用途:
- 在 JDK 1.5 以及之前的版本中与
Parallel Scavenge
收集器搭配使用。 - 作为 CMS 收集器的后备预案,在并发收集发生
Concurrent Mode Failure
时使用。
- 在 JDK 1.5 以及之前的版本中与
- Parallel Old :并行收集器 - 标记整理算法
Parallel Old
是Parallel Scavenge
收集器的老年代版本,使用多线程和 "标记 - 整理" 算法。这个收集器在 JDK 6 中才开始提供。
- CMS :并发标记清除收集器 - 标记清除算法
CMS
(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。- 前很大一部分的 Java 应用集中在互联网网站或者 B/S 系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS
收集器就非常符合这类应用的需求。 CMS
收集器是基于 "标记 - 清除" 算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为 4 个步骤:- 初始标记
- 并发标记
- 重新标记
- 并发清除
- 其中,初始标记、重新标记这两个步骤仍然需要 "Stop The World"。
CMS
收集器的主要优点:- 并发收集
- 低停顿
CMS
收集器的三个明显的缺点:CMS
收集器对 CPU 资源非常敏感。CPU 个数少于 4 个时,CMS
对用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为 "增量式并发收集器" 的CMS
收集器变种。CMS
收集器无法处理浮动垃圾,可能会出现 "Concurrent Mode Failure" 失败而导致另一次Full GC
的产生。在 JDK 1.5 的默认设置下,CMS
收集器当老年代使用了 68% 的空间后就会被激活。CMS
是基于 "标记 - 清除" 算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发Full GC
。
- Serial Old :串行收集器 - 标记整理算法
- 新生代和老年代垃圾收集器 :
- G1 收集器 :标记整理算法
- JDK 1.7 之后全新的回收器,用于取代
CMS
收集器。 G1
收集器的优势:- 独特的分代垃圾收集器,分代 GC:分代收集器,同时兼顾年轻代和老年代。
- 使用分区算法,不要求 Eden、年轻代或老年代的空间都连续。
- 并行性:收集期间,可由多个线程同时工作,有效利用多核 CPU 资源。
- 空间整理:收集过程中,会进行适当对象移动,减少空间碎片。
- 可预见性:
G1
可选取部分区域进行收集,可以缩小收集范围,减少全局停顿。
G1
收集器的阶段分为以下几个步骤:- 初始标记:它标记从 GC Roots 开始直接可达的对象。
- 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象。
- 最终标记:标记那些在并发标记阶段发生变化的对象,将被回收。
- 筛选回收:首先对各个 Region 的回收价值和成本进行排序,根据用户所期待的 GC 停顿时间指定回收计划,回收一部分 Region。
- JDK 1.7 之后全新的回收器,用于取代
- G1 收集器 :标记整理算法
- 新生代的收集器 :
- 总结 :
- 年轻代:复制算法。所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速地收集掉那些生命周期短的对象。
- 老年代:标记 - 清除或标记 - 整理。在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
- 每一种算法都会有很多不同的垃圾回收器去实现,在实际使用中,根据自己的业务特点做出选择就好。
- 常见的垃圾收集器 :
-
JVM 类加载机制
-
类加载机制是 Java 虚拟机(JVM)在运行 Java 程序时负责将类加载到内存中的过程。它包括以下几个步骤:
- 加载(Loading) :
- 在此阶段,类加载器负责查找类的字节码文件,并将其加载到内存中。字节码可以来自文件系统、网络等位置。加载阶段不会执行类中的静态初始化代码。
- 连接(Linking) :
- 连接阶段包括三个子阶段:
- 验证(Verification):确保加载的类文件格式正确,并且不包含不安全的构造。
- 准备(Preparation) :在内存中为类的静态变量分配内存空间,并设置默认初始值。这些变量在此阶段被初始化为默认值,比如数值类型为 0,引用类型为
null
。 - 解析(Resolution):将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。这一步骤可能包括将常量池中的符号引用解析为直接引用。
- 连接阶段包括三个子阶段:
- 初始化(Initialization) :
- 在此阶段,执行类的静态初始化代码,包括静态字段的赋值和静态代码块的执行。静态初始化在类的首次使用时进行,可以是创建实例、访问静态字段或调用静态方法。
- 加载(Loading) :
-
示例代码 :
javapublic class ClassLoadingExample { public static void main(String[] args) { // 步骤 1:加载 MyClass myClass = new MyClass(); // 步骤 3:初始化 System.out.println(MyClass.staticField); // 使用类中的方法 myClass.printMessage(); // 输出:Initialized static method } } class MyClass { // 步骤 2:连接 - 准备 public static String staticField = "Initialized static field"; // 步骤 3:初始化 static { System.out.println("IntializedInitialized static method"); } public void printMessage() { System.out.println("Instance method called"); } }
-
-
双亲委派机制是什么
- 双亲委派机制是 Java 类加载器中采用的一种类加载策略。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
- JVM 中的类加载器 :
- 启动类加载器(
Bootstrap ClassLoader
) :负责加载%JAVA_HOME%/lib
目录下的核心类库(如rt.jar
)。 - 扩展类加载器(
Extension ClassLoader
) :负责加载%JAVA_HOME%/lib/ext
目录下的扩展类库。 - 系统类加载器(
System ClassLoader
) :负责加载用户类路径(classpath
)下的应用程序类。
- 启动类加载器(
- 加载机制 :
- 它们之间的关系是父子关系,启动类加载器是最顶层的类加载器,扩展类加载器是启动类加载器的子加载器,系统类加载器又是扩展类加载器的子加载器。
- 当类加载器接收到类加载的请求时,首先检查该类是否已经被当前类加载器加载。
- 若该类未被加载过,当前类加载器会将加载请求委托给父类加载器去完成。
- 若当前类加载器的父类加载器为
null
,会委托启动类加载器完成加载。 - 若父类加载器无法完成类的加载,当前类加载器才会去尝试加载该类。
- 双亲委派机制的优缺点 :
- 优点 :
- 避免重复加载:由于类加载器直接从父类加载器那里加载类,避免了类的重复加载。
- 提高安全性 :通过双亲委派模型,Java 标准库中的核心类库(如
java.lang.*
)由启动类加载器加载,这样能保证这些核心类库不会被恶意代码篡改或替换,从而提高程序的安全性。 - 保持类加载的一致性:这种方式确保了同一个类的加载由同一个类加载器完成,从而在运行时保证了类型的唯一性和相同性。这也有助于减轻类加载器在处理相互关联的类时的复杂性。
- 缺点 :
- 灵活性降低:由于类加载的过程需要不断地委托给父类加载器,这种机制可能导致实际应用中类加载的灵活性降低。
- 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。
- 优点 :
- 如何破坏双亲委派机制 :
- Tomcat 的 Web 应用加载 :Tomcat 使用
URLClassLoader
,会优先加载指定的类路径。 - Java Agent:通过 Java Agent 技术,可以在 JVM 启动时加载特定的类,从而绕过双亲委派机制。
- Tomcat 的 Web 应用加载 :Tomcat 使用
-
JVM 参数配置
- 堆大小 :
-Xms<size>
:设置 JVM 堆的初始大小。-Xmx<size>
:设置 JVM 堆的最大大小。
- 新生代大小 :
-Xmn<size>
:设置新生代的大小。
- 新生代最小/最大空间 :
-XX:NewSize<size>
:设置新生代最小空间大小。-XX:MaxNewSize<size>
:设置新生代最大空间大小。
- 永久代/元空间大小 :
-XX:MaxPermSize<size>
:设置永久代的最大大小。-XX:MaxMetaspaceSize<size>
:设置元空间的最大大小。
- 线程栈大小 :
-Xss<size>
:设置每个线程的堆栈大小。
- 垃圾回收器配置 :
-XX:+UseParallelGC
:选择垃圾收集器为并行收集器。此配置仅对年轻代有效,即年轻代使用并发收集,而老年代仍旧使用串行收集。-XX:ParallelGCThreads=20
:配置并行收集器的线程数,即同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
- 堆大小 :