深入JVM虚拟机(三) Java GC垃圾收集

 

 

深入JVM虚拟机() Java GC垃圾收集

 


1 Java GC垃圾收集

1.1 GC的概念

Java GCGarbage Collection,垃圾收集,垃圾回收)机制,是JavaC++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对 JVMJava Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。


1.2 GC算法

1、引用计数法:

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

深入JVM虚拟机(三) Java GC垃圾收集_第1张图片

缺点:

-         引用和去引用伴随加法和减法,影响性能

-         很难处理循环引用

深入JVM虚拟机(三) Java GC垃圾收集_第2张图片

图中3个对象引用值都为1,它们都不可回收。

注:在JAVA中未使用引用计数法。


2、标记清除法:

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

深入JVM虚拟机(三) Java GC垃圾收集_第3张图片

标记出存活对象,将未标记的垃圾对象全部清降,或者标记出拉圾对象,将拉圾对象全部清除。

 

3、标记压缩法:

标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间

深入JVM虚拟机(三) Java GC垃圾收集_第4张图片

4、复制算法:

优势:与标记-清除算法相比,复制算法是一种相对高效的回收方法。不适用于存活对象较多的场合如老年代。

原理:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

深入JVM虚拟机(三) Java GC垃圾收集_第5张图片

缺点:空间浪费

优化:整合标记清理思想:

-         将大对象复制到担保空间(保留空间)回收垃圾几次后,将大对象放到老年代。左侧的表格中将小对象复制到右侧表格中空闲空间。最后清空原来使用的空间。

深入JVM虚拟机(三) Java GC垃圾收集_第6张图片


5、分代思想:

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法:

-         少量对象存活,适合复制算法。

-         大量对象存活,适合标记清理或者标记压缩。


1.3 可触及性

1、可触及性:

可触及的:从根节点开始进行扫描,可以触及到这个对象,那么这个对象就是可触及的。

可复活的:一旦所有引用被释放,就是可复活状态,因为在finalize()中可能复活该对象。

不可触及的:在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,可以回收。

JAVA代码:

public class CanReliveObj {
	public static CanReliveObj obj;

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("CanReliveObj finalize called");
		// GC垃圾回收器,只会调用一次finalize(),obj赋值当前对象,变成了可触及状态
		obj = this;
	}

	@Override
	public String toString() {
		return "I am CanReliveObj";
	}

	public static void main(String[] args) throws InterruptedException {
		// 声明对象
		obj = new CanReliveObj();
		// 将对象赋值给null,一般赋值为null,垃圾回收器,将回收值为null的对象
		obj = null; // 可复活

		// 调用gc()方法,调用对象的finalize()方法,此时obj赋值this
		System.gc();

		// 当前线程睡眠1秒
		Thread.sleep(1000);
		if (obj == null) {
			System.out.println("obj 是 null");
		} else {
			System.out.println("obj 可用");
		}

		/*
		 * 由于finalize()方法只会在调用gc()的时候调用一次,
		 * 调用gc()方法过后,不会再调用finalize()方法,此时对象为null。
		 */
		System.out.println("第二次gc");
		obj = null; // 不可复活
		System.gc();
		Thread.sleep(1000);
		if (obj == null) {
			System.out.println("obj 是 null");
		} else {
			System.out.println("obj 可用");
		}
	}
}

经验:

 避免使用finalize(),操作不慎可能导致错误。

 优先级低,何时被调用,不确定,何时发生GC不确定。

可以使用try-catch-finally来替代它。

2、根:

-         栈中引用的对象

-         方法区中静态成员或者常量引用的对象(全局对象)

-         JNI方法栈中引用对象


1.4 Stop-The-World

产生Stop The World的原因:

Java中一种全局暂停的现象,所有的线程全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互,多半由于GC引起。

GC开始工作时,将现在在进行的线程全部都停止,以保证不会再有先的垃圾产生。如果不能暂定正在进行线程,垃圾清理的清况就无法得到保证(Sun将这件事情称为“Stop The World”)。

影响:

  长时间服务停止,没有响应。

遇到HA系统,可能引起主备切换,严重危害生产环境。

Java代码:

	public class PrintThread extends Thread{
		public static final long starttime=System.currentTimeMillis();
		@Override
		public void run(){
			try{
				while(true){
					long t=System.currentTimeMillis()-starttime;
					System.out.println("time:"+t);
					Thread.sleep(100);
				}
			}catch(Exception e){
				e.printStackTrace();
			}
		}
		public static void main(String[] args) {
			PrintThread printThread = new PrintThread();
			printThread.start();
		}
	}

预期,应该是每秒中有10条输出:

执行结果

GC垃圾回收日志

time:2018

