JVM点滴

JVM

  1. java拥有GC,为什么还会内存泄漏?

    1. 理解什么是内存泄漏: Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。
    2. Java为了简化编程工作,对于不再使用的对象直接交给了GC,这一点区别于c/c++,c/c++认为内存太重要啦我要自己管理,java说内存太重要啦不能让你管理,但是实际的工作中的情况是:GC不在内存十分紧张的情况下是不做GC操作的,谁也不想Stop The World,耗时。。。各种不爽,实在无奈还是执行下GC操作,但是对于一些对象尽管不使用了,还是清理不到,有的压根就不在清理的列表中,这个要看jvm判断对象已死的策略,引用计数算法,可达性分析算法【有向图实现】
    3. 【场景一】如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露,如下所示
    public class Simple {
        Object object;
        public void method1(){
            object = new Object();
            //...其他代码
            /* 如果object仅仅为method1服务的话,就导致生命周期不一致,导致只有回收Simple对象的时候才会回收object,这就是内存泄漏
             * 不要这样写代码,非要执着的话解决办法就是执行完业务以后,将object指向null
             */
            object = null;
        }
    }
    

    现在实现Stack的pop方法想想怎么实现

    public E pop(){
        if(size == 0)
            return null;
        else
            // 造成内存溢出:elementData[size-1]依然持有E类型对象的引用,并且暂时不能被GC回收
            return (E) elementData[--size];
    }
    

    再来看看jdk1.8 官方的实现代码

    // Stack.class实现
    public synchronized E pop() {
        E       obj;
        int     len = size();
    
        obj = peek();
        removeElementAt(len - 1);
    
        return obj;
    }
    public synchronized E peek() {
        int     len = size();
    
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
    // Vector.calss 实现
    public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }
    

    So,在方法中操作生命周期不属于这个栈空间的对象时,一定注意看看是不是需要设置为null
    4. 【场景二】容器使用时的内存泄露
    5. 【场景三】各种提供了close()方法的对象
    *. 比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,以及使用其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因依然是长生命周期对象持有短生命周期对象的引用。
    6. 【场景四】单例模式导致的内存泄露
    *. 单例模式,很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用,也很容易发生内存泄露。

  2. JVM内存分配

  3. new一个对象在JVM中的过程

    1. 虚拟机遇到一个new指令,首先到常量池中是否可以匹配一个类的符号引用,并检查这个符号引用是否已经被加载、解析、初始化,如果没有的话那就必须先执行类的加载
    2. 加载通过,分配新生对象的内存,一个对象需要内存的大小在加载完成之后就完全确定【这个里面还是有很多的细节】
    3. 内存分配完成后,jvm需要将分配到的内存空间初始化为零值
    4. jvm要对对象进行必要的设置,例如这个对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的gc分代年龄,这些信息都是放置在对象头之中
    5. 上面工作完成之后,从jvm角度来看,一个新的对象就产生啦,但是从java程序的角度,对象创建才刚刚开始,按照程序的意愿做初始化,完成后一个正真的对象才算产生啦
  4. 对象的死亡过程 java finalize方法总结、GC执行finalize的过程

    1. 首先是可达性分析中的不可达对象
    2. 第一次筛选,该对象是否有必要执行finalize方法,当对象没有覆盖finalize或者finalize已经被jvm执行了,这两种情况视为“没有必要执行”
    3. 如果判断有必要执行,放置在一个低优先级的F-Queue队列中
    4. 第二次筛选就是执行finalize过程中获得新的连接就可以逃出升天啦,不被回收啦
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        // willGc是GC不可达的对象,此时获得新的连接,就可以不被回收
        willGc = this;
    }
    
  5. inalize方法

    • Object对象中的方法,gc准备释放内存的时候,会先调用finalize()
    • 对象不一定会被回收
    • 垃圾回收并不是析构函数
    • 垃圾回收只与内存有关
    • 垃圾回收和finalize()都是靠不住的,只要JVM还没有快到耗尽内存的地步,它是不会浪费时间进行垃圾回收的
    • 总结: 别指望他做一个对象被回收的收尾工作,压根靠不住,鬼知道什么时候执行
    • 那为什么还要设计它呢? 主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存
  6. 怎么判断一个对象是可回收的标准?

    • 引用计数算法,无法解决对象间循环应用的问题
    • 可达性分析算法,GC Root
  7. 什么是引用?

    • reference类型中存储的数值是另一块内存的起始地址,在内存中代表一个引用
    引用类型 说明
    强引用 普遍存在的,new的对象,只要存在jvm永远不会回收
    软引用 有用但非必要的对象,系统发生内存溢出之前,将会把这些对象作为回收,如果此时还是出现溢出,才跑出内存溢出
    弱引用 非必要的对象,只能活到下次gc之前
    虚引用 无法通过引用获取对象的实例;存在的唯一目的就是能在对象被回收的时候收到一条系统通知
  8. 垃圾收集器

  9. OOM出现了,怎么解决

    • 造成OutOfMemoryError原因一般有2种:

      1. 内存泄露,对象已经死了,无法通过垃圾收集器进行自动回收,通过找出泄露的代码位置和原因,才好确定解决方案;
      2. 内存溢出,内存中的对象都还必须存活着,这说明Java堆分配空间不足,检查堆设置大小(-Xmx与-Xms),检查代码是否存在对象生命周期太长、持有状态时间过长的情况。
    • 内存管理工具Memory Analyzer的使用

JVM 调优

  1. jvm命令行工具

  2. 实践Java程序内存分析:使用mat工具分析内存占用

  3. MAT


堆大小设置

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
典型设置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
回收器选择

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

  1. 吞吐量优先的并行收集器
    如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

典型配置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC             -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
  1. 响应时间优先的并发收集器
    如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

典型配置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
常见配置汇总
  1. 堆设置
  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -XX:NewSize=n:设置年轻代大小
  • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  • -XX:MaxPermSize=n:设置持久代大小
  1. 收集器设置
  • -XX:+UseSerialGC:设置串行收集器
  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器
  1. 垃圾回收统计信息
  • -XX:+PrintGC
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename
  1. 并行收集器设置
  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
  • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  1. 并发收集器设置
  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
调优总结
  1. 年轻代大小选择
  • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
  • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  1. 年老代大小选择
  • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

    1. 并发垃圾收集信息
    2. 持久代并发收集次数
    3. 传统GC信息
    4. 花在年轻代和年老代回收上的时间比例
      减少年轻代和年老代花费的时间,一般会提高应用的效率
  • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

  1. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

    1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
posted @ 2017-06-02 10:28  王半农  阅读(185)  评论(0编辑  收藏  举报