Thinking in java-4 Java内存管理之垃圾回收Garbage Collection

本文详细探讨了Java的垃圾回收机制,包括自动垃圾回收的原理、GC的运行机制、两类垃圾回收方案以及Tracing Collector。重点介绍了垃圾回收的三个基本步骤:标记、常规删除和有紧致效果的删除。此外,还讲解了如何使用jvisualvm、jconsole和jstat等工具进行垃圾回收监视,以分析和优化内存管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 垃圾回收机制

1.1. 自动垃圾回收

Java编程语言的一个特色就是其自动垃圾回收机制,不像C/C++那样,内存的分配和释放都是程序员手动操作的。
有人认为,碎片化(not GC)是影响java程序性能的主要原因。当前主流(并不一定是最新的)的处理堆内存碎片化问题的解决方式是分代GC&紧致(Generational GC & compaction)。

1.2. GC的运行原则和机制

Garbage Collector垃圾回收器,是在后台运行的程序,它照看所有内存中的对象,找到没被程序中任何其他部分所引用的对象。所有这些未被引用的对象被删除,其空间被释放用来分配给其他对象。
当我们命令行运行一个程序时,如果设置命令行参数java -Xmx:2g MyApp , 则表示分配了2g的内存给该java进程,即heap内存。在程序运行时,总有堆内存耗尽的时刻,这时新的线程因为没有连续内存空间而无法被创建。这个时候JVM决定开始进行garbage collection,当然我们也可以显式地调用Sys tem.gc()来建议JVM进行垃圾回收 (之所以说是建议,因为JVM并不一定保证会进行垃圾回收)。
GC的进行需要有2个条件满足:1). 绝不回收当前依然存活的对象--有活跃引用的对象。2). 成为垃圾的对象dead objects也不是保证被立即回收的。这取决于回收策略:是保证足够的内存使用还是保证系统整体的延迟和吞吐量的考量。

1.3.  2类垃圾回收方案:

a).  Referencing Counting collectors: 引用计数垃圾回收器
思想:记录下指向每一个对象的引用数量,一旦一个对象的引用数为0则将其回收。
优点:可以立即对称为垃圾的对象进行回收,速度快;且算法简单。
缺点:其一,当对象过多时,开销大;其二,主要原因--无法解决循环引用对象问题Circular referencing.\

b).  Tracing collectors: 跟踪回收器
 思想:所有活的 live对象可以在当前已知的活的对象(root object 存在于 registers, global fields, stack frames)中找到其引用。
优点:解决了循环结构问题(本文之后所有内容都是针对于Tracing collectors而言的)。
缺点:对大部分跟踪收集器而言,在标记阶段--在回收未被引用的垃圾对象时需要等待。

1.4.  2大类Tracing Collector

1.4.1 复制回收器Coping Collectors

原理:在堆上有2块单独定义的地址from-space & to-space, 在垃圾回收时,from-space的存活的对象被复制到to-space中,然后from-space中进行垃圾回收。
优点:在 to-space中对象在内存中很紧致地放在一起, 完全没有了内存碎片化的烦恼。
缺点: 复制回收器是一种 stop-the-world collector, 意味着在垃圾回收循环中,其他应用不能执行,这对于需要高响应速度的应用而言无法容忍;而且,当from-space中的所有对象都不是垃圾--都是live对象时,to-space需要有很大空间存放from-space内容,这也使得其效率很差。
1.4.2 标记-清理回收器Mark-and-sweep collector
大部分商业JVM是使用mark-and-sweep 垃圾收集器的, 因为它没有copy-collector的负面性能缺点。一些最著名的回收策略包括:CMS, G1, GenPar( link)都是采用mark-and-sweep策略的!
原理:mark-and-sweep 垃圾回收器跟踪并标记每个live对象用一个'live'标志位。在所有的live对象都已被mark之后,进行的将是sweep操作;sweep将遍历整个heap空间,找到未被标记的内存空间地址,收集器将这些未被标记的内存空间地址链接在一起并组织成自由的表free-list,  在GC中可能有不同的链表并以块的方式组织起来(organized by chunk size). 在sweep阶段完成后,开始分配新的空间,新的空间以满足新对象要求的最合适的位置分配。
优点:响应度比copy-collector高。
缺点:mark阶段取决于堆上live对象数量,sweep阶段取决于堆的大小,因而在heap很大时效率较低

1.5 标记和清理 Mark-and-Sweep的实现方式
商业上可用的至少有2种实现标记-清理的方式。其一是并行方式Parallel approach, 其二是并发方式Concurrent approach(or is mostly concurrent).