time:2121

time:2221

time:2325

time:2425

time:2527

time:2631

time:2731

time:2834

time:2935

time:3035

time:3153

time:3504

time:4218

time:4349

time:4450

time:4551

3.292: [GC3.292: [DefNew: 959K->63K(960K), 0.0024260 secs] 523578K->523298K(524224K), 0.0024879 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]


3.296: [GC3.296: [DefNew: 959K->959K(960K), 0.0000123 secs]3.296: [Tenured: 523235K->523263K(523264K), 0.2820915 secs] 524195K->523870K(524224K), [Perm : 147K->147K(12288K)], 0.2821730 secs] [Times: user=0.26 sys=0.00, real=0.28 secs]


3.579: [Full GC3.579: [Tenured: 523263K->523263K(523264K), 0.2846036 secs] 524159K->524042K(524224K), [Perm : 147K->147K(12288K)], 0.2846745 secs] [Times: user=0.28 sys=0.00, real=0.28 secs]


3.863: [Full GC3.863: [Tenured: 523263K->515818K(523264K), 0.4282780 secs] 524042K->515818K(524224K), [Perm : 147K->147K(12288K)], 0.4283353 secs] [Times: user=0.42 sys=0.00, real=0.43 secs]


4.293: [GC4.293: [DefNew: 896K->64K(960K), 0.0017584 secs] 516716K->516554K(524224K), 0.0018346 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

……省略若干…..

4.345: [GC4.345: [DefNew: 960K->960K(960K), 0.0000156 secs]4.345: [Tenured: 522929K->12436K(523264K), 0.0781624 secs] 523889K->12436K(524224K), [Perm : 147K->147K(12288K)], 0.0782611 secs] [Times: user=0.08 sys=0.00, real=0.08 secs]

       红色加粗的地方是GC引起的线程停顿(Stop The World)现象。垃圾回收的时间基本上是等于停顿的时间。


2 GC参数设置

2.1 简要GC信息

Eclipse中设置eclipse.ini文件。

3、打开eclipse安装的根目录下%{Eclipse_HOME}\eclipse.ini,在文件的末尾添加参数:

-XX:+PrintGC

-verbose:gc

-Xloggc:../logs/jvm-gc/gc.log

4、PrintGC打印GC的简要信息

[GC 4790K->374K(15872K), 0.0001606 secs]

[GC 4790K->374K(15872K), 0.0001474 secs]

[GC 4790K->374K(15872K), 0.0001563 secs]

[GC 4790K->374K(15872K), 0.0001682 secs]

GC之前使用:4790K

GC之后使用:374K

整个堆的大小:15872K


2.2 详细GC信息

1、打开eclipse安装的根目录下%{Eclipse_HOME}\eclipse.ini,在文件的末尾添加参数:

-XX:+PrintHeapAtGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-verbose:gc

-Xloggc:../logs/jvm-gc/gc.log


2、PrintGCDetails打印详信息,PrintGCTimeStamps打印时间戳,如:

[GC[DefNew: 4416K->0K(4928K), 0.0001897secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00,real=0.00 secs]

GC之前使用:4416K

GC之后使用:0

整个堆的大小:4928K

 

3、PrintHeapAtGC打印详信息

程序运行结束后会将整个堆的运行状态,打进行印:

Heap

 def new generation   total13824K, used 11223K [0x27e80000,0x28d80000,0x28d80000)

  eden space12288K,  91% used [0x27e80000, 0x28975f20, 0x28a80000)

  from space1536K,   0% used [0x28a80000, 0x28a80000, 0x28c00000)

  to   space1536K,   0% used [0x28c00000,0x28c00000, 0x28d80000)

 tenured generation   total5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)

   the space 5120K,   0%used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000)

 compacting perm gen  total 12288K,used 142K [0x34680000, 0x35280000, 0x38680000)

   the space 12288K,   1%used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000)

    ro space 10240K,  44%used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)

rw space 12288K,  52% used [0x39080000,0x396cdd28, 0x396cde00, 0x39c80000)

新生代def new generation空间共:total 13824K

已经使用:used 11223K

低边界:0x27e80000

当前边界:0x28d80000

最高边界:0x28d80000

新生代内存:(0x28d80000-0x27e80000)/1024/1024=15M

新生代总合:(12288K+1536K+1536K) /1024=15M

新生代可申请内存:13824K = 12288K + 1536K

 

生成对象eden空间:space12288K,使用量为91%

新生代from to两个值是相等的。

 

老年代tenured generation空间共:total 5120K

已经使用:used 0k

 

方法区compacting perm gentotal 12288K

已经使用:used 142K

 

2.3 指定最大堆和最小堆

