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

 2019-12-22 10:31  阅读(581)
文章分类:JVM

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

1 Java GC垃圾收集

1.1 GC的概念

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

1.2 GC算法

1、引用计数法:

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

201912000191\_1.png

缺点:

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

–很难处理循环引用

201912000191\_2.png

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

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

2、标记清除法:

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

201912000191\_3.png

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

3、标记压缩法:

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

201912000191\_4.png

4、复制算法:

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

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

201912000191\_5.png

缺点:空间浪费

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

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

201912000191\_6.png

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垃圾回收日志
执行结果 GC垃圾回收日志
time:2018time:2121time:2221time:2325time:2425time:2527time:2631time:2731time:2834time:2935time:3035time:3153time:3504time:4218time:4349time:4450time:4551 3.292:[GC3.292:[DefNew:959K->63K(960K),0.0024260secs]523578K->523298K(524224K),0.0024879secs][Times:user=0.02sys=0.00,real=0.00secs]3.296:[GC3.296:[DefNew:959K->959K(960K),0.0000123secs]3.296:[Tenured:523235K->523263K(523264K),0.2820915secs]524195K->523870K(524224K),[Perm:147K->147K(12288K)],0.2821730secs][Times:user=0.26sys=0.00,real=0.28secs]3.579:[FullGC3.579:[Tenured:523263K->523263K(523264K),0.2846036secs]524159K->524042K(524224K),[Perm:147K->147K(12288K)],0.2846745secs][Times:user=0.28sys=0.00,real=0.28secs]3.863:[FullGC3.863:[Tenured:523263K->515818K(523264K),0.4282780secs]524042K->515818K(524224K),[Perm:147K->147K(12288K)],0.4283353secs][Times:user=0.42sys=0.00,real=0.43secs]4.293:[GC4.293:[DefNew:896K->64K(960K),0.0017584secs]516716K->516554K(524224K),0.0018346secs][Times:user=0.00sys=0.00,real=0.00secs]……省略若干…..4.345:[GC4.345:[DefNew:960K->960K(960K),0.0000156secs]4.345:[Tenured:522929K->12436K(523264K),0.0781624secs]523889K->12436K(524224K),[Perm:147K->147K(12288K)],0.0782611secs][Times:user=0.08sys=0.00,real=0.08secs]

红色加粗的地方是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 gen共total 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.4orlator)   注意:此处的大小是(eden+2survivorspace).与jmap-heap中显示的Newgen是不同的。
      整个堆大小=年轻代大小+年老代大小+持久代大小.
      增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize 设置年轻代大小(for1.3/1.4)    
-XX:MaxNewSize 年轻代最大值(for1.3/1.4)    
-XX:PermSize 设置持久代(permgen)初始值 物理内存的1/64  
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4  
-Xss 每个线程的堆栈大小   JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
      一般小的应用,如果栈不是很深,应该是128k够用的大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
      和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:””
      -XssistranslatedinaVMflagnamedThreadStackSize”
      一般设置这个值就可以了。
–XX:ThreadStackSize ThreadStackSize   (0meansusedefaultstacksize)[Sparc:512;Solarisx86:320(was256priorin5.0andearlier);Sparc64bit:1024;Linuxamd64:1024(was0in5.0andearlier);allothers0.]
-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 softlyreachableobjectswillremainaliveforsomeamountoftimeafterthelasttimetheywerereferenced.Thedefaultvalueisonesecondoflifetimeperfreemegabyteintheheap
-XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节新生代采用ParallelScavengeGC时无效
      另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:TLABWasteTargetPercent TLAB占eden区的百分比 1%  
-XX:+CollectGen0First FullGC时是否先YGC FALSE  

4、辅助信息

-XX:+PrintGC   输出形式:[GC118250K->113543K(130112K),0.0094143secs][FullGC121376K->10414K(130112K),0.0650971secs]
-XX:+PrintGC   输出形式:[GC118250K->113543K(130112K),0.0094143secs][FullGC121376K->10414K(130112K),0.0650971secs]
-XX:+PrintGCDetails   输出形式:[GC[DefNew:8614K->781K(9088K),0.0123035secs]118250K->113543K(130112K),0.0124633secs][GC[DefNew:8614K->8614K(9088K),0.0000665secs][Tenured:112761K->10414K(121024K),0.0433488secs]121376K->10414K(130112K),0.0436268secs]
     
-XX:+PrintGCTimeStamps    
-XX:+PrintGC:PrintGCTimeStamps   可与-XX:+PrintGC-XX:+PrintGCDetails混合使用输出形式:11.851:[GC98328K->93620K(130112K),0.0082960secs]
-XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间.可与上面混合使用 输出形式:Totaltimeforwhichapplicationthreadswerestopped:0.0468229seconds
-XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 输出形式:Applicationtime:0.5291524seconds
-XX:+PrintHeapAtGC 打印GC前后的详细堆栈信息  
-Xloggc:filename 把相关日志信息记录到文件以便分析.  
  与上面几个配合使用  
-XX:+PrintClassHistogram garbagecollectsbeforeprintingthehistogram.  
-XX:+PrintTLAB 查看TLAB空间的使用情况  
XX:+PrintTenuringDistribution 查看每次minorGC后新的存活周期的阈值  
     
-XX:+PrintTLAB 查看TLAB空间的使用情况  
XX:+PrintTenuringDistribution 查看每次minorGC后新的存活周期的阈值 Desiredsurvivorsize1048576bytes,newthreshold7(max15)
    newthreshold7即标识新的存活周期的阈值为7。

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

&bsp; ——厚积薄发(yuanxw)

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入JVM虚拟机(三) Java GC垃圾收集

相关推荐