自动内存管理机制

本贴最后更新于 3020 天前,其中的信息可能已经天翻地覆

第二部分 自动内存管理机制

第2章 JAVA内存区域与内存溢出异常

运行时数据区

根据JAVA虚拟机规范 SE 7 运行时数据区包括以下内容

  • 所有线程共享的
    • 方法区
  • 线程隔离的数据区
    • 虚拟机栈
    • 本地方法栈
    • 程序计数器

程序计数器

  • 存储当前线程执行到哪一个字节码。
  • JAVA虚拟机可以在一个系统线程内,控制切换多个 JAVA线程,程序计数器 提供了这个实现的可能
  • 执行NATIVE方法时这个计数器值为空(JIT后应该也为空)
  • 程序计数器应该跟 栈帧 配合使用

JAVA虚拟机栈

虚拟机栈

  • 虚拟机栈是一个存储当前线程执行的方法层次的一个FILO容器
  • 虚拟机每开始执行一个JAVA方法都会创建一个 栈帧,并将其放到虚拟机栈中
  • 虚拟机方法执行完后就会从虚拟机栈中移除这个栈帧

栈帧

  • 栈帧保存的内容包括:局部变量表,返回值,操作数栈(?),动态链接(?)
  • 局部变量表 存放了编译器可知的 各种基本数据类型,boolean,int,char,short,int,long,float,double,对象引用 及 returnAddress(指向字节码指令的地址)

可能抛出的异常

  • StackOverflowError 超出虚拟机所允许的最大栈长度
  • OutOfMemoryError 栈扩展时无法申请所需的空间

本地方法栈

  • 与JAVA虚拟机栈相似,不过本地方法栈是为了虚拟机调用 本地方法而设定。
  • 有些虚拟机的本地方法栈和JAVA虚拟机栈是合二为一的,如HOTSPOT

JAVA堆

  • JAVA堆设计的唯一目的是存放实例对象
  • 几乎所有的实例对象和数组都是放在堆里
  • 随着JIT编译器的发展 以及 逃逸分析技术逐渐成熟 栈上分配 以及 标量替换优化技术 使得对象不一定在堆上分配
  • JAVA堆是GC的主要区域,而GC算法大多都采用分代算法因此,JAVA堆根据GC算法的不同还可以分为不同的区域:
    • 粗略的划分:新生代,老年代
    • 细致的划分:Eden空间(刚创建,未经历过GC的对象所存放的空间),From Survivor,To Survivor
    • 功能的划分:线程私有分配缓冲区
  • 当没有空间完成对象的分配时,会抛出OutOfMemoryError异常

方法区

  • 用于存储 已经加载的类的信息,常量,静态变量,即时编译器编译后的代码数据等
  • 方法区 跟 JAVA堆一样,都在堆中。为了区分Java堆和方法区,方法区的一个别名是 Non-Heap
  • 方法区的具体设计在JAVA虚拟机规范中并没有强制规定,因此在一些JVM的实现中,方法区会放到了JAVA堆里。
  • 当方法区无法满足新的内存分配需求时,会抛出 OutOfMemoryError异常

常量池

  • 常量池存储在方法区中
  • 分为编译时常量池与运行时常量池
  • 运行时可以把常量加到常量池中,如String.intern()
  • 虚拟机规范对编译时常量的存储有严格规范,但是对运行时常量没有规范的要求

直接内存

  • 直接内存并不是JAVA虚拟机管控的内存
  • JDK1.4加入了NIO类,引入了基于Channel以及Buffer的IO形式,可使用Native方法直接分配JAVA堆外内存,在一些场景下能提高性能,避免在JAVA堆和Native堆中来回复制数据
  • 直接内存不受Xmx虚拟机参数管控,因此,有可能导致软件使用的内存大于指定的空间

HOTSPOT虚拟机对象解密

对象的创建

  • 遇到NEW时,检查常量池中有没有对应的类的符号引用
  • 检查类有没有被初始化,若无,则初始化
  • 在JAVA堆中分配出对象所需的空间
    • GC方法带有压缩过程的话,如Serial、ParNew,会使用指正碰撞的方法分配内存。(指针记录左边已分配,右边未分配)
    • GC方法不带压缩的话,如CMS,会使用空闲列表来分配内存。
    • 因为多线程会竞争分配,这里会降低效率,因此设计了Thread Local Allocate Buffer,预先为线程分配了内存,因此线程内无需考虑并发情况
    • 分配对象内存时,虚拟机会给分配的空间的值都值为0
    • 然后设置对象的对象头,包括 MarkWord,对象对应的类的指针,数组长度等。
    • 然后虚拟机负责的任务就完成了,开始调用用户自定义的初始化方法

