JVM垃圾回收

一.如果判断对象是垃圾对象

1.引用计数法:

给对象加一个引用计数器,每有一个引用指向它,计数加一,如果计数为0,就是垃圾对象

优点:实现简单,效率高

缺点:无法解决对象之间相互引用的问题。有两个对象AB,A的属性指向B,B的属性指向A,这两个对象都没用了,但由于相互引用,导致无法回收

2.可达性分析算法:

以GC Roots为起点,当一个对象从GC Roots无法到达时,这个对象就可以回收。

GC Roots:

     虚拟机栈(栈帧中的本地变量表)中引用的对象;

    方法区中类静态属性引用的对象;

    方法区中常量引用的对象;

    本地方法栈中引用的对象;

下图中object567虽然互相引用,但是可以回收

二.垃圾收集算法

1.标记清除算法

先标记出所有要回收的对象,然后统一清除

缺点:标记和清除效率都不高;会产生不连续的空间,如果分配大对象,因为碎片空间太多无法找到足够的内存不得不进行另一次垃圾回收动作

2.复制算法 

现在虚拟机都采用这种算法,但研究表明98%的对象是朝生夕死的,所以不需要 把内存分配成1:1。

因为新生代对象生命周期一般很短,现在一般将该内存区域划分为三块部分,一块大的叫Eden,两块小的叫Survivor。他们之间的比例一般为8:1:1。

使用的时候只使用Eden + 一块Survivor。用Eden区用满时会进行一次minor gc,将存活下面的对象复制到另外一块Survivor上。如果另一块Survivor放不下,对象通过分配担保机制直接进入老年代。

为什么有两个Survivor?一次垃圾回收会让Eden 和Survivor A清空,存活的对象进入另一个Survivor B,随着程序运行,新对象再次进入Eden和有对象的Survivor B,当下次垃圾回收时,存活的对象进入Survivor A,SurvivorB和Eden清空,如此往复。

3.标记-整理算法

 

三.垃圾收集器

垃圾回收器是上述算法的实现

1. Serial收集器

最悠久的收集器,单线程,并且在垃圾回收线程工作时,其他工作线程全部会停,包括用户的线程。Stop the world.,会有一个停顿时间。

但优点是简单高效,在桌面应用场景中,分配给虚拟机管理的内存不会很大,停顿时间可以接受。

jvm有两种模式,Server和Client模式,Client模式默认的新生代收集器就是这种收集器

参数控制:-XX:+UseSerialGC 串行收集器

2.Parnew

Serial的多线程版本,其控制参数、分配对象规则、回收算法和Serial都一样。

有一点,它是JVM在Server模式下的首选新生代收集器,同时只有Parnew和Serial能够与CMS收集器配合。

Parnew也是使用-XX:+UseConcMarkSweepGC后的默认新生代收集器,也可以手动制指定-XX:+UseParNewGC.

单线程情况下Parnew肯定不如Serial,上下文切换耗费资源。随着cpu增加,情况会变好,可用-XX:ParallelGCThreads限制垃圾回收的线程数。

参数控制:

-XX:+UseParNewGC ParNew收集器 
-XX:ParallelGCThreads 限制线程数量

3.Parallel Scavenge

一个新生代收集器,并且是多线程,与Parnew的区别:

 

重要的参数有三个,其中两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的 -XX:GCTimeRatio参数。另外一个是UseAdaptiveSizePolicy开关参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。

-XX:+UseAdaptiveSizePolicy是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
https://blog.csdn.net/ffm83/article/details/42872661这篇有实验
 

4.SerialOld

5.Parallel Old

ParallelOld是Parallel Scavenge的老年代版本。以前新生代选择了Parallel Scavenge,只能选择SerialOld用于老年代与之配合。但SerialOld是单线程的,这样配合的吞吐量不给力,也利用不了CPU。

在注重吞吐量和CPU敏感的情况下,可以优先考虑Parallel Scavenge和ParallelOld。

6.CMS(concurrent-mark-sweep) 

CMS是一种以获取最短停顿时间为目标的收集器。在java应用在网站和bs系统这种注重相应速度的地方,CMS是很好的选择。

从名字Mark sweep看出,CMS是基于标记清除算法实现的,它运作的过程有四步:

1. 初始标记(CMS-initial-mark) ,会导致swt; 
2. 并发标记(CMS-concurrent-mark),与用户线程同时运行; 
3. 重新标记(CMS-remark) ,会导致swt; 
4. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;

初始标记和重新标记会到时STW。

初始标记是标记GCRoots能直接关联到的对象,速度很快;

并发标记就是GC Roots Tracing的过程(通过一系列名为”GCRoots”的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,则证明这个对象不可用);

重新标记是修正并发标记期间,因用户程序运行而导致标记变动的那一部分对象记录,这个停顿时间会比初始标记长,但时间比并发标记所用时间短;

优点:并发、停顿时间短

缺点:

 

参数控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器 
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长 
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理 
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

https://blog.csdn.net/zqz_zqz/article/details/70568819这里有一篇对CMS步骤详细分析和实验