1.5.1.  并行收集器Parallel Collector
并行垃圾回收意味着:为了达到并行垃圾回收的目的,分配给进程的资源被并行地使用着。
含义:大部分商业上实现的并行垃圾回收器是一种stop-the-world垃圾回收器--所有的应用线程在垃圾回收周期中停止,直到垃圾回收完成。
优点:垃圾回收过程中停止所有线程,使得所有资源在垃圾回收周期中国年能被有效地并行利用;这会带来很高的效率,适用于高吞吐量throughput的工作。
缺点:和优点对应的--在垃圾回收阶段,应用线程不能做任何工作,这点和copy collector一样。如果应用是response-time senstive,即:要求高响应的话,最好不要选择此类型。

 1.5.2.  并发收集器Concurrent Collector
 并发垃圾回收器更适合于高响应的应用。
含义:并发意味着--一些(或者说大部分)垃圾回收工作是和运行的应用线程并执行的。
优点:保证了应用高响应。
缺点:因为并不是所有的资源都用于GC,也不能一直进行GC操作(因为这样会消耗系统资源,影响吞吐量),所以这里存在至少2个问题。
1). 何时开始进行GC操作?
我们需要有足够的时间进行跟踪和标记存活的对象,如果garbage collection没有按时完成,应用将抛出Out of memory error。但是,GC操作也不能一直运行,因为这将消耗过多系统资源,因而会对系统吞吐量有影响。如何在动态环境中作出out-of-memory error  and responsive time 平衡是一件很难的事呢!
2). 何时能够安全地进行操作(that requires a complete and true snapshot of the world)?
比如我们在Mark-and-sweep操作时,需要知道什么时候所有的live对象已经被标记marked了,然后才能转到sweep部分。在并行GC中这个很简单,因为一切都是静态的,而在并发环境中,已经标记的内存区域可能又被其它线程做了修改。

1.6).  垃圾回收步骤

1.6.1 垃圾回收一般包括三个基本步骤

  • 标记--Marking
这是 垃圾回收的第一步,这里垃圾回收器辨认哪些对象在使用中,哪些对象没有被使用。
  • 常规删除--Normal Deletion
垃圾回收器删除未被使用的对象,并将对象所占的空间释放。
  • 有紧致效果的删除--Deletion with Compacting
为了更好的性能,垃圾回收器在删除了未被使用的对象后,所有幸存的对象将被移动到一起。这将提高分配给新对象内存的性能。

1.6.2  问题和对策

问题:这种基本的标记和删除--Mark and delete的方式存在以下问题:
  a. 首先,因为绝大部分新创建的对象将会变得不可用而成为垃圾,这是其效率低的一个原因;
b. 其次,在多个垃圾回收周期中被使用的对象,很可能在未来的周期依然被使用。
对策:内存分代。
因为存在上述的问题,所以我们采用了Java Garbage Collection is Generational,我们在内存上分为Young Generation 和 Old Generation.
所以Heap内存的是分为Young Generation/Old Generation; Heap内存的回收也是分为 Minor GC和Major GC。

1.7). JVM内存分代内存
关于JVM heap内存分代的讨论可以在 此处找到详细Q&A。
Eden space(heap):  伊甸区,新创建的对象都被分配在伊甸区。
Survivor Space(heap): 存活区, 池中包含伊甸区GC中活下来的对象。
Tenured Space(heap): Old generation, 老年代,在存活区中存活时间较久的对象。
Permanent Generation(non-heap):  该区域包含了虚拟机的反射数据,比如说类数据和方法对象--Java8中已更名为Metaspace。
一些常见命令及含义:
-Xms20m: 初始化堆大小为20m, 包括Eden,Survivor, Tenured.
-Xmx100m: 堆内存最大值为100m, 包括预留区。
-XX:NewSize=?: 年轻带内存区域大小(不含预留区)。
-XX:MaxNewSize=?: 年轻带内存区域大小
-XX:PermSize=? //ignored in java8
-XX:MaxPermSize=?
-XX:NewRatio=?: Old/New, 老年代和年轻带内存区域的比值, default=2。
-XX:SurvivorRatio=?: Young/Survivor, default=8.

2. 垃圾回收策略

共有5种垃圾回收类型,我们可以在应用中使用。我们只需使用JVM switch来选取给定的策略。
    1).Serial GC(-XX:+UseSerialGC): 串行GC使用的是简单的mark-sweep-compact策略来处理年轻带和老年代的垃圾回收,也就是所谓的Minor GC和 Major GC。