对象的内存布局

包含三项内容

  • 对象头
    • MarkWord
    • 对象所属类的定义的指针
    • 数组长度(可选,若是数组,则有这个东西)
  • 实例数据(真正的用户数据)
  • Padding(对齐填白,HOTSPOT管理内存时,要求对象的起始地址要为8字节的整数倍,因此当大小不符时,要填白)

对象访问定位

通过栈中的Referance访问对象的实现有两种类型,使用句柄 以及 直接指针访问

  • 使用句柄
    • JAVA栈中的Referance指向句柄池(存储在JAVA堆)
    • 句柄存储着对象的类型的指针以及对象自身的指针
  • 直接指针访问
    • 栈中的Referance直接指向对象的地址
    • 对象自身存储着对象所属类型的指针

使用句柄的好处是,在GC的时候,只需修改句柄中对象的地址

使用直接指针访问的好处是访问速度快,不像句柄还需一次中间的转换,但是缺点是GC的时候需要修改Referance中的地址

Hotspot使用的是直接指针访问

OutOfMemoryError实战

以下测试基于HOTSPOT

JAVA堆溢出

可以通过JVM参数 -XX:+HeapDumpOnOutOfMemoryError在内存出现溢出异常时,Dump出当前内存堆的快照以便分析。

OutOfMemoryError异常会具体提示是哪一块出现问题了,如提示 JavaHeapSpace

分析工具Eclipse Memory Analyzer

确认分析内存中的对象是否是必要的——跟踪泄露对象到GC Roots的引用链

若没有泄露的对象,就要考虑增加最大的堆大小。

虚拟机栈 和 本地方法栈溢出

HOTSPOT 的虚拟机栈 以及 本地方法栈 都是同一个,因此不必区分。

经测试 无论是 减少栈容量大小 还是 增加调用方法的本地变量大小 造成的异常都是 StackOverFlowError。异常时输出的栈深度 会相对的减少

通过不断创建线程的方法,可以产生 OutOfMemoryError。

在32位WINDOWS中,系统给单个进程的内存限制为2G,减去Xmx最大堆容量,再减去MaxPermSize最大方法区容量,再减去程序计数器的容量,再减去虚拟机自身的消耗,那么 剩下的内存就由 虚拟机栈 和 本地方法栈 瓜分,当编写多线程程序的时候,发现报 OOM异常,且提示为 JAVA栈 内存 不够时,如果 不能减少线程数 或者 更换64位虚拟机时 需要减少最大堆大小

方法区和运行时常量池溢出

因运行时常量池就在方法区中,因此,这两个的测试可以一并进行

JDK 1.7 开始去除永久代,因此 在 方法区 溢出时的表现 1.6与1.7 存在差异

String.intern()方法用于检索指定的字符串在 常量池中 是否存在,若存在则返回对应引用,若不存在则将其放到 运行时常量池中。

一个不断往运行时常量池里通过 String.intern()塞东西的测试案例,在1.6中会报PermGen Space OOM,而在1.7中并不会报这个错误,因为1.7没有PermGem Space,且会对那些运行时常量进行回收操作。

1.6的String.intern会将字符串复制一份到PermGem中,而1.7则会在运行时常量池里添加一个到Heap中的Referance,而不会复制一份

造成方法区溢出的主要原因是 动态创建了太多类 或者 使用了OSGI 而类无法及时回收。

本机直接内存溢出

Direct Memory 容量可以通过 -XX:MaxDirectMemory来指定,若不指定,则跟Xmx一样。 Java虚拟机会记录一共申请了多少Direct Memory,若超过了指定值,会直接报错(不会向系统申请)

这个Direct Memory的OOM并不会指明是哪个空间内存不够,因此需要自行判断下,是否直接内存不足。

第3章 垃圾收集器 与 内存分配策略

对象已死?

引用计数算法