7.G1收集器 

JAVA8之后广泛使用,G1 将整个对区域划分为若干个Region,每个Region的大小是2的倍数(1M,2M,4M,8M,16M,32M,通过设置堆的大小和Region数量计算得出。
Region区域划分与其他收集类似,不同的是单独将大对象分配到了单独的region中,会分配一组连续的Region区域

https://www.jianshu.com/p/0f1f5adffdc1

https://blog.csdn.net/ityouknow/article/details/78037470

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

收集步骤:

1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。


4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

8.垃圾回收器组合

转载https://blog.csdn.net/zqz_zqz/article/details/70568819

垃圾回收器组合
young                       Tenured                  JVM options
Serial                        Serial                     -XX:+UseSerialGC
Parallel Scavenge    Serial                     -XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel Scavenge    Parallel Old           -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New或Serial    CMS                  -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1                                                            -XX:+UseG1GC
垃圾回收器从线程运行情况分类有三种

  1. 串行回收,Serial回收器,单线程回收,全程stw;
  2. 并行回收,名称以Parallel开头的回收器,多线程回收,全程stw;
  3. 并发回收,cms与G1,多线程分阶段回收,只有某阶段会stw;

四.一些概念

1.GC日志

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]

100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]

最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

但这里也有个问题:目前所有的垃圾回收器都会出现STW,只是时间长短的问题。总之GC日志中说的GC 和FullGC不是区分新生代老年代的,区域在后面明确写了

 

接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。

如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。

如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

 

后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。 
而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。

再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。

有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以读者看到user或sys时间超过real时间是完全正常的。

2.Minor GC、Major GC和Full GC之间的区别

转载http://www.importnew.com/15820.html

Minor GC:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

  1. 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  2. 内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
  3. 执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
  4. 质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存

Major GC vs Full GC:

大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻带内存应该被设计得简单:

  • Major GC 是清理老年代。
  • Full GC 是清理整个堆空间—包括年轻代和老年代。

很不幸,实际上它还有点复杂且令人困惑。首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。另一方面,许多现代垃圾收集机制会清理部分永久代空间,所以使用“cleaning”一词只是部分正确。

这使得我们不用去关心到底是叫 Major GC 还是 Full GC,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程

3.常用参数

JVM的GC日志的主要参数包括如下几个: 

-verbose:gc 输出简要gc日志
-XX:+PrintGC 输出GC日志 
-XX:+PrintGCDetails 输出GC的详细日志 
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式) 
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) 
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息 
-XX:+PrintGCApplicationStoppedTime // 输出GC造成应用暂停的时间 
-Xloggc:../logs/gc.log 日志文件的输出路径

常用JVM参数 
分析gc日志后,经常需要调整jvm内存相关参数,常用参数如下 

-XX:+PrintCommandLineFlags参数:输出执行的命令

-server:用虚拟机的server模式运行
-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制 
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 
-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。 
在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 
-XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。 
-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:”-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了。 
-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。 
-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。 

jvm的启动参数是:
-server -Xms512m -Xmx1024m -Xss256m  -XX:SurvivorRatio=8 -XX:NewRatio=5 -XX:+UseParNewGC -XX:+HeapDumpOnOutOfMemoryError
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/xx/gc.log

4.eclipse输入jvm参数

右击RUN运行,选择Run Configurations

五.内存分配与回收策略

用代码证明一些常见内存分配和回收规则

1.对象优先在Eden分配

	private static int M=1024*1024;
	
	public static void main(String[] args) {
		byte [] a,b,c,d;
		a=new byte[M*2];
		b=new byte[M*2];
		c=new byte[M*2];

        //这里将会发生一次MinorGc
		d=new byte[M*4];
	}

虚拟机参数:使用serial收集器,设置总内存大小为20,新生代10m,eden8m

-Xms20m -Xmx20m -Xmn10m 
-XX:SurvivorRatio=8
-verbose:gc -XX:+PrintGCDetails
-XX:+PrintCommandLineFlags
-XX:+UseSerialGC

看gc日志,因为内存不够,在新生代发生了一次gc,7130k变成了549k,因为我们先放了6M的对象,然后要再放4M的对象,eden区和一个survivor只有9m,放不下4M了,而前6M还是有用对象,不能被回收到另一个suvivor,只能分配担保进入老年代。gc后的结果就是4M的对象放到eden,6M的对象被放到老年代。

至于为什么4727不是正好的4M?我发现用户程序即使什么对象都不新建,新生代依然会被占用一点 ,说明程序本身会占用一点。但老年代就是正正好好的6M了。

用serial才显示了垃圾回收的过程

2.大对象直接进入老年代 

private static int M=1024*1024;
	
	public static void main(String[] args) {
		byte [] a;
		a=new byte[M*4];
	}

jvm参数:设置大于3m的对象直接进入老年代

-Xms20m -Xmx20m -Xmn10m 
-XX:SurvivorRatio=8
-verbose:gc -XX:+PrintGCDetails
-XX:+PrintCommandLineFlags
-XX:+UseSerialGC
-XX:PretenureSizeThreshold=3m