串行GC对于用户机来说是有效的,这些客户机运行的是独立的应用,并有很小的CPU。这种策略适用于内存需求小的小型应用。
2).Parallel GC(-XX:+UseParallelGC):
并行GC除了它产生N个线程对应于年轻代的垃圾回收(这里N对应于系统中CPU的核数),基本和串行GC相同。我们可以通过控制线程数量使用:-XX:ParallelGCThreads=n JVM选项来调整。
并行GC也被称作Throughput collector(吞吐量垃圾回收器),因为它使用了多个CPU加速了GC的性能。并行GC使用的是单线程控制老年代的垃圾回收。
3).Parallel Old GC(-XX:+UseParallelOldGC):和Parallel GC相同,除了其对于老年代的内存回收也使用了多线程处理之外。
4).Concurrent Mark Sweep(CMS)(-XX:+UseConcMarkSweepGC): CMS 垃圾回收器,也被称作“并发低暂停”垃圾回收器。这里的并发主要对应的是老年代。CMS collector试图通过最小化垃圾回收所带来的延迟,通过并发地和应用线程一起执行来解决。
CMS collector 所用的年轻带的算法和并行GC相同。
这种垃圾回收算法适用于需要高度响应的我们无法忍受长时间等待的系统中。我们可以通过下列参数控制CMS collector线程的数量:-XX:ParallelCMSThreads=n.
5).G1 Garbage Collector(-XX:+UseG1GC):Garbage First或者叫G1 garbage collector。这种策略从Java 7开始可用,它的长期目标是取代现有的CMS collector。G1 garbage collector 是一种并行的、并发的、内存紧致、低延迟的垃圾回收器。
G1垃圾回收器和其他垃圾回收器不同,而且没有年轻带和老年代的概念。它把堆内存分成了多个大小相同的堆区域。当垃圾回收器开始工作时,首先把区域中具有更少存活数据的区域收集起来,这就是所谓的“Garbage First”--垃圾第一。

3. 垃圾回收监视


这里有如下demo所需文件 java2demo.jar。
      首先,这里运行

java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:PermSize=20m -XX:+UseSerialGC -jar java2demo.jar 

1). 使用jvisualvm命令

在命令行键入上述代码段,之后输入jvisualvm 命令,然后选择运行demo所在的pid即可监视相关资源使用和GC回收情况。


关于图示分析:

java -Xms30m -Xmx120m -XX:PermSize=20m -XX:MaxPermSize=30m -XX:SurvivorRatio=3 -XX:+UseSerialGC -jar java2demo.jar

这里将参数设置解释一下:

-Xms30m: 设置堆大小起始为30m;

-Xmx120m: 设置堆最大为120m;

-XX:PermSize=20m:设置永久带起始大小为20m;

-XX:MaxPermSize=30m:设置永久带最大值为30m;

-XX:SurvivorRatio=3:设置Eden Generation/Survivor=3;

-XX:+UseSerialGC: 设置垃圾回收策略为简单的串行垃圾回收。



根据截图解释:

  • 老年代和年轻带如果不手动设置-XX:NewRatio=?,则系统取默认值2.这里的Heap大小设置为120m,所以老年代80m, 年轻带40m.
  • 由于设置了-XX:SurvivorRatio=3, 年轻带为40m, 故而Eden Space=40/(3+1+1)*3=24m, Survivor Space =40/(3+1+1)*1=8m.
  • Survivor 0 & Survivor 1 总有一个是空置的。

2). 通过jconsole 命令,这里不在赘述。

3). 使用jstat 命令

MacBook-Pro:~ fqyuan$ ps -eaf | grep java2demo.jar
  501  6431  6097   0 10:03AM ttys000    6:21.67 java -Xms30m -Xmx120m -XX:PermSize=20m -XX:MaxPermSize=30m -XX:SurvivorRatio=3 -XX:+UseSerialGC -jar java2demo.jar
  501  6611  6600   0 10:23AM ttys002    0:00.00 grep java2demo.jar