新增一个引用,则计数器+1,引用消失 则计数器 -1,计数器为0,则表示对象不可能再被使用

本方法实现简单,但无法解决循环引用的问题。即 A->B 且 B->A。这种情况,将一直无法进行GC

可达性分析

通过GC ROOTS对象能访问得到的对象就是正在使用的对象,访问不到的,则可回收

GC Roots对象包括以下几种:

  • 虚拟机栈(机栈中的本地变量表)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

引用的分类

引用实际上在JDK1.2后,分为了几个类型

  • 强引用,正常的引用
  • 软引用(SoftReferance),当内存不够,OOM发生之前,会将软引用的数据回收,若还是不够则抛出OOM
  • 弱引用(WeakReferance),无论内存是否足够,弱引用对象都会在下一次GC的时候被回收
  • 虚引用(PhantomReferane),比弱引用更弱,无法通过虚引用获得一个对象实例。设置虚引用的唯一目的是在对象被GC时受到一个系统通知

拯救死亡对象

对象若达到了GC条件,那么很快就会被回收掉,回收前夕,如果对象重写了 finalize方法,那么该对象有可能通过finalize方法避免GC。只要在finalize中重新为对象建立一个到GC ROOTS的强连接即可。finalize执行完后,虚拟机会再次判断该对象是否要GC,发现已经跟GC ROOTS 建立了强连接的话,那么就会将其移出要回收的对象的组合。

但虚拟机并不承诺等待finalize的执行完毕,拯救行动有可能失败。 而且finalize只会被虚拟机执行一遍,第二次要被GC的时候,就无法被拯救了。

拯救死亡对象并不是一个被推荐的方法,可能导致奇怪的情况出现。

回收方法区

方法区回收的主要内容是 废弃常量 以及 无用的类。

废弃常量的判断方法跟 可达判断类似,除了自己之外,通过其他GC Roots不可达,那么该常量可被回收。

废弃类的判断比较复杂:

  • 要求该类的所有实例都已被回收
  • 加载该类的ClassLoader已经被回收
  • 该类对应的 java.lang.Class对象没有在任何地方被引用(即无法在任何地方通过反射,访问该类的方法)

满足以上条件的类,可以被回收。具体是否回收在HotSpot中提供了 -Xnoclassgc参数进行控制,使用-verbose:class以及-XX:+TraceClassLoading(Product版本可用) -XX:+TraceClassUnLoading(FastDebug版本可用)查看类的加载和卸载信息

大量使用反射、动态代理、CGLib等ByteCode框架需要具备类卸载的功能,否则有可能导致方法区溢出(Hotspot 1.6 及之前的永久代溢出)

垃圾回收算法

标记-清楚算法

分为 标志 及 清除 两个阶段,是大多数回收算法的基础,存在以下缺点

  • 效率,标记以及清除的效率都不高
  • 空间,清除后会产生大量空间碎片,导致可能无法继续分配大对象

复制算法

将内存分为对等的两半,每次只使用其中的一半,GC时,将存活的对象直接复制到另外一半中,原有的一半一次性清理掉。这样就解决了碎片的问题 以及 清理效率的问题。

但是代价也很明显,就是只能使用一半的内存。因此实际应用中,是有改进的。

据IBM的调查发现 新生代的对象 98% 都会在第一次GC时被回收掉,因此HOTSPOT新生代的默认设计如下

  • 80%新生代容量分配给Eden(一次GC都没经历过的对象所存放的空间)
  • 有两个大小一样的survivor区,各占新生代容量的10%。

GC时,将Eden以及当前在使用的survivor存活的对象复制到另外一个survivor中。 按照如上设计,新生代可用内存为新生代总内存的90%。

当然,有可能存在 存活对象的大小大于 survivor区大小的情况,这时候,survivor可以跟老年代借内存。当多次GC都无法将借的内存偿还时,就将部分对象升级为 老年代,这样survivor又可以恢复到原来的大小了。

标记-整理算法

复制算法只适用于 新生代,因为其存活率较低,但是老年代的存活率较高,因此需要一个新的算法。

标记整理算法就是,标记处哪些是需要保留的对象,然后将存活的对象都往一边移动,直接清理掉边界以外的内存。就跟磁盘整理程序一样一样的。

分代收集算法

