JVM
JVM基础:
结构:
- 程序计数器(Program Counter Register):每个线程都有自己的程序计数器,是一个小内存空间。它记录了当前线程执行的字节码的地址,帮助CPU知道下一步要执行的指令是哪个。这也是唯一一个在JVM规范中不报内存溢出的区域。
Java虚拟机栈(Java Virtual Machine Stack):虚拟机栈是线程私有的,他的生命周期和线程的生命周期是一致的。里面装的是一个一个的栈帧,每一个方法在执行的时候都会创建一个栈帧,栈帧中用来存放(局部变量表、操作数栈 、动态链接 、返回地址);在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
- 局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量。底层是变量槽(variable slot)。
- 操作数栈:是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。
- 动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。
- 返回地址(returnAddress):类型(指向了一条字节码指令的地址)JIT即时编译器(Just In Time Compiler),简称 JIT 编译器: 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,比如锁粗化等。
- 本地方法栈(Native Method Stack):与虚拟机栈类似,但用于处理本机方法。Java程序使用JNI(Java Native Interface)调用其他语言(如C/C++)编写的程序,此时本地方法栈会被使用。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
堆(Heap):
- JVM用来存储对象实例和数组,它是所有线程共享的区域,也是垃圾回收的主要区域。
- 开启逃逸分析后,某些未逃逸对象可以通过标量替换在栈上分配。
- 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区。
元空间(MetaSpace):
- 存储已经被java虚拟机加载的类信息、常量、静态变量。
- 元空间不是堆的一部分,而是存放在本机内存中,不受JVM堆内存限制,更好利用内存。
- 减少对应用程序行为依赖,更好管理类及元数据的内存分配,使类加载器更可靠的执行类的动态卸载。
- 利用OS的内存,使得JVM启动阶段和内存管理更为高效,减少了永久代相关的Full GC。
JVM参数:
堆内存设置
-Xms<size>
:设置JVM启动时的初始堆大小(例如,-Xms512m
表示初始堆大小为512MB)。-Xmx<size>
:设置JVM允许的最大堆大小(例如,-Xmx2g
表示最大堆大小为2GB)。-Xmn<size>
:设置年轻代的大小(仅适用于某些垃圾收集器,如Serial, Parallel)。
垃圾回收(GC)相关
-XX:+UseG1GC
:启用G1垃圾收集器。-XX:+UseConcMarkSweepGC
:启用CMS(Concurrent Mark Sweep)垃圾收集器。-XX:+UseParallelGC
:启用并行垃圾收集器(年轻代使用并行收集,老年代使用单线程标记-清除)。-XX:MaxGCPauseMillis=<value>
:设置目标最大GC暂停时间(仅对G1有效)。-XX:+PrintGCDetails
:打印详细的GC日志信息。
性能调优
-XX:PermSize=<size>
和-XX:MaxPermSize=<size>
:设置永久代(方法区)的初始大小和最大大小(在Java 7及之前版本中使用;Java 8之后被Metaspace取代)。-XX:MetaspaceSize=<size>
和-XX:MaxMetaspaceSize=<size>
:设置元空间的初始大小和最大大小(Java 8及以上版本)。-XX:+AggressiveOpts
:开启激进的性能优化选项(包括一些实验性的优化)。
调试与诊断
-XX:+HeapDumpOnOutOfMemoryError
:当发生OutOfMemoryError时自动生成堆转储文件。-XX:HeapDumpPath=<path>
:指定堆转储文件的存储路径。-XX:+PrintFlagsFinal
:打印所有JVM参数的最终值(可用于验证参数是否正确应用)。-verbose:gc
:输出GC简要信息。
其他
-server
或-client
:选择JVM运行模式(-server
模式通常具有更好的性能,但启动较慢;-client
模式启动快但执行效率相对较低)。-D<name>=<value>
:设置系统属性值,可以在程序中通过System.getProperty("<name>")
获取。
OOM:
Java堆空间(Java heap space)
- 当JVM尝试在堆上为新对象分配内存但没有足够的空间时发生。这通常是由于应用程序创建了太多对象而未能及时回收旧对象导致的。
垃圾收集器过度(GC overhead limit exceeded)
- 当JVM花费过多时间进行垃圾回收,并且回收的内存很少时会发生此错误。默认情况下,如果超过98%的时间用于GC并且回收了不到2%的堆,则会触发这个错误。
永久代/元空间不足(PermGen space / Metaspace)
- 在Java 7及之前版本中,使用的是“永久代”来存储类定义及其相关的元数据。而在Java 8及之后版本中,“永久代”被“元空间”取代,它直接使用本地内存。当类加载器加载的类信息超出分配给永久代或元空间的限制时,就会发生此类错误。
方法区内存不足(Method area)
- 方法区用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然现在大多数现代JVM已经转向使用元空间代替方法区,但在某些配置下仍然可能出现与此区域相关的内存问题。
直接缓冲区内存不足(Direct buffer memory)
- 当程序使用
java.nio.ByteBuffer.allocateDirect()
方法分配直接缓冲区,且请求的内存超出了JVM允许的最大值时发生。 - 和普通的ByteBuffer用法相似,使用put和get方法来进行数据写入和读取,但是由于分配的是堆外内存,需要手动进行内存管理。
- 当程序使用
栈溢出(Stack overflow)
- 虽然严格来说这不是一种OOM错误,而是
StackOverflowError
,但它也与内存有关,通常由无限递归调用引起。
- 虽然严格来说这不是一种OOM错误,而是
【tips:虽然理论上可以通过try-catch块捕获OutOfMemoryError
,但由于这种错误往往意味着系统资源已极度紧张,试图恢复可能不是最佳选择,因为后续操作也可能因缺乏足够资源而失败。】
内存分代:
- 优化内存分配:将不同生命周期的对象分开处理,减少老年代碎片化,避免频繁GC。
- 提高垃圾回收效率:不同代可以使用不同的GC方式,如年轻代使用Scanvenge GC,老年代使用GMS或G1等。
- 简化内存管理:年轻代创建销毁频繁,根据对象的不同生命周期特点,JVM可以更灵活地选择垃圾回收策略,例如,年轻代可以频繁进行快速的复制回收,而老年代可以使用更多时间进行标记-清除-压缩等操作以减少内存碎片。
双亲委派:
- 定义:在类加载过程中,先判断是否已经加载,是则直接返回,否则尝试进行加载。加载会将请求委派给父类加载器进行loadClass(),因此所有的请求最终都会传送到顶层的启动类加载器
BootstrapClassLoader
中,父类无法加载才由自己加载,如果父加载器为null也会使用顶层启动类加载器作为父类加载器。 - 优点:可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
破坏:自定义一个类加载器,重写loadClass方法;Tomcat可以加载自己路径下的class文件,而不会传递给父加载器;Java的SPI发起者已经是最上层
BootstrapClassLoader
。- 对Tomcat来说,先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。
- 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。
- 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,而不是使用AppClassLoader 来加载类(这里破坏了双亲委派,但是对于ExtClassLoader双亲委派仍然成立)比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader,这个加载链,就保证了 Object 不会被重复加载。
- 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。
- 加载依然失败,才使用 AppClassLoader 继续加载。
- 都没有加载成功的话,抛出异常。
对象创建:
类加载检查:当执行
new
关键字创建对象时,首先会检查这个类是否已经被加载、解析和初始化。如果目标类还未被加载到JVM中,则需要先进行类加载过程。这包括加载(Loading)、链接(Linking,进一步分为验证Verification、准备Preparation和解析Resolution)和初始化(Initialization)。以下是类加载过程:- 通过类的全限定名来获取该类的二进制字节流:这通常意味着从文件系统读取.class文件,但也可能来自网络、数据库或其他来源。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构:即把类的信息存储到方法区中。
- 在内存中生成一个代表该类的
java.lang.Class
对象,作为方法区这个类的数据访问入口。 - 验证(Verification):确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要包括文件格式验证、元数据验证、字节码验证和符号引用验证。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值。注意这里的初始值通常不是代码中指定的值,而是类型的零值(例如int类型的0,boolean类型的false等)。对于final修饰的基本数据类型常量,如果它们在编译期就可以确定值,则会在准备阶段赋予正确的值。
- 解析(Resolution):将常量池内的符号引用替换为直接引用。符号引用是一个以字符串形式给出的名字,而直接引用可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 初始化:初始化是类加载过程的最后一步,它根据程序中的赋值语句以及静态代码块中的语句为类的静态变量赋予正确的初始值。这是执行类构造器
<clinit>()
方法的过程,这个方法由编译器自动收集类中的所有静态变量赋值动作和静态代码块合并而成。当首次主动使用某个类或接口时,就会触发该类或接口的初始化。
分配内存:
在确认了类已被正确加载后,接下来是在堆内存中为新对象分配空间。分配的内存量等于对象自身大小加上对象头的大小。对象头包含了元数据信息,如哈希码、GC分代年龄、锁状态标志等。
- 内存分配有两种主要方式:指针碰撞(Bump the Pointer),适用于堆内存规整的情况;空闲列表(Free List),适用于堆内存碎片化的情况。
- 初始化零值:内存分配完成后,分配给对象的这块内存空间会被初始化为零值(不包括对象头)。这一步确保了实例字段在没有显式初始化的情况下也能获得一个默认值(例如,数值类型为0,布尔型为false,引用类型为null)。
- 设置对象头信息:初始化完零值之后,JVM会对对象头进行必要的设置。这可能包括记录对象的哈希码、GC信息(比如该对象属于哪个年代)、锁信息等。
- 执行构造方法(
方法) :最后一步是执行构造方法来初始化对象的成员变量,并执行其他初始化操作。当你使用new
关键字并传递参数给构造函数时,这些参数将用于初始化对象的状态。 - 返回引用:构造方法执行完毕后,一个新的对象就被成功创建了。此时,
new
表达式会返回对该对象的引用,可以赋值给一个变量或者作为参数传递给方法等。
堆内存分配:
- 确定对象大小:当一个对象被创建时,首先需要确定该对象所需的内存大小。这取决于对象的字段(包括基本类型和引用类型)以及对象头(Object Header)。对象头通常包含一些元数据信息,如锁状态、哈希码等。
- 检查Eden区空间:大多数情况下,新的对象会首先尝试在年轻代的Eden区进行分配。如果对象所需的空间小于Eden区剩余的可用空间,那么对象将直接分配在此处。
分配对象:
- 指针碰撞(Bump the Pointer):如果堆内存是规整的(即所有存活对象都紧密排列在一起),那么JVM可以通过维护一个指针来追踪下一个可用位置。每当有新对象分配时,只需要移动这个指针即可完成分配。
- 空闲列表(Free List):如果堆内存不是规整的(例如经过多次垃圾回收后产生了碎片),JVM会维护一个记录所有空闲块的列表。当需要分配对象时,从列表中找到合适的块分配给对象。
- 触发垃圾回收:如果Eden区没有足够的空间来分配新对象,JVM会触发一次Minor GC(年轻代垃圾收集)。这次垃圾回收旨在清理Eden区和其中一个Survivor区中的无用对象,为新对象腾出空间。如果经过垃圾回收后,Eden区仍然无法满足分配需求,则可能将部分存活对象提前晋升到老年代。
- 晋升到老年代:对于某些较大的对象,或者经历了几次垃圾回收仍然存活的对象,它们可能会直接或间接地被分配到老年代。老年代用于存放生命周期较长的对象,其垃圾回收频率低于年轻代。
- Full GC:如果老年代也没有足够的空间来容纳从年轻代晋升的对象,或者直接分配的大对象,将会触发Full GC。这是一个相对耗时的过程,因为它涉及整个堆的垃圾回收。
TLAB:
- 定义:TLAB会为每个线程预先分配一块内存空间,线程在分配对象时,会从自己的TLAB中进行分配,而不是直接在堆上进行分配。过小的TLAB可能会导致频繁的垃圾回收,而过大的TLAB可能会浪费内存空间。
优点:
- 减少线程同步:由于每个线程都有自己的TLAB,因此不需要进行线程同步操作,可以减少线程竞争和锁的开销。
- 提高分配速度:由于对象分配是在TLAB中进行的,而不是在堆上进行,因此可以减少对象分配的开销,提高分配速度。
- 提高局部性:由于每个线程都有自己的TLAB,对象分配在TLAB中进行,可以提高局部性,减少对共享内存的访问,提高缓存命中率。
GC:
存活算法和两次标记过程:
引用计数法:
- 给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
- 优点:实现简单,判定效率也很高
- 缺点:他很难解决对象之间相互循环引用的问题,基本上被抛弃
可达性分析法:
- 通过一系列的成为“GC Roots”(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI引用)的对象作为起始点,从这些节点ReferenceChains开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;
两次标记过程:
- 对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。
三色标记:
GC Root节点:
- 局部变量的引用:在方法中定义的局部变量,包括方法的参数和局部变量,这些变量的引用指向了对象的实例。
- 静态变量的引用:静态变量存在于类加载器的内存中,是类的一部分,它们的引用也被视为GC Roots。
- 活动线程引用:正在运行的线程的引用和线程本地存储中的对象通常被视为GC Roots。
- JNI引用:通过Java Native Interface(JNI)创建的引用连接了Java堆内存和本地代码的内存,也可以被视为GC Roots。
- 虚拟机引导类加载器:虚拟机内部使用的类加载器引用(通常是一些核心类或库)也是GC Root。
三色标记:三色标记算法,用于垃圾回收器升级,将STW变为并发标记。STW就是在标记垃圾的时候,必须暂停程序,而使用并发标记,就是程序一边运行,一边标记垃圾。
- 黑色: 代表对象已经检查过,且成员对象也被检查过了;如果有其他对象引用指向了黑色对象,无须重新检查一遍;黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
- 灰色:代表对象已经检查过,但成员还没全部检查完成。
- 白色:代表对象没有被检查, 在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
标记过程:并发标记,传统标记会STW,期间对象引用不会变化,但是并发标记可能出现漏标或错标。
- 初始所有对象都是白色集合
- GC Root直接引用放入灰色集合
- 从灰色集合获取对象,将本对象引用的其他对象也移动到灰色集合,本对象则移动到黑色集合
- 重复步骤3,知道灰色集合为空
- 结束后白色集合可以回收掉
问题:
- 浮动垃圾:标记过不是垃圾成为了垃圾(黑色或灰色突然变成垃圾),只能等待下次GC回收,问题较小。
- 对象漏标:需要的被回收:一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题。
GC算法:
垃圾回收算法:
- 复制算法:复制算法将可用内存划分为两个等大小的区域,仅使用其中一个区域进行分配,另一个作为备用。当正在使用的区域满时,垃圾收集器会暂停程序执行,扫描整个使用的区域,并将所有存活的对象复制到备用区域,同时清理原区域。【Serial和Parallel Scavenge收集器】
- 标记清除:标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象【CMS】
- 标记整理:标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题
垃圾回收器:
Serial GC
- 描述:Serial GC是一个单线程的垃圾收集器,意味着它在执行垃圾回收时会暂停所有应用程序线程(即所谓的“Stop the World”事件)。适用于单核处理器或小内存、低延迟要求的应用程序。
Parallel GC (Throughput Collector)
- 描述:Parallel GC使用多个线程进行垃圾回收,可以显著提高垃圾回收的速度。年轻代和老年代都可以并行处理。
- 适用场景:适用于需要高吞吐量的应用程序,尤其是在拥有多个CPU核心的服务器环境中。
- 优点:通过并行处理提高了垃圾回收效率,特别适合后台任务或对响应时间不太敏感的应用。
- 缺点:虽然提高了吞吐量,但可能会导致较长的停顿时间,因为整个过程仍然是“Stop the World”的。
CMS (Concurrent Mark Sweep) GC
描述:CMS GC旨在减少停顿时间,特别是在老年代垃圾回收期间。它尽可能地与应用程序线程并发执行标记和清除阶段(一般这两个阶段耗时最长),但在某些阶段仍需暂停应用程序线程。【标记清除】
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
- 并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
- 适用场景:适用于交互式Web应用或其他对响应时间敏感的应用。
- 优点:减少了长时间的“Stop the World”事件,提高了用户体验。
- 缺点:CMS可能无法完全避免停顿,并且在高负载下可能导致碎片化问题,影响性能。
G1 (Garbage First) GC
描述:G1 GC将堆划分为多个大小相等的区域,并能独立于其他区域进行垃圾回收。它优先清理包含最多垃圾的区域(这也是G1名称的由来),旨在提供可预测的停顿时间。【标记整理】
- 初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记;
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢;
- 最终标记:Stop The World,使用多条标记线程并发执行;
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行;
- 适用场景:适用于大内存或多核处理器系统,特别是那些希望控制停顿时间的应用。
- 优点:能够较好地平衡吞吐量和停顿时间,适合大型企业级应用。
- 缺点:配置相对复杂,需要根据具体应用调整参数以获得最佳性能。
ZGC (Z Garbage Collector)
- 描述:ZGC是一个可扩展的低延迟垃圾收集器,专为满足现代硬件需求而设计。它的设计目标是在不影响应用性能的前提下支持非常大的堆(从几GB到数TB)。
- 适用场景:非常适合需要极低延迟和大堆内存的应用场景。
- 优点:几乎所有的操作都是并发执行的,极大地减少了停顿时间。
- 缺点:相比其他收集器,ZGC还在不断发展中,某些功能可能不如传统收集器成熟。
其他
Java对象:
对象的基本结构
对象头(Object Header):
每个对象都有一个对象头,它包含了运行时数据如哈希码、GC分代年龄、锁状态标志等信息。对象头的大小依赖于JVM的实现和是否启用了压缩指针。
- 在64位JVM上,默认情况下对象头是12字节(如果启用了指针压缩,则为12字节;否则为16字节)。
- 在32位JVM上,对象头通常是8字节。
实例数据(Instance Data):
实例数据部分包含了对象中实际存储的数据,即类中的字段(包括从父类继承下来的字段)。每个字段根据其类型占据不同的空间:
boolean
,byte
- 1字节char
,short
- 2字节int
,float
- 4字节long
,double
- 8字节- 引用类型(如其他对象的引用)- 在32位JVM上通常为4字节,在64位JVM上默认为8字节(若开启指针压缩则为4字节)。
对齐填充(Padding):
- Java对象要求其大小必须是8字节的倍数以提高性能,因此可能会有额外的填充字节来满足这一要求。例如,如果一个对象的总大小(对象头+实例数据)为13字节,则需要再加3字节的填充使其达到16字节。
堆内存设置原则:
- 应用程序的需求
- 对象创建频率和规模:如果您的应用程序频繁创建大量临时对象,或者处理大数据集,则可能需要更大的堆空间。
- 长生命周期的对象:对于那些具有较长生命周期的对象,确保有足够的老年代空间来容纳它们,避免过早的老年代垃圾回收。
- 系统硬件限制
- 物理内存大小:确保分配给JVM的最大堆内存不会超出服务器的物理内存。否则,可能导致操作系统使用交换分区,这会严重影响性能。
- 其他进程需求:除了JVM之外,服务器上可能还运行着其他服务或进程,预留足够的内存供这些进程使用是很重要的。
- 垃圾回收行为
- GC频率与暂停时间:较大的堆空间可以减少垃圾回收的频率,但每次GC的时间可能会更长。较小的堆可能导致更频繁但更短暂的GC事件。根据应用对GC暂停时间的要求调整堆大小。
- GC算法的选择:不同的垃圾收集器有不同的优化策略。例如,G1 GC旨在提供可预测的停顿时间,而CMS则侧重于缩短停顿时间。选择合适的GC算法也会影响堆大小的选择。
- 应用的并发性
- 线程数量:每个线程都有自己的栈空间,默认情况下每个线程的栈大小可能是512KB到1MB不等。高并发应用中,大量的线程也会消耗相当多的内存。
- 数据结构复杂度:复杂的嵌套数据结构会增加内存占用量,需据此调整堆大小。
- 性能测试与监控
- 基准测试:基于实际工作负载进行基准测试可以帮助确定最佳的堆大小配置。
- 持续监控:利用工具如VisualVM、JConsole或其他监控解决方案,实时监控堆使用情况、GC活动等指标,以便根据实际情况动态调整堆大小。
- 安全边际
- 预留缓冲区:为了避免因内存突然激增而导致OOM错误,通常建议不要将所有可用内存都分配给堆。留出一定的安全边际以应对突发的内存需求。
自定义类加载器:
//自定义类加载器,打破双亲委派
//对于双亲委派机制而言,本质上是在Classloader这个抽象类中实现的,如果我们想打破双亲委派机制,主要继承Classloader重写里面的方法比如loadclass就能够实现我们自定义的加载逻辑
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// Define the class with a custom byte array, breaking the parent delegation
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
// Convert package name to directory structure
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
FileInputStream inputStream = new FileInputStream(path);
byte[] buffer = new byte[(int) new File(path).length()];
inputStream.read(buffer);
inputStream.close();
return buffer;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String classPath = "path/to/classes";
CustomClassLoader classLoader = new CustomClassLoader(classPath);
try {
// Load a class named "DynamicClass" from the specified class path
Class<?> dynamicClass = classLoader.loadClass("DynamicClass");
System.out.println("Class " + dynamicClass.getName() + " loaded successfully.");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
堆外内存示例:
import java.nio.ByteBuffer;
public class OffHeapMemoryExample {
public static void main(String[] args) {
// 分配堆外内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 1KB的直接内存
// 写入数据
buffer.put((byte) 'a');
buffer.put((byte) 'b');
// 翻转模式,将缓冲区从写模式切换到读模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 清除引用(帮助垃圾回收释放内存)
buffer = null; // 切断引用
System.gc(); // 提示 GC 回收,但不保证
System.out.println("\nEnd of example");
}
}