看到4M的对象直接进入老年代,而新生代也有占用,发现如果用户程序不建对象,这部分新生代依然占用

注意:64位系统下默认的jvm是server模式,而在server下默认的垃圾回收期是Parallel Scvenge,所以手动指定了Serial回收器

3.长期存活的对象进入老年代

这个 规则在jdk1.6后并没有完全遵守,原规则是对象每熬过一minorgc年龄加1,加到15进入老年代。

可以用-XX:MaxTenuringThreadshold=15指定多少年龄进入老年代。但实验中发现即使指定了15,用system.gc()也有可能将对象回收到老年代

4.空间分配担保

在前面,新生代的对象再minorgc后,如果survivor放不下,就会被放到老年代。但如果老年代的连续空间大小小于将要放置的对象怎么办?不是放不下了吗?就会进行一次fullgc。

但是fullgc是消耗资源的,不可能每次都进行fullgc,所以规则是 如果老年代剩余连续空间 小于 以往晋升老年代大小的平均值、或新生代对象的总大小,才进行fullgc,否则就可以直接进行minorgc,如果刚好这次minor的对象很大,放不下了,再进行fullgc。

之前这里有个设置是否允许担保失败的参数,但在jdk1.6后废弃了。目标的规则就是上面的。

5.栈上分配和逃逸分析

1.什么是逃逸:

  逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到;这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量引用。正常的方法调用中,方法体中创建的对象将在执行完毕之后,将回收其中创建的对象;故由于无法回收,即成为逃逸。

  • 全局变量赋值逃逸
  • 方法返回值逃逸
  • 实例引用发生逃逸

线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量.

public class EscapeAnalysis {

     public static Object object;
     
     public void globalVariableEscape(){//全局变量赋值逃逸  
         object =new Object();  
      }  
     
     public Object methodEscape(){  //方法返回值逃逸
         return new Object();
     }
     
     public void instancePassEscape(){ //实例引用发生逃逸
        this.speak(this);
     }
     
     public void speak(EscapeAnalysis escapeAnalysis){
         System.out.println("Escape Hello");
     }
}

2.标量和聚合量

标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。

3.标量替换

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。

4.栈上分配

我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。那就通过标量替换将该对象分解在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力

参考https://www.jianshu.com/p/580f17760f6e

参考https://blog.csdn.net/blueheart20/article/details/52050545

-XX:-DoEscapeAnalysis 关闭逃逸,-XX:+DoEscapeAnalysis 开启逃逸分析,默认是开启的。

上面两篇博客都对逃逸分析进行了实验,实验结果是开启了逃逸分析,所占用堆内存少,而关闭逃逸分析,所有对象都被创建在堆里,会进行gc。

但这些实验只证明了在栈上分配更快,比如下图,所有的对象都是在调用方法内创建的,没有发生逃逸,但gc日志依然有回收,说明对象不是绝对被创建在了栈中,也有在堆里的,只是比关闭逃逸分析后所占用的堆少很多

public class StackOnTest {
	public static void alloc() {
		byte[] b = new byte[2];
		b[0] = 1;
	}
 
	public static void main(String[] args) {
		long b = System.currentTimeMillis();
		for (int i = 0; i < 100000000; i++) {
			alloc();
		}
		long e = System.currentTimeMillis();
		System.out.println(e - b);
	}
}

6.当一个对象从GC Roots无法到达时,不见得会立刻回收

可达性分析算法说的是当一个对象从GC Roots无法到达时,这个对象就可以回收,而不是立刻会被回收。当jvm觉得内存不足不够再放新对象时才会触发回收动作,或者修改垃圾回收器的参数,调整回收的情况比如Parallel Scavenge回收器。

来试一下UseSerialGC的可回收对象的回收时机。

-Xms20m -Xmx20m -Xmn10m 
-XX:SurvivorRatio=8
-verbose:gc -XX:+PrintGCDetails
-XX:+PrintCommandLineFlags
-XX:+UseSerialGC

明显byte c[]作用域过了后,就是可以被回收的对象了。但并不会被立刻回收,依然在新生代中

public static void main(String[] args) {
		byte[] a,b;
		a=new byte [M*2];
		
		{
			byte c[] = new byte[M*2];
		}
}

 即使加上了system.gc也不会被回收,竟然还会被放到老年代中。

public static void main(String[] args) {
		byte[] a,b;
		a=new byte [M*2];
		
		{
			byte c[] = new byte[M*2];
		}
		
		System.gc();
}

再试试堆内存不够的情况,才会触发回收

public static void main(String[] args) {
		byte[] a,b;
		a=new byte [M*2];
//		System.gc();
		for(int i=0;i<5;i++) {
			b=new byte [M*2];
//			byte c[] = new byte[M*2];
		}
	}

日志显示发生了两次回收,理论上执行完代码后,只有a引用的对象是可达的,但最后结果在内存还够的情况下,不只有2M内存被占用,说明还有其他对象存活了下来,说明了垃圾对象只是可回收,不代表立刻会被回收,即使执行了system.gc()

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值