就是说 根据 对象存活的年代选用 不同的回收算法。 像上面的,新生代使用 复制算法,老年代使用 标记-整理 算法。

HOTSPOT的算法实现

枚举根节点

枚举根节点进行GC分析的时候,所有线程都必须停止下来,否则不能得到一个准确的结果。 因此枚举根节点的过程必须快。

根节点包括 方法区里的静态引用,常量,以及 本地方法 JAVA方法栈 里的局部变量。

在上述的位置的变量里,只有少数部分才是Referance,这些会影响GC。其余的GC并无影响。 因此快速准确识别出那些是Referance,才能提高GC的时间。

对于静态引用及常量,可维护一个 OOPMAP(Ordinary Object Map),若是Referance,则将其加到Map中,若静态引用及常量被移除

对于 JAVA方法 及 JIT后的方法,也会使用OOP来快速识别这些Referance。

对于解析的JAVA方法,OOP MAP的生成是动态的,在GC开始后,分析字节码生成 对于JIT后的方法,OOP MAP会在编译时生成,并放在编译后的代码的一些特定位置,如:方法调用前,循环跳转,异常跳转等。这些特定的位置叫安全点

对于JNI方法 对于JNI方法中受虚拟机管控的GC ROOTS的枚举无法使用OOP MAP,因为 代码不是JIT生成的,也非JVM解析执行的。 对于传入JNI的JAVA对象,必须通过句柄的包装。这样虚拟机就可以通过句柄 枚举JNI调用部分的 GC ROOTS

准确GC

OOP MAP另外一个作用就是方便JVM实现 准确GC。就是说能识别出所有位置的Referance,当Referance指向的Object位置发生变化时,也可以对Referance中存储的地址进行变更。因此准确GC可以移动Object在内存中的位置。

对应的保守GC 不能准确地判断出某些变量是否Referance,因此无法移动Object的位置

安全点

上面一节已经提到,存储了OopMap的位置称为安全点。

当需要GC时,所有的线程都必须处于安全点中,这个做法通常是,当GC线程发现需要进行GC时,则设置某一个标志位,其余线程在安全点(OOP MAP)检查这个标志是否置上,若置上了,会进入特定的等待处理逻辑。

等到所有的线程都停止了,就可以进行 GC 了。

安全区域

但有时 线程处于Sleep 或者 Blocked的时候,不太可能继续继续往下走,这样GC就等待不到所有的线程进入安全点。因此,引入一个新的概念,安全区域。

安全区域指代 在安全区域执行代码的线程的引用关系不会发生变化。当一个线程将自己标志位 已进入Safe Region,那么GC线程就不管这个线程,直接进行GC了

当线程离开Safe Region的时候,要检查系统是否完成了 根节点枚举(或者是整个GC过程),如果完成了,则可以安全离开,若否,则需要等待可以离开的信号。

垃圾收集器

以下基于 JDK 1.7 UPDATE14 后的HOTSPOT虚拟机

  • 新生代收收集器
    • Serial
    • ParNew
    • Parallel Scavenge
    • G1
  • 老年代收集器
    • CMS
    • Serial Old(MSC)
    • Parallel Old
    • G1

没有最好的垃圾收集器,只有根据场景最适用的垃圾收集器

Serial收集器

新生代区域内存收集器,使用一个线程进行GC,使用复制算法。 优点:

  • 单线程效率最高的垃圾收集器
  • Client模式下默认的收集器

缺点:

  • GC时会STOP THE WORLD

Client模式下的默认新生代收集器,因为其适用于 回收小内存,几百M的内存回收在一百多毫秒内就能完成,用户基本察觉不到

ParNew收集器

Serial收集器的多线程版本,它们公用了很多的代码

他是很多服务器的首选收集器,因为只有 它以及Serial 可以跟 CMS配合使用

ParNew开启的GC线程默认跟CPU核数一样,可以通过 JVM参数 ParallelGCThreads进行限制

Parallel Scavenge 收集器

与PARNEW一样,是多线程复制新生代收集算法。但其可以根据运行的业务调整吞吐量,侧向于高吞吐量。吞吐量=用户运行代码时间/(运行用户代码时间+垃圾收集时间),通过以下两个参数设置:

  • MaxGCPauseMills 控制最大GC时间
  • GCTimeRation 直接控制吞吐量