-Xmx参数:最大堆

-Xms参数:最小堆

1、-Xmx1024 –Xms256m

运行代码:

        public static void main(String[] args) {
            System.out.print("Xmx=");
            System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
    
            System.out.print("freemem=");
            System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
    
            System.out.print("totalmem=");
            System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
        }

        运行结果:

            Xmx=1811.5M

freemem=120.04971313476562M

total mem=122.0M

 

2、堆分配参数总结:

-  根据实际事情调整新生代和幸存代的大小。

-  官方推存新生代占堆的3/8

-  幸存代占新生代的1/10

 

3、内存参数总结:

参数名称

含义

默认值

说明

-Xms

初始堆大小

物理内存的1/64(<1GB)

默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.

-Xmx

最大堆大小

物理内存的1/4(<1GB)

默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

-Xmn

年轻代大小(1.4or lator)

 

注意:此处的大小是(eden+ 2 survivor space).jmap -heap中显示的New gen是不同的。

 

 

 

整个堆大小=年轻代大小 +年老代大小 +持久代大小.

 

 

 

增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

-XX:NewSize

设置年轻代大小(for 1.3/1.4)

 

 

-XX:MaxNewSize

年轻代最大值(for 1.3/1.4)

 

 

-XX:PermSize

设置持久代(perm gen)初始值

物理内存的1/64

 

-XX:MaxPermSize

设置持久代最大值

物理内存的1/4

 

-Xss

每个线程的堆栈大小

 

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

 

 

 

一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)

 

 

 

threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"

 

 

 

-Xss is translated in a VM flag named ThreadStackSize

 

 

 

一般设置这个值就可以了。

-XX:ThreadStackSize

Thread Stack Size

 

(0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]

-XX:NewRatio

年轻代(包括Eden和两个Survivor)与年老代的比值(除去持久代)

 

-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

 

 

 

Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

-XX:SurvivorRatio

Eden区与Survivor区的大小比值

 

设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

-XX:LargePageSizeInBytes

内存页的大小不可设置过大, 会影响Perm的大小

 

=128m

-XX:+UseFastAccessorMethods

原始类型的快速优化

 

 

-XX:+DisableExplicitGC

关闭System.gc()

 

这个参数需要严格的测试

-XX:MaxTenuringThreshold

垃圾最大年龄

 

如果设置为0的话,则年轻代对象不经过Survivor,直接进入年老代.对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率

 

 

 

该参数只有在串行GC时才有效.

-XX:+AggressiveOpts

加快编译

 

 

-XX:+UseBiasedLocking

锁机制的性能改善

 

 

-Xnoclassgc

禁用垃圾回收

 

 

-XX:SoftRefLRUPolicyMSPerMB

每兆堆空闲空间中SoftReference的存活时间

1s

softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap

-XX:PretenureSizeThreshold

对象超过多大是直接在旧生代分配

0

单位字节 新生代采用Parallel Scavenge GC时无效

 

 

 

另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.

-XX:TLABWasteTargetPercent

TLABeden区的百分比

1%

 

-XX:+CollectGen0First

FullGC时是否先YGC

FALSE

 

 

 

4、辅助信息

-XX:+PrintGC

 

输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails

 

输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

 

 

 

-XX:+PrintGCTimeStamps

 

 

-XX:+PrintGC:PrintGCTimeStamps

 

可与-XX:+PrintGC -XX:+PrintGCDetails混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationStoppedTime

打印垃圾回收期间程序暂停的时间.可与上面混合使用

输出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:+PrintGCApplicationConcurrentTime

打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用

输出形式:Application time: 0.5291524 seconds

-XX:+PrintHeapAtGC

打印GC前后的详细堆栈信息

 

-Xloggc:filename

把相关日志信息记录到文件以便分析.

 

 

与上面几个配合使用

 

-XX:+PrintClassHistogram

garbage collects before printing the histogram.

 

-XX:+PrintTLAB

查看TLAB空间的使用情况

 

XX:+PrintTenuringDistribution

查看每次minor GC后新的存活周期的阈值

 

 

 

 

-XX:+PrintTLAB

查看TLAB空间的使用情况

 

XX:+PrintTenuringDistribution

查看每次minor GC后新的存活周期的阈值

Desired survivor size 1048576 bytes, new threshold 7 (max 15)

 

 

new threshold 7即标识新的存活周期的阈值为7

 


 

                --以上为《深入JVM虚拟机(三) Java GC垃圾收集》,如有不当之处请指出,我后续逐步完善更正,大家共同提高。谢谢大家对我的关注。

                                                                                                                                                                                      ——厚积薄发(yuanxw)


你可能感兴趣的:(#,JVM虚拟机)