赞
踩
JVM整体结构及内存模型
试题回答参考思路:
1、堆结构
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的,存活的对象是应用可以访问的,不会被垃圾回收。
死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
2、永久代(Perm Gen space)
永久代主要存在类定义,字节码,和常量等很少会变更的信息。并且永久代不会发生垃圾回收,如果永久代满了或者超过了临界值,会触发完全垃圾回收(Full Gc)
而在java8中,已经移除了永久代,新加了一个叫做元数据区的native内存区。
3、元空间
元空间和永久代类似,都是对JVM中规范中方法的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存的限制。
类的元数据放入native memory,字符串池和类的静态变量放入java堆中。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
4、采用元空间而不用永久代的原因
为了解决永久代的OOM问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
永久代会为GC带来不必要的复杂度,并且回收效率偏低。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
当对象的所有引用被置为null时,这个对象确实成为了垃圾收集(GC)的候选对象,但垃圾收集器并不会立即释放这个对象所占用的内存。
在Java和许多其他管理内存的编程语言中,垃圾收集的执行并不是立即发生的,它依赖于垃圾收集器的算法和策略以及当前内存使用情况。
垃圾收集器通常在以下几种情况下运行:
因此,即使对象的引用被置为null,也不能保证垃圾收集器会立即运行来清理这些对象。实际上,具体的释放时间取决于垃圾收集器的实现细节和当前的内存状况。
首先,行收集器,也就是Serial GC,它是一个单线程的垃圾收集器。如下图:
这意味着它在执行垃圾收集时,会暂停所有其他应用线程,也就是我们常说的“Stop-the-World”暂停。
行收集器因为其简单和单线程的特性,非常适合于单核处理器或内存资源相对较小的环境中。
它通常用在客户端应用或者较小的服务器上。
然后是吞吐量收集器,也称为Throughput GC或Parallel GC,它使用多个线程来并行地进行垃圾收集。
这种方式可以有效地提高垃圾收集的速度,尤其是在多核处理器的环境下。
吞吐量收集器的目标是最大化应用的运行时间比例,减少垃圾收集占用的时间,从而提高整体的应用吞吐量。
这使得它非常适合那些对吞吐量要求较高的后端服务和大数据处理系统。
总的来说,选择哪种收集器,主要取决于你的应用场景和具体需求。如果是在资源有限或单核CPU的环境下,行收集器可能是个更好的选择。而在多核心和需要高吞吐量的环境下,吞吐量收集器则更加合适。
在Java中,一个对象是否可以被垃圾回收主要取决于是否还有活跃的引用指向这个对象。
具体来说,有几个条件可以使对象成为垃圾回收的候选者:
因此,总结来说,一个Java对象在没有任何有效的引用指向它,或者只通过弱引用或软引用被访问时,就可以被视为垃圾并可能在下一次垃圾收集时被清理掉。这个过程依赖于垃圾收集器的具体实现和触发条件。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
是的,在JVM的早期版本中,永久代是确实存在的,并且确实会发生垃圾回收。
永久代主要用来存储JVM加载的类信息、常量以及方法数据。尽管永久代中的对象通常有较长的生命周期,但它仍然可能涉及垃圾回收,特别是在类卸载的时候或者清理常量池中不再使用的数据时。
这种垃圾回收通常发生在进行全局垃圾收集(Full GC)的时候。
不过,需要注意的是,类的卸载和常量池的清理并不是特别频繁,因为它们的生命周期通常跟应用程序的生命周期一样长。
另外,从Java 8开始,Oracle的JVM不再使用永久代的概念了,转而使用元空间(Metaspace)来替代。
元空间不再使用JVM堆的一部分,而是直接使用本地内存。
在元空间中,仍然会发生垃圾回收,主要是为了清理不再使用的类元数据。
这种变化主要是为了提高性能以及更灵活地管理内存。
所以,如果我们谈论的是Java 8及以后的版本,那么应该关注元空间的垃圾回收,而不是永久代。
DGC叫做分布式垃圾回收。RMI使用DGC来做自动垃圾回收。
因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC使用引用计数算法来给远程对象提供自动内存管理。
分布式垃圾回收的工作原理基本上是通过跟踪网络中对象的引用来实现的。主要有以下几个步骤:
分布式垃圾回收面临的主要挑战是确保效率和准确性,特别是在大规模分布式系统中。延迟、网络分区和节点故障都可能影响DGC的准确性和性能。因此,实现一个高效且可靠的DGC机制是一个具有挑战性的任务,通常需要在系统设计阶段就进行细致的规划和测试。
1:Java虚拟机(JVM)一种用于计算机设备的规范,可用不同的方式(软件或硬件)加以实现。编译虚拟机的指令集与编译微处理器的指令集非常类似。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
Java虚拟机(JVM)是可运行Java代码的假想计算机。只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行。
2:Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现。Java虚拟机有自己想象中的硬件,如处理器、堆栈、寄存器等,还具有相应的指令系统。
3:Java虚拟机规范定义了一个抽象的——而非实际的——机器或处理器。这个规范描述了一个指令集,一组寄存器,一个堆栈,一个“垃圾堆”,和一个方法区。一旦一个Java虚拟机在给定的平台上运行,任何Java程序(编译之后的程序,称作字节码)都能在这个平台上运行。Java虚拟机(JVM)可以以一次一条指令的方式来解释字节码(把它映射到实际的处理器指令),或者字节码也可以由实际处理器中称作just-in-time的编译器进行进一步的编译。
静态变量是类级别的变量,在Java中,它们随类一起加载。关于静态变量何时加载的问题,我们可以这样解释:
静态变量在运行期被加载和初始化。具体来说,它们是在类被Java虚拟机加载到内存中后、类被第一次引用时初始化的。这个过程是在运行时发生的,而非编译期。
完整的Java对象创建过程如下:
让我详细解释一下这个过程:
这个初始化过程确保在任何静态方法或静态变量被访问之前,所有的静态变量已经被设置为指定的初始值。因此,虽然类的结构和静态变量的声明在编译期被确定并存储在类的字节码中,实际上它们的加载和初始化则发生在运行期。这种机制确保了Java类和对象的初始化顺序得到正确的管理和执行。
在Java虚拟机(JVM)中,确实有几种机制可以被视为内部的“缓存”功能,这些功能主要用于提高程序执行的效率和性能。
以下是几个例子:
对于8G内存,我们一般是分配4G内存给JVM,正常的JVM参数配置如下:
-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
调优的话要具体场景具体分析,可以通过监控确认FullGC频率和次数,通过dump文件分析大对象等手段。
可以从代码层面优化或者是调整年轻代、老年代、幸存区的内存大小,来达到调优的目的
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
在 Java 中,堆和栈分别表示两种不同的内存分配方式,各自用于存储不同类型的数据和具有不同的管理机制。
总结:
在 64 位 JVM 中,int 数据类型的长度依然是 32 位(4 字节)。这在 Java 语言规范中有明确规定,不受运行环境的 32 位或 64 位体系结构的影响。
Java 基础数据类型的长度是固定的,独立于硬件架构:
因此,在 64 位 JVM 中,int 仍然保持 32 位(4 字节)。
在 Java 虚拟机 (JVM) 中,Serial 垃圾收集器和 Parallel 垃圾收集器是两种不同的垃圾收集策略,它们的主要区别在于并发性和目标使用场景。
新生代采用复制算法,老年代采用标记-整理算法
新生代采用复制算法,老年代采用标记-整理算法
JVM 选项 -XX:+UseCompressedOops 主要用于 64 位 JVM 上的堆内存优化,简称为“压缩 OOP(普通对象指针)”
64为虚拟机对象头如下:
在 Java 中,可以通过检查系统属性 os.arch 来确定 JVM 是 32 位还是 64 位。该属性表示操作系统的体系结构。一般来说,如果属性值包含“64”,则表示是 64 位 JVM,否则是 32 位。
例如:
public class JVMArchitecture {
public static void main(String[] args) {
String architecture = System.getProperty("os.arch");
System.out.println("JVM Architecture: " + architecture);
if (architecture.contains("64")) {
System.out.println("This is a 64-bit JVM.");
} else {
System.out.println("This is a 32-bit JVM.");
}
}
}
在 Java 中,JVM 最大堆内存的限制取决于虚拟机架构(32 位或 64 位)、操作系统和硬件资源
一般情况下,32 位和 64 位 JVM 的最大堆内存如下:
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
Java 堆空间(Heap)是 JVM 的一部分内存区域,用于存储由程序动态分配的所有对象。
它在 JVM 启动时创建,所有线程共享。垃圾收集器(GC)会在堆空间中自动管理内存,定期清理不再使用的对象。
垃圾收集器是负责自动管理堆空间的内存分配和回收的组件。它的目标是移除不再被使用的对象,释放内存以供其他对象使用。常见的垃圾收集算法和收集器包括:
在 Java 中,不能绝对保证垃圾收集(GC)在特定时间执行。
垃圾收集的行为由 JVM 控制,并根据具体的内存需求和配置参数自动决定何时触发。
虽然我们可以通过配置或提示来增加垃圾收集的机会,但无法严格控制它在特定时间执行。
JVM 会根据应用程序的运行状况和当前的内存使用情况来自动决定最佳的垃圾收集时间。
在 Java 中,可以使用 Runtime 类或 Java Management Extensions (JMX) 来获取 Java 程序使用的内存和堆内存使用的百分比。
Runtime 类提供了获取 JVM 内存使用信息的方法
例如:
public class MemoryUsageExample { public static void main(String[] args) { // 获取当前 JVM 的运行时实例 Runtime runtime = Runtime.getRuntime(); // 获取最大可用内存 long maxMemory = runtime.maxMemory(); // 获取当前已分配的总内存 long totalMemory = runtime.totalMemory(); // 获取当前空闲内存 long freeMemory = runtime.freeMemory(); // 计算已使用的内存 long usedMemory = totalMemory - freeMemory; // 打印信息 System.out.printf("最大堆内存: %.2f MB%n", maxMemory / (1024.0 * 1024)); System.out.printf("已分配的总堆内存: %.2f MB%n", totalMemory / (1024.0 * 1024)); System.out.printf("已使用的堆内存: %.2f MB%n", usedMemory / (1024.0 * 1024)); System.out.printf("堆内存使用百分比: %.2f%%%n", (usedMemory * 100.0) / maxMemory); } }
Java Management Extensions (JMX) 提供了更高级的内存管理信息。可以使用 MemoryMXBean 获取详细的堆和非堆内存使用情况:
import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; public class JMXMemoryUsageExample { public static void main(String[] args) { // 获取 JVM 的内存管理 MXBean MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); // 获取堆内存的使用情况 MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); long maxHeapMemory = heapMemoryUsage.getMax(); long usedHeapMemory = heapMemoryUsage.getUsed(); // 打印堆内存信息 System.out.printf("最大堆内存: %.2f MB%n", maxHeapMemory / (1024.0 * 1024)); System.out.printf("已使用的堆内存: %.2f MB%n", usedHeapMemory / (1024.0 * 1024)); System.out.printf("堆内存使用百分比: %.2f%%%n", (usedHeapMemory * 100.0) / maxHeapMemory); } }
JVM 内存区域大致可以分为两种类型:堆(Heap)和非堆(Non-Heap)内存。
堆内存是 JVM 中的主要内存区域,用于存储 Java 对象实例。堆内存在 JVM 启动时分配,并由垃圾收集器(GC)管理。
非堆内存是 JVM 使用的其他内存区域,包括:
总结起来,JVM 内存区域可划分为堆和非堆两种类型,并进一步细分为不同的区域,用于高效地存储和管理应用程序的不同类型数据。
以下是一些主要的 JVM 关键名词及其解释:
JVM 运行时内存主要划分为多个区域,每个区域用于不同类型的数据和任务。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
在Java中,判断一个对象是否可以被视为垃圾,并由垃圾收集器(GC)回收,主要通过可达性分析(Reachability Analysis)来完成。这个方法不依赖于对象的引用计数,而是看对象是否可以从一组称为“GC Roots”的对象访问到。
假设有以下类结构和对象引用情况:
class A { B bInstance; } class B { int value; } public class TestGC { public static void main(String[] args) { A a = new A(); a.bInstance = new B(); // 情况1 a = null; // 情况2 // a.bInstance = null; } }
在上面的代码中:
当执行 a = null; 后,对象 A 的实例不再有任何栈帧中的引用指向它,变成了不可达状态。由于 A 的实例是唯一引用 B 的实例的对象,因此,B 的实例也会随着 A 的实例变成不可达。在下一次垃圾收集时,这两个对象都可能被回收。
如果只执行 a.bInstance = null;,则只有 B 的实例变成不可达状态,因为不再有引用指向它。A 的实例仍然由局部变量 a 引用,因此它仍然是可达的,不会被回收。
通过这种方式,Java的垃圾收集器可以确定哪些对象是“垃圾”并应当被回收,从而管理内存使用,防止内存泄漏。
例如:
class MyClass { private static B staticObj = new B(); // 类静态属性引用的对象 private int[] numbers = new int[10]; // 实例引用的对象 public static void main(String[] args) { A localA = new A(); // 虚拟机栈中的引用变量 int[] localNumbers = {1, 2, 3}; // 虚拟机栈中的引用变量 localA.doSomething(); } } class A { public void doSomething() { String localStr = "hello"; // 虚拟机栈中的引用变量 System.out.println(localStr); } } class B { private long id; private String value; }
在这个例子中:
通过这些GC Roots作为起点,垃圾收集器可以遍历所有可达的对象。不可达的对象,即那些从任何GC Roots都无法到达的对象,被视为垃圾,可能会在垃圾收集过程中被回收。这种方法确保了只有真正不再被使用的内存会被清理。
在Java虚拟机(JVM)中,对象头(Object Header)是所有Java对象在内存中的重要组成部分。
对象头主要包含两个部分:标记字(Mark Word)和类型指针。对于数组类型的对象,还包括一个长度部分。
标记字是对象头中用于存储对象自身的运行时数据的部分,其大小通常为32位或64位,具体取决于JVM的位数和内存模型。标记字包含了以下信息:
类型指针指向它的类元数据(Class Metadata),即指向描述类(包括类的方法、字段等信息)的数据结构的指针,JVM通过这个指针来确定对象属于哪个类。
对于数组对象,对象头还包含一个长度部分,记录数组的长度。这是因为数组的长度不是固定的,而且Java中数组的长度在创建后不可变,所以存储长度信息对于数组操作非常重要。
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
解决并发问题的方法:
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
JVM的分代收集算法是基于一个核心观察:不同对象的生命周期不同。有些对象很快就变得不可达(如局部变量或临时对象),而有些则可能存活较长时间甚至贯穿整个应用程序的生命周期(如缓存数据或单例对象)。
根据这一观察,JVM的堆内存被分为几个不同的区域,以更有效地管理内存,从而优化垃圾收集性能。
主要包括以下几个部分:
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
JDK 1.8 相较于 JDK 1.7 在JVM方面进行了几项重要的优化和更新:
内存泄漏是指程序在申请内存后,无法适时释放已不再使用的内存。这导致了无用内存的累积,可能最终耗尽系统资源,影响程序或系统的性能。长期运行的系统如果发生内存泄漏,可能导致内存资源枯竭,甚至导致系统崩溃。
例如: 在Java中,内存泄漏常见的情况是集合类持有对象引用而未能释放:
import java.util.*;
public class MemoryLeak {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
Object o = new Object();
list.add(o);
o = null; // 即使设置为null,集合中仍然持有对象引用
}
}
}
在这个示例中,虽然对象o被设置为null,意图释放引用,但由于ArrayListlist仍然持有所有创建的对象引用,这些对象不能被垃圾回收,从而导致内存泄漏。
内存溢出是指程序在运行过程中,因为需求的内存超过了系统可供分配的最大内存,导致申请新内存失败。这通常会引发错误或异常,如Java中的OutOfMemoryError。
例如: 在Java中,内存溢出的一个常见例子是请求的数据量超过了JVM允许的堆大小限制:
public class OutOfMemory {
public static void main(String[] args) {
int[] bigArray = new int[Integer.MAX_VALUE]; // 尝试分配巨大的数组空间
}
}
这段代码尝试创建一个非常大的数组,如果JVM的最大堆内存设置不足以存储这么多数据,将抛出OutOfMemoryError。
内存泄漏的发生方式可以按照其产生的原因和上下文进行分类。下面是几种常见的内存泄漏分类及其示例:
发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏
例如:静态字段引用
静态字段的生命周期跟类的生命周期一样长,因此静态字段如果持续持有对象引用,那么这些对象也不会被回收。
public class StaticFieldLeak {
private static List<Object> objects = new ArrayList<>();
public void add(Object object) {
objects.add(object);
}
}
在这个例子中,objects是一个静态字段,它会持续持有所有加入的对象,除非显式清空或者程序结束。
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的
例如:集合对象中的内存泄漏
如果对象被存储在集合中,并且在不需要时没有被移除,这些对象会一直保留在集合中,从而导致内存泄漏。
public class CollectionLeak {
private List<Object> cacheData = new ArrayList<>();
public void processData() {
Object data = new Object();
cacheData.add(data);
// 在适当的时候忘记从cacheData中移除data
}
}
这个示例中,虽然data只在processData方法中需要,但加入到cacheData后,如果没有适当的移除机制,data会一直占用内存。
如果注册了监听器或回调而未适当取消注册,那么这些监听器或回调可能会持续存在,导致所有相关对象无法被垃圾回收。
示例:
public class ListenerLeak {
private final List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
// 忘记提供一个移除监听器的方法
}
如果ListenerLeak的实例不断添加监听器而不提供移除它们的方式,那么这些监听器及其相关的对象都不能被回收。
非静态内部类和匿名内部类会隐式持有对其外部类实例的引用。如果内部类的实例被长时间持有,那么外部类的实例也不能被回收。
示例:
public class OuterClass { private int size; private final String name; public OuterClass(String name) { this.name = name; } public Runnable createRunnable() { return new Runnable() { public void run() { System.out.println(name); } }; } }
在这个例子中,如果Runnable对象被长时间持有(例如,被提交到一个长期运行的线程中),那么由于Runnable隐式地持有对OuterClass实例的引用,OuterClass的实例也会一直保持在内存中。
1**、内存溢出的可能原因 **
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
代码中存在死循环或循环产生过多重复的对象实体;
使用的第三方软件中的BUG;
启动参数内存值设定的过小
**2、内存溢出的解决方案: **
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
**重点排查以下几点: **
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
在Java虚拟机(JVM)中,栈上分配和内存逃逸是两个与对象分配位置和性能优化相关的重要概念。
它们与JVM如何处理对象的内存分配和回收有关。
栈上分配是指在JVM中将对象分配在调用线程的栈内存上,而不是在堆上。这样做的主要优势是提高内存分配的效率和降低垃圾收集的开销,因为栈内存能够随着方法调用的结束而自动清理,不需要单独的垃圾回收过程。
特点:
在Java中,栈上分配并不是默认行为,但JVM的即时编译器(JIT)可以通过逃逸分析技术来实现条件性的栈上分配。
内存逃逸分析是一种编译时优化技术,用于确定对象的作用域和生命周期,从而决定是否可以将对象分配在栈上。如果一个对象在方法外部没有被引用,它就被认为是“不逃逸”的,可以在栈上安全地分配。
如何工作:
例如:
public class EscapeAnalysis {
public static void main(String[] args) {
Point p = new Point(10, 20);
System.out.println("Point: (" + p.x + ", " + p.y + ")");
}
static class Point {
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}
在上面的代码中,Point 对象在 main 方法中创建并立即使用。这个对象没有逃逸出 main 方法的范围,因此理论上可以在栈上分配。
使用JConsole或JVisualVM等JMX工具实时监控Java应用的内存使用情况。这些工具可以帮助开发者观察到内存使用随时间增长的趋势。如果发现内存持续上升且不下降,那么很可能存在内存泄漏。
定期进行代码审查也是发现内存泄漏的一种有效方法。特别关注以下方面:
在代码中使用分析API(如Java的Runtime类),定期打印自由内存和总内存的使用情况,可以帮助开发者了解应用的内存使用模式。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
1. 避免类的重复加载:由于在顶层类加载器已经加载的类不会被子加载器再次加载,因此保证了同一个类在JVM中的唯一性。
2. 安全性:防止核心API被随意篡改。例如,用户不能定义一个称为java.lang.Object的类,因为启动类加载器会首先加载Java的核心类,这样通过双亲委派机制就保证了JVM核心API的类型安全。
3. 稳定性:系统类优先加载,这保证了Java核心库的类不会被自定义的类所替代,系统的稳定性得以提高。
我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:
//ClassLoader的loadClass方法,里面实现了双亲委派机制 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查当前类加载器是否已经加载了该类 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 (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { //不会执行 resolveClass(c); } return c; } }
为什么要设计双亲委派机制?
确定适合服务器配置的 -Xmx(Java堆内存的最大值)需要考虑服务器的核心数和总内存,以及其他因素,如其他应用的内存需求、操作系统占用、JVM内部需求(如元空间、代码缓存等)和具体应用的内存需求。
对于一个4核8GB的服务器:
对于一个8核16GB的服务器:
在确定 -Xmx 值时,理论和实践可能会有所不同。建议在生产环境中进行监控和调整。可以开始于一个较低的设置,根据实际运行中的内存使用情况(通过JVM监控工具)逐步调整,直到找到最佳平衡点。此外,对于不同类型的应用,其内存消耗模式可能会有很大差异,因此应该针对具体应用进行调整和优化。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
Java8默认的垃圾收集器是Parallel Garbage Collector,也称为Parallel GC。
Parallel GC 是一种使用多个线程进行垃圾收集的收集器,主要目的是增加吞吐量。它在新生代使用并行的标记-复制(Mark-Copy)算法,在老年代使用并行的标记-清除-整理(Mark-Sweep-Compact)算法。该垃圾收集器在处理大量数据且拥有多核处理器的服务器环境中表现尤为出色,因为它能充分利用多核硬件的优势。
在Java 8中,虽然默认使用Parallel GC,但可以通过JVM启动参数来切换使用不同的垃圾收集器。例如:
并行垃圾收集,是指使⽤多个GC worker 线程并行地执行垃圾收集,能充分利用多核CPU的能力,缩短垃圾收集的暂停时间。
工作流程如下:
除了单线程的GC,其他的垃圾收集器,比如 PS,CMS, G1等新的垃圾收集器都使用了多个线程来并行执行GC⼯作
在Java虚拟机(JVM)的垃圾收集过程中,“Stop-The-World”(STW)是一个非常重要的概念,它涉及到垃圾收集时对应用程序的影响。
此外,“安全点”(Safepoint)和"安全区域"(Safe Region)是与STW密切相关的机制,用于确保在执行垃圾收集时,应用程序的状态是一致且可预测的。
STW是指在执行垃圾收集过程中,JVM会暂停应用程序的所有线程(除了垃圾收集线程),确保在垃圾收集执行期间不会有任何线程对堆进行修改,从而使得垃圾收集器能在一个一致的内存快照上工作。
STW事件可以由任何类型的垃圾收集引起,无论是Minor GC、Major GC还是Full GC。
影响: STW事件会导致应用程序的暂时性停顿。这些停顿的时间长度取决于堆的大小、堆中对象的数量以及垃圾收集器的类型和配置。STW是影响高性能应用实时响应能力的主要因素之一。
安全点是程序执行中的特定位置,JVM只能在这些点上暂停线程,进行垃圾收集。这些点通常在执行过程中不会改变堆内存的操作,如方法调用、循环迭代、异常跳转等。
选择安全点的原因: 选择这些点是因为在这些位置上,所有对象的引用关系等都处于一种易于管理的、一致的状态。这使得垃圾收集器可以准确地确定对象的可达性,从而正确地进行垃圾回收。
实现: 当JVM发出STW请求时,所有线程将继续执行,直到它们达到最近的一个安全点。一旦所有线程都达到安全点,STW事件就会发生。
安全区域是在程序执行的某个阶段中,线程处于一种“不会引用堆中对象”的状态,或者说所有引用关系在这段时间内不会发生变化。当线程处于这样的安全区域时,即使它没有达到安全点,垃圾收集也可以安全地执行。
应用场景: 安全区域通常用于处理那些不能立即响应STW请求的线程状态,例如线程处于阻塞状态或执行非常长的操作无法达到安全点。
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决。
三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
CMS是一种以获取最短停顿时间为目标的收集器,使用标记-清除算法。其过程可以概括为:
G1收集器旨在提供一个更可预测的垃圾收集行为,适用于大堆内存的系统,它通过划分内存为多个区域(Region)来管理内存,具体步骤包括:
在并发GC环境下,三色标记可能会遇到“漏标”问题,即由于并发修改,一些本应标记的对象被遗漏。为了解决这个问题,CMS和G1都采用了各种策略(如增量更新、原始快照、读写屏障等),以确保在并发修改场景下的正确性。
三色标记算法是现代垃圾收集技术中的一个关键部分,它帮助GC算法在应用程序运行时高效、准确地识别和处理内存中的对象。
String strong = new String("I am a strong reference");
import java.lang.ref.SoftReference;
SoftReference<String> soft = new SoftReference<>(new String("I am a soft reference"));
import java.lang.ref.WeakReference;
WeakReference<String> weak = new WeakReference<>(new String("I am a weak reference"));
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantom = new PhantomReference<>(new String("I am a phantom reference"), queue);
Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。
GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控
Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理
**GC的触发 **
GC的触发通常由以下几种情况:
在Java虚拟机(JVM)中,程序计数器(Program Counter, PC)是一个较小的内存区域,它是JVM中的一种运行时数据区。程序计数器为每个正在执行的线程保留一个独立的空间,因此它也被称为“线程私有”的内存。
程序计数器的主要功能是存储当前线程所执行的字节码的指令地址。如果正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,则程序计数器的值为undefined。这种机制确保了在多线程环境中线程切换后能恢复到正确的执行位置。
假设一个线程正在执行一个Java方法,此方法中包含多条字节码指令。
程序计数器会指向当前正在执行的字节码指令的地址。
当线程执行到新的字节码指令时,程序计数器的值会更新为新指令的地址。如果线程执行的是Native方法,则程序计数器中的值不确定。
Java虚拟机栈(JVM Stack)是Java虚拟机用来存储线程执行Java方法(或称为函数)的内存模型的一部分。每个线程在Java虚拟机中都有自己的虚拟机栈,这个栈与线程同时创建。虚拟机栈的主要作用是管理Java程序中方法的调用和执行。
例如:
public class Example { public static void main(String[] args) { int a = 1; int b = 2; int c = 30; sum(a,b) System.out.println("a: " + a); System.out.println("b: " + b); System.out.println("c: " + c); } public static int sum(int a, int b) { return a + b; } }
在这个例子中,当main方法执行时,一个栈帧被创建并压入虚拟机栈中,用来存储main方法的信息和局部变量。当main方法中调用sum方法时,又一个栈帧被创建并压入栈中来处理sum方法的执行。一旦sum方法执行完毕,其结果被返回到main方法,sum方法的栈帧就从栈中弹出。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
Java本地方法栈(Native Method Stack)是Java虚拟机(JVM)中的一个重要组成部分,它主要用于支持Java的本地方法执行。本地方法是指用Java以外的语言(如C或C++)编写的方法,这些方法通常是为了访问操作系统的底层资源,或者为了性能考虑而实现的。
例如:AtomicInteger的compareAndSet方法就是使用的本地方法栈的方法
AtomicInteger atomicInteger = new AtomicInteger(5);
// 尝试将值从5更新为10
boolean success = atomicInteger.compareAndSet(5, 10);
if (success) {
System.out.println("Update successful, new value: " + atomicInteger.get());
} else {
System.out.println("Update failed, current value: " + atomicInteger.get());
}
JVM的方法区就像是一个图书馆,用来存放Java程序里所有类的信息。想象一下,每个类都是一本书,这些书里记录了类的结构,比如它有哪些方法(就是类里可以做的事情),有哪些变量等等。方法区还存放了这些方法的具体指令,也就是说,当你的程序运行时,它会来这个“图书馆”查找需要的信息。
除此之外,方法区还有一个特别的角落叫做“运行时常量池”,这里面存放的是一些常用的数字、字符串之类的常量信息,方便程序快速使用。
还有,方法区也负责存放静态变量。静态变量不像普通变量那样随着对象的创建而存在,它是跟着类一起加载的,可以被类的所有对象共享。
所以,你可以把方法区看作是存放所有类信息和部分特殊数据的地方,是程序能正确运行不可缺少的一部分。从Java 8开始,原来的方法区有了新名字,叫做元空间,但角色和功能基本没变。
运行时常量池(Runtime Constant Pool)是Java虚拟机(JVM)中的一部分,它属于每个类或接口的一部分。
简单来说,它的作用主要包括以下几点:
运行时常量池是每个类和接口的私有资产,它随着类或接口的加载而创建,在类或接口被卸载时销毁。简而言之,运行时常量池是一个类中固定和动态数据的存储空间,是实现Java代码在虚拟机中高效运行的关键组件之一。
JVM的直接内存(Direct Memory)不是Java虚拟机运行时数据区的一部分,而是在Java堆外分配的内存。这部分内存的使用与JVM的堆内存相互独立,主要有以下几个特点和作用:
总的来说,JVM的直接内存是一种高效的数据处理方式,适用于需要高性能I/O的场景。
然而,它的使用需要谨慎,以避免内存溢出等问题。
堆溢出(Heap Overflow)通常是指在Java虚拟机(JVM)中,当对象的内存分配需求超过了堆内存(Heap)的最大限制时发生的一种错误,这会导致OutOfMemoryError。具体原因可以从以下几个方面来理解:
栈溢出(Stack Overflow)是由于程序执行时栈内存空间被耗尽而引发的错误,常见于Java虚拟机(JVM)中。具体可能导致栈溢出的原因有下面几个可能的原因:
例如:计算阶乘的递归函数,如果没有正确处理递归终止条件,可能导致栈溢出。
java
Copy code
public int factorial(int n) {
if (n == 1) return 1;
else return n * factorial(n - 1);
}
例如:多个方法相互调用,形成长链。
java
Copy code
public void method1() {
method2();
}
public void method2() {
method3();
}
public void method3() {
method1(); // 如果调用关系复杂,可能导致调用链过长
}
例如:在方法内部定义一个大数组。
java
Copy code
public void largeLocalVariables() {
int[] largeArray = new int[100000]; // 大数组占用栈空间
// 使用数组
}
例如:一个方法中有多个大数据类型的局部变量。
java
Copy code
public void largeStackFrame() {
double[] largeDoubleArray = new double[10000];
long[] largeLongArray = new long[10000];
}
例如:在JVM启动参数中设置较小的栈大小,如 -Xss256k。
运行时常量池(Runtime Constant Pool)是每个类或接口的一部分,存储在Java虚拟机(JVM)的方法区内。
运行时常量池主要用于存储编译期生成的各种字面量和符号引用,这包括类和接口的全限定名、字段名、方法名及其他常量。
运行时常量池溢出是指这个存储区域由于存储的数据过多而超过了方法区的限制,导致OutOfMemoryError。
以下是造成运行时常量池溢出的几种原因:
好消息是,自Java 8起,运行时常量池已从永久代(PermGen)移至堆内存中的元空间(Metaspace),因此默认情况下,元空间的大小仅受系统可用内存的限制,运行时常量池溢出的情况有所减少,但在内存受限的环境中仍需谨慎管理内存使用,避免无节制的资源消耗。
方法区(Method Area),在Java虚拟机(JVM)中,主要用于存放已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。
自Java 8起,传统的方法区(有时被称为永久代PermGen)被元空间(Metaspace)所取代。
方法区溢出,或在Java 8之后的版本中称为元空间溢出,主要由以下几个因素引起:
解决方法区溢出的策略包括优化类加载的管理,避免过度使用反射和动态代理,适当增大方法区的大小设置,以及及时检测和处理可能的内存泄漏。对于使用较多第三方库和框架的复杂应用,需要特别注意这些方面的管理。
.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
**划分内存的方法: **
**1、“指针碰撞”(Bump the Pointer)(默认用指针碰撞) **
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
**2、“空闲列表”(Free List) **
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
通常情况下,JVM对于对象内存分配确保了高度的线程安全性
解决并发问题的方法:
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。
在Java虚拟机(JVM)中,对象的内存布局是标准化的,主要由以下几个组成部分构成:
32位虚拟机对象头
64位虚拟机对象头
每个部分都有其特定功能和重要性,合理的布局可以优化对象的存取效率及降低内存的使用。例如:
在Java中,访问对象主要有以下几种方式,这些方式反映了Java虚拟机(JVM)如何从内存中定位和操作对象数据:
例如,HotSpot JVM采用直接指针访问方式,因为它在大多数场景下提供了较好的性能平衡。
ZGC(Z Garbage Collector)是从Java 11开始引入的一种垃圾收集器,主要目标是为大型应用提供低延迟的垃圾回收解决方案。
ZGC设计的主要特点和目标包括:
另外,Oracle官方提到了它最大的优点是:
它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。
由于ZGC的这些特性,它特别适用于需要处理大规模数据且对延迟敏感的应用,如大数据分析、高频交易系统等。不过,由于ZGC在某些场景下可能会增加CPU的负担,并且对系统资源的管理(如内存页的管理)有更高的要求,因此在选择使用ZGC之前需要仔细评估应用的特性和需求。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
以下是一些常用的JVM监控和调试工具:
类加载(Class Loading)是Java虚拟机(JVM)中将类的Class文件加载到内存中的过程,并为之生成对应的java.lang.Class对象。Java的类加载过程涉及到加载、链接和初始化三个主要步骤:
Java的类加载机制是动态的,类在首次被使用时才被加载,这就是Java中所说的“延迟加载”。这种机制帮助减少内存的使用,并且分散了类加载的负载,优化了应用的启动时间。
开发者可以根据需要自定义类加载器,以实现特定的加载策略,比如从加密包中加载类,或者通过网络加载类等。这些自定义的加载器在遵循双亲委派模型(即先委派给父加载器尝试加载,失败后再由自身尝试加载)的前提下,提供了极大的灵活性。
Java内存模型(Java Memory Model,JMM) 是《Java虚拟机规范》中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK5开始 JMM才正真成熟,完善起来。
Java内存模型的主要目的是定义程序中各种变量(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)。每条线程都有自己的工作内存(Working Memory),用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
关于一个变量如何从工作内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成
。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可在分的(对 double 和 long 类型的变量来说,load、store、read 和 write操作在某些平台上允许有例外)。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
JVM(Java虚拟机)对Java的原生锁(synchronized关键字)进行了多种优化以提升性能,主要包括以下几种:
当Java程序出现内存溢出错误时,意味着程序在执行过程中申请的内存超过了Java虚拟机(JVM)分配给它的内存限制。为了排查和解决这个问题,以下是一些常用的排错步骤:
** 1、查看错误信息 **
首先,检查Java程序抛出的异常信息。通常,内存溢出错误会抛出java.lang.OutOfMemoryError异常,可能会伴随有其他的详细信息,如"Java heap space"(Java堆空间)或"PermGen space"(永久代空间)。
** 2、分析堆栈跟踪 **
查看堆栈跟踪以确定哪个部分的代码导致了内存溢出错误。堆栈跟踪将显示代码的调用层次结构,从中可以看到哪些方法在错误发生时被调用。
** 3、检查内存配置 **
确认Java虚拟机的内存配置是否合理。内存溢出错误可能是由于分配给Java堆、栈或永久代的内存不足所致。可以通过修改JVM启动参数中的-Xmx(最大堆内存)和-Xms(初始堆内存)选项来增加可用的内存。
** 4、检查代码逻辑 **
检查代码是否存在内存泄漏的情况。内存泄漏是指程序在不再使用某些对象时未能释放对它们的引用,导致这些对象无法被垃圾回收器回收。常见的内存泄漏情况包括未关闭的文件、未释放的数据库连接、长生命周期的缓存等。使用内存分析工具可以帮助确定是否存在内存泄漏问题。
** 5、调整内存使用 **
如果确认代码逻辑正确且没有明显的内存泄漏问题,可以尝试优化代码以减少内存使用。例如,使用合适的数据结构、及时释放不再使用的对象、避免创建过多的临时对象等。
Java出现内存溢出怎么排错?
** 6、增加硬件资源 **
如果经过以上步骤后仍然无法解决内存溢出问题,可能是因为程序的内存需求超过了系统的硬件资源限制。此时可以考虑增加物理内存或迁移到更高配置的服务器。
** 7、使用内存分析工具 **
Java提供了多种内存分析工具,如VisualVM、Eclipse Memory Analyzer等。这些工具可以帮助识别内存泄漏、查看对象的引用关系、分析内存使用情况等,有助于更深入地排查内存溢出问题。
**总结 **
在处理内存溢出错误时,重要的是要通过分析和排查确定导致问题的根本原因。这需要结合实际情况和调试工具来进行逐步排查,以找到解决方案并确保代码的稳定性和性能
Full GC(全面垃圾收集)是Java虚拟机(JVM)中清理内存的一种方式,它涉及到所有堆区域的清理,包括年轻代、老年代以及永久代(或元空间,取决于使用的JVM版本)。Full GC通常比较耗时,会暂停所有应用线程(即“Stop-The-World”),因此理解何时会触发Full GC对于优化Java应用性能非常重要。以下是一些常见的触发Full GC的情形:
loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
对象的创建
对象创建的主要流程:
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
1、“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
2、“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录
解决并发问题的方法:
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB**),-XX:TLABSize 指定TLAB大小。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
类的生命周期包括这几个部分:
加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
链接,链接又包含三块内容:验证、准备、初始化。
1)验证,文件格式、元数据、字节码、符号引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用初始化,为类的静态变量赋予正确的初始值使用,new出对象程序中使用卸载,执行垃圾回收
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成:
第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)
第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
**32位虚拟机对象头: **
**64位虚拟机对象头: **
判断对象是否存活一般有两种方式:
**1、引用计数: **
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
此方法简单,无法解决对象相互循环引用的问题。
**2、可达性分析(Reachability Analysis): **
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
下面列出一些常用的Java性能调优命令:
Minor GC主要处理Java堆中的年轻代(Young Generation)。年轻代是新生成的对象的主要存放地,这个区域相对较小,因此Minor GC比较频繁但通常很快。
由于年轻代中的对象生命周期通常很短,很多对象在Minor GC后就被清除了,少数存活的对象则会被移动到Survivor区或老年代。
Full GC涉及整个Java堆的清理,包括年轻代、老年代以及元空间(如果使用的是Java 8及以上版本的永久代已被替换为元空间)。Full GC比Minor GC慢得多,且会引发更长时间的停顿。
「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上分配。
**「什么是逃逸分析?」 **
逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
**
「逃逸分析的好处」 **
栈上分配,可以降低垃圾收集器运行的频率。同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同
步。标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。
在Java 8中,Java虚拟机(JVM)用元空间(Metaspace)替换了原先的永久代(PermGen,Permanent Generation)。
这一改变是为了解决永久代存在的几个问题并改进JVM的性能、可扩展性和可维护性。
下面是为什么使用元空间替换永久代的主要原因:
永久代有一个固定的最大内存限制,这意味着所有的类元数据、字符串常量和静态变量都必须在这个固定大小的空间中管理。这种固定大小的设置在很多大型或者动态生成很多类的Java应用中导致了频繁的Full GC和OutOfMemoryError。
由于永久代的大小是固定的,应用如果加载了大量的类或者使用了大量的反射,容易发生永久代内存不足的情况,从而引起内存泄漏。这在长时间运行的应用中尤为常见,例如应用服务器和JEE应用。
永久代是使用标记-整理算法进行垃圾收集的,这意味着清理未使用的类和常量较为复杂和耗时。这增加了Full GC的时间,从而影响到应用的响应时间和吞吐量。
永久代的管理和优化对JVM的开发者来说是一个挑战。它需要JVM维护一个额外的、与Java堆分开的内存区域,这增加了JVM的复杂性和维护难度。
元空间不再使用JVM堆的一部分,而是直接使用本地内存(即操作系统内存),这样做的好处是利用了操作系统本身更成熟的内存管理技术,减少了Java堆的压力,提高了性能。
与永久代不同,元空间的默认大小只受系统可用内存的限制。这意味着元空间可以根据需要动态扩展,减少了因为内存不足导致的系统崩溃的可能性,并可以更灵活地适应不同的应用需求。
元空间的引入简化了Java的内存模型,使开发者更容易理解和优化Java应用的内存使用。
通过使用元空间代替永久代,Java的内存管理变得更加现代化和高效,同时解决了之前由永久代大小固定和难以管理带来的一系列问题。这些优势使得JVM能更好地支持大规模应用和微服务架构。
进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。
在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
这些特定的位置主要在:
1.循环的末尾(非 counted 循环)
2.方法临返回前 / 调用方法的call指令后
3.可能抛异常的位置
这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在
停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。
一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。
当类加载检查通过后,Java虚拟机开始为新生对象分配内存。
如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是 指针碰撞。
如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。
可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存) 。虚拟机通过 -XX:UseTLAB 设定它的。
当 tomcat启动时,会创建几种类加载器: Bootstrap 引导类加载器 加载 JVM启动所需的类,以及标准扩展类(位于 jre/lib/ext 下) System 系统类加载器 加载 tomcat 启动的类,比如bootstrap.jar,通常在 catalina.bat 或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下
tomcat的几个主要类加载器:
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
safepoint又叫安全点
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始
执行 GC,
最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的, [7701页的BAT大佬写的刷题笔记,让我offer拿到手软]
本文,已收录于,我的技术网站 [cxykk.com:程序员编程资料站],有大厂完整面经,工作技术,架构师成长之路,等经验分享
点赞对我真的非常重要!在线求赞,加个关注我会非常感激!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。