可以使用UseAdaptiveSizePolicy参数开启自适应GC策略,会自动调整 EDEN,Suvivor的比例,晋升老年代年龄等等。

这个自适应策略是区别于ParNew的一个重要特性。

Serial OLD收集器

Serial收集器的老年代版本,单线程收集器,使用 标记-整理(Mark-Compact) 算法

一般情况下也是用于客户端

若用于服务器,一般用于CMS的后备方案(后文会讲)

Parallel Old收集器

Parallel Scavenge的老年代版本。使用多线程 Mark-Compact 算法。只能与Parallel Scavenge配合使用。

CMS(Concurrent Mark Sweep)收集器

以获取最短停顿为目标的老年代收集器。使用多线程 Mark-Sweep 算法。

本算法的主要核心算法

  • 第一次进入SafePoint的时候,Stop the World,获取所有的 GC ROOTS进行初始标志完成后,恢复大部分用户线程
  • 剩余的一部分未恢复的线程会 继续进行 并发标志(并发 指代 用户线程 与 GC线程一起工作)
  • 并发标志结束后,由于之前用户线程继续运行,会新增新的GC ROOT 以及 一些对象会新增一些引用
  • 因此需要在下次Safe Point的时候再次STOP THE WORLD,所有的计算 资源都投入到这些增量的引用的计算中
  • 经过这次STOP THE WORLD就能标志出所有可能在使用的对象,据此可以反推出 一定是无用的对象,此时可以恢复大部分用户的线程,留下一部分GC线程回收这些标志为垃圾的对象即可
  • 当回收完毕,GC线程全部释放,计算能力全部归还用户

优点:低停顿

缺点:

  • 当主机核数为4核以上时,会占用不少于25%的用户计算资源,如果核数为2核时,会占用50%的计算支援,1核就是全部了,实际上还是会影响服务器的性能的
  • CMS无法处理浮动垃圾。在并发清理或者并发标记的过程中,用户处理线程不断产生对象,导致内存不够用的时候,CMS不再适用,会报 Concurrent Model failure,改成使用 Serial Old算法来清除(所谓的FULL GC)。
  • CMS使用的是Sweep,因此有可能没有连续的空间用于创建大对象,此时,要进行FULL GC(Serial OLD),Serial Old会进行COMPACT的操作。JVM也支持设置成 进行N次FULL GC后才COMPACT的设置

FULL GC的Serial Old算法可改造成 Parallel,社区已经有体验版

G1收集器

思想简介

是当前收集器技术发展最前沿的成果之一,HotSpot赋予G1的使命是最终能替换掉CMS收集器 G1可用于新生代及老年代,其适用于 多核,大内存的服务器,它STOP THE WORLD的时间短,且有较高的吞吐量,且G1允许用户设定 最大停顿时间。

G1的主要设计思想是化整为零,逐个击破。

G1会将JAVA堆分成很多个大小相等的REGION(具体的大小会在运行时视情况实时调整),这些REGION可以是 eden,也可以是 survivor,也可以是 ,每个Region都可以单独的被回收,回收时将存活的对象都拷贝到另外一个REGION,拷贝完成后全部回收之前的REGION ,回收的个数也会视情 用户设定的 最大停顿时间 及 历史经验 动态决定,但GC会优先回收 回收价值最高的REGION(根据经验值 推测 回收获得的空间大小及所用的时间),这也是G1(Garbage first)的名字的由来。

RememberSet

在年轻代回收的时候,如果每次都需要在年轻代 及 老年代 全体对象间计算引用关系的话,那么将耗费巨大的时间。为了减少 老年代 引用的计算,Serial,ParNew等GC算法 会维护一个 老年代的RememberSet.RememberSet代表着整个老年代引用年轻代对象的记录(在为一个对象内的Referance赋值时,JVM会检查对象是否为老年代 若是 且 referance指向 年轻代,那么就会为rememberSet增加一条记录。删除RS记录的原理也类似)。有了RS后,Serial等Minor GC时,将老年代的RS也作为 GC ROOTS,一起计算 young gen 的可回收对象。这样就大大节约了年轻代回收 标志的时间。

在G1中,为了避免全堆栈扫描,只回收部分REGION也做了类似优化。G1为每个REGION都维护了RS,那么要回收特定的几个REGION时,就将其余REGION的RS也加入到GC ROOTS的计算即可。