MacBook-Pro:~ fqyuan$ jstat -gc 6431 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
8192.0 8192.0  0.0    0.0   24576.0   2324.6   81920.0    76028.3   25472.0 24853.2 3456.0 3264.0     50    0.292  449    52.978   53.270
8192.0 8192.0  0.0    0.0   24576.0   1301.9   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0   2831.1   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0   5069.4   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0   8039.7   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0  10764.9   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0  13683.8   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0  15767.2   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0  17675.3   81920.0    76107.5   25472.0 24853.2 3456.0 3264.0     50    0.292  450    53.163   53.455
8192.0 8192.0  0.0    0.0   24576.0   993.8    81920.0    76822.4   25472.0 24864.9 3456.0 3265.6     50    0.292  451    53.372   53.664
8192.0 8192.0  0.0    0.0   24576.0   1028.9   81920.0    72249.2   25472.0 24864.9 3456.0 3265.6     50    0.292  452    53.594   53.886
8192.0 8192.0  0.0    0.0   24576.0   1592.3   81920.0    72267.8   25472.0 24864.9 3456.0 3265.6     50    0.292  453    53.788   54.080
8192.0 8192.0  0.0    0.0   24576.0   1674.7   81920.0    72441.0   25472.0 24864.9 3456.0 3265.6     50    0.292  454    53.969   54.261
8192.0 8192.0  0.0    0.0   24576.0   3588.5   81920.0    72441.0   25472.0 24864.9 3456.0 3265.6     50    0.292  454    53.969   54.261
8192.0 8192.0  0.0    0.0   24576.0   4908.1   81920.0    72441.0   25472.0 24864.9 3456.0 3265.6     50    0.292  455    53.969   54.261
8192.0 8192.0  0.0    0.0   24576.0   377.0    81920.0    72548.1   25472.0 24864.9 3456.0 3265.6     50    0.292  456    54.336   54.628
8192.0 8192.0  0.0    0.0   24576.0   1760.2   81920.0    72548.1   25472.0 24864.9 3456.0 3265.6     50    0.292  456    54.336   54.628
8192.0 8192.0  0.0    0.0   24576.0   3430.4   81920.0    72548.1   25472.0 24864.9 3456.0 3265.6     50    0.292  456    54.336   54.628
8192.0 8192.0  0.0    0.0   24576.0   5381.4   81920.0    72548.1   25472.0 24864.9 3456.0 3265.6     50    0.292  456    54.336   54.628

首先用 ps -eaf| grep java2demo.jar 得到所运行线程的pid(也可用jps 命令);

然后用jstat -gc 6431 1000;6431指代的是线程pid, 1000表示每过1000ms输出一次结果。

S0C:幸存区0的当前大小,以KB为单位。

S1C:幸存区1的当前大小,以KB为单位。

S0U:幸存区0的当前使用内存大小,以 KB为单位。

S1U:幸存区1的当前使用内存大小,以KB为单位。注意,总是有一个幸存区总是空的。
EC:Current Eden Space大小,当前伊甸区大小,以KB为单位。
EU:当前伊甸区使用的大小,以KB为单位。
OC:老年代的大小,以KB为单位。
OU:老年代内存使用情况,以KB为单位。
MC:元数据内存的大小,以KB为单位。
MU:元数据区使用大小,以KB为单位。
CCSC:Compressed class space capacity,当前压缩类空间大小,以KB为单位。
CCSU:压缩类空间使用情况,以KB为单位。
YGC:number of young generation GC events, 年轻带GC发生次数。
YGCT:Young generation collection time, 年轻带GC时间。
FGC:number of full GC events, 完全GC发生的次数。
FGCT:完全GC所用时间。
GCT:GC所用总时间。

4. GC总结

1. 不同的垃圾回收算法是为满足不同应用服务的,需要区别对待。其中跟踪垃圾回收器Tracing Collector 在商业环境中被广泛运用。
2. Parallel GC 并行的使用可用资源完成GC功能,这种策略通常需要一种Stop-the-world 的垃圾回收器,为了快速的垃圾回收使用所有可能的系统资源,提高了系统的吞吐量,但是对于系统响应较差。
3. Concurrent GC 并发的GC在线程运行时进行垃圾回收工作,主要为了保证系统响应时间。但是何时开始进行GC操作--即:在系统吞吐量throughtput 和 response time之间权衡是一件很复杂的事情!而且在Mark 和sweep时间点的选择甚难抉择。
4.  分代内存分配、分代垃圾回收 Generational garbage collection 可以推迟JVM内存碎片化的时间,但是并不能避免内存碎片化问题的存在。使用分代垃圾回收的思想来源是:绝大部分新建的对象会在第一次垃圾回收中被回收!
5. JVM虚拟机参数调节tuning:   只能推迟OutOfMemoryError出现的时间,随之而来的是如果做太多调整将带来rigidity。
6. 唯一能解决内存碎片化问题的方案是:内存紧致处理。绝大部分垃圾回收器在进行紧致处理的时候需要进行stop-the-world 操作,因此,应用运行时间越长,需要紧致的时间越长;堆空间越大,需要进行紧致的时间代价也会更大。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值