具体执行过程
  • 初始标志(方法区,栈,RS作为GC ROOTS标志对应的对象,STOP THE WORLD)
  • 并发标志(根据之前的标志,继续往下进行标志,时间较长,与用户应用一起运行)
  • 重新标志(STOP THE WORLD,根据 RS的变化日志,栈引用变化,方法区引用变化增量标志有用的对象)
  • 并行回收(STOP THE WORLD,根据标志复制有用的对象到新的REGION,无用的清空)
  • 恢复用户线程
是否选择G1?

当之前的CMS,ParNew组合无法满足 延时 需求的时候,可以考虑使用G1,否则无需更换

参考

G1收集器ORACLE简介

内存分配回收策略

  • 内存优先在EDEN中分配
  • 大对象直接进入老年代,避免在 复制算法中 来回复制
  • 长期存活的对象将进入老年代(每经过一次MINOR GC都存活,默认超过15次则会进入老年代)
  • 动态年龄判断(如果某一代的对象的大小总和大于survivor的一半,那么这一代及比它们老的一代会直接进入老年代。个人猜测本条可能不适用于G1...)
  • 空间分配担保(如果survivor的大小不足于保存存活的eden对象,那么只能让一部分对象直接进入老年代),参数HandlePromotionFailure
    • 为FALSE,不允许担保失败。MinorGC发生时,检测老年代的连续空间是否大于新生代的总空间,若是,则只进行MinorGC,若否 则进行FullGC.
    • 为true,则允许担保失败。MinorGC发生时,检测老年代的连续空间是否大于新生代存活的平均大小(经验值),若小于,则直接FULL GC,若小于则执行MinorGC,若由于没有足够多的连续空间而发生担保失败时,将会浪费掉之前MinorGC执行的时间,然后触发一次FULL GC。

虚拟机性能监控与故障处理

JDK命令行工具

jps

  • -m 传入main的参数
  • -l 输出执行类的全名,如果是JAR包,则输出JAR包的地址
  • -v 输出虚拟机启动的参数

jstat 虚拟机统计信息监视工具

显示 本地或者远程 虚拟机进程中的 类装载、内存、垃圾收集、JIT编译等运行数据

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

options: -class -compiler -gc -gccapacity -gccause -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -printcompilation

jstatd可创建远程RMI服务器,然后可通过远程UI排查问题

jinfo(Configuration info for java)

实时的查看和调整虚拟机各项参数,不过调整能否马上生效就不清楚了

jmap(生成JAVA堆转储快照)

  • -dump 生成堆转储快照
  • -finalizerinfo 查看Finalizer等待执行的对象
  • -heap 查看GC详细信息
  • -histo 显示堆中对象统计信息,包括类,实例数量,合计容量

jhat

在命令行环境下分析dump的工具,并提供http形式的访问,分析功能较弱...看不出啥东西

jstatck

在线分析性能问题,定位无响应,死锁,死循环 最有力的工具。用于生成当前线程快照。

HSDIS:JIT生成代码反编译

暂时还用不着

可视化工具

jconsole

jvisualvm

  • 显示虚拟机进程及进程信息(jps,jinfo)
  • 监控应用程序的CPU,GC,堆,方法区以及线程的信息(jstat,jstack)
  • DUMP及分析转储快照(jmap,jhat)
  • 性能调优,找出方法级别被调用的次数及时间
  • 其他的PLUGIN...

jmc

调优案例与实战

案例分析

高性能硬件上的程序部署策略

若由于64位虚拟机且配上大内存导致的FULL GC时间过长,那么可以考虑将一个虚拟机改造成多个32位虚拟机的集群形式。 又或者尝试使用G1 垃圾收集器。

堆外内存导致的移除错误

当发生OOME的时候,且没有指明溢出位置,那很有可能就是 堆外内存发生了溢出。

堆外内存泄漏在32位JVM中特别容易发生,因为单进程的理论最大内存为4G,windows中为2G,如果给堆内存就分配了1.6G,那么堆外内存可用就特别的少了

堆外内存的清理,要在FULL GC的时候才会执行

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • 内存管理
    3 引用 • 1 回帖
  • 对象
    11 引用 • 44 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...