zoukankan      html  css  js  c++  java
  • Java 内存管理总结

    1、Java中的finalize

    finalize-方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。

    垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作(如关闭流等操作)。

           Finalize 操作具有下列限制:

    垃圾回收过程中执行终结器的准确时间是不确定的。不保证资源在任何特定的时间都能释放,除非调用 Close 方法或 Dispose 方法。

    即使一个对象引用另一个对象,也不能保证两个对象的终结器以任何特定的顺序运行。即,如果对象 A 具有对对象 B 的引用,并且两者都有终结器,则当对象 A 的终结器启动时,对象 B 可能已经终结了。

    运行终结器的线程是未指定的。

    在下面的异常情况下,Finalize 方法可能不会运行完成或可能根本不运行:

    另一个终结器无限期地阻止(进入无限循环,试图获取永远无法获取的锁,诸如此类)。由于运行时试图运行终结器来完成,所以如果一个终结器无限期地阻止,则可能不会调用其他终结器。

    进程终止,但不给运行时提供清理的机会。在这种情况下,运行时的第一个进程终止通知是 DLL_PROCESS_DETACH 通知。

    在关闭过程中,只有当可终结对象的数目继续减少时,运行时才继续 Finalize 对象。

    如果 Finalize 或 Finalize 的重写引发异常,并且运行库并非寄宿在重写默认策略的应用程序中,则运行库将终止进程,并且不执行任何活动的 try-finally 块或终结器。如果终结器无法释放或销毁资源,此行为可以确保进程完整性。

    给实现者的说明 默认情况下,Object.Finalize 不执行任何操作。只有在必要时才必须由派生类重写它,因为如果必须运行 Finalize 操作,垃圾回收过程中的回收往往需要长得多的时间。 如果 Object 保存了对任何资源的引用,则 Finalize 必须由派生类重写,以便在垃圾回收过程中,在放弃 Object 之前释放这些资源。 当类型使用文件句柄或数据库连接这类在回收使用托管对象时必须释放的非托管资源时,该类型必须实现 Finalize。有关辅助和具有更多控制的资源处置方式,请参见 IDisposable 接口。 Finalize 可以采取任何操作,包括在垃圾回收过程中清理了对象后使对象复活(即,使对象再次可访问)。但是,对象只能复活一次;在垃圾回收过程中,不能对复活对象调用 Finalize。

    析构函数是执行清理操作的 C# 机制。析构函数提供了适当的保护措施,如自动调用基类型的析构函数。在 C# 代码中,不能调用或重写 Object.Finalize。

    2、Java内存分配解析

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识。一般Java在内存分配时会涉及到以下区域:

    ◆寄存器:我们在程序中无法控制

    ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中

    ◆堆:存放用new产生的数据

    ◆静态域:存放在对象中用static定义的静态成员

    ◆常量池:存放常量

    ◆非RAM存储:硬盘等永久存储空间

     

    常量池 (constant pool)

    常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

    ◆类和接口的全限定名;

    ◆字段的名称和描述符;

    ◆方法和名称和描述符。

       

        解释几段程序:

            String str1="Hello";

           String str2="Hello";

           String str3 = new String("Hello");

           String str4 = new String("Hello");

           System.out.println(str1==str2); 

    结果为true,因为 str1和str2指向同一块常量池内存块。类似C中得指针指向同一个内存地址.

           System.out.println(str1==str3);

    结果为false,因为 str1和str2指向同一块常量池内存块。类似C中得指针指向同一个内存地址.

           System.out.println(str3==str4);

        结果为false,因为str3和str4虽然所指向的字符串对象是等值的,但是所指向的区域是不同的,就像是比较指针 ,呵呵 ,有点怀念C++ 了。

    再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的 intern()方法就是扩充常量池的 一个方法;当一个String实例str调用intern()方法时,Java 查找常量池中 是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常 量池中增加一个Unicode等于str的字符串并返回它的引用。

       

        再解释一段程序:

           String str1="Hello";

           String str2= new String("Hello");

           System.out.println(str1==str2.intern());

        结果为true,因为string调用intern()方法,会到常量池去寻找“Hello”常量,如果找到就直接把这个常量的引用返回了,所以str1和str2指向的是同一个引用。

    3、Java的垃圾回收算法

    * 引用计数

        该算法在Java虚拟机没被使用过,主要是循环引用问题,因为计数并不记录谁指向他,无法发现这些交互自引用对象。(感觉有点类似C++中的智能指针的计数机制)

        -- 怎么计数?

            当引用连接到对象时,对象计数加1

            当引用离开作用域或被置为null时减1

        -- 怎么回收?

            遍历对象列表,计数为0就释放

        -- 有什么问题?

            循环引用问题。A引用B,B引用A,那么A,B就永远都不会被释放

    * 标记算法

        标记算法的思想是从堆栈和静态存储区的对象开始,遍历所有引用,标记活得对象。

        对于标记后有两种处理方式:

      (1) 停止-复制

        -- 所谓停止,就是停止在运行的程序,进行垃圾回收

        -- 所谓复制,就是将活得对象复制到另外一个堆上,以使内存更紧凑

        -- 优点在于,当大块内存释放时,有利于整个内存的重分配

        -- 有什么问题?

            一、停止,干扰程序的正常运行,

    二、复制,明显耗费大量时间,

    三,如果程序比较稳定,垃圾比较少,那么每次重新复制量是非常大的,非常不合算

     -- 什么时候启动停止-复制?

      内存数量较低时,具体多低我也不知道

     (2) 清除 也称标记-清除算法

    -- 也就是将标记为非活得对象释放,也必须暂停程序运行

        -- 优点就是在程序比较稳定,垃圾比较少的时候,速度比较快

        -- 有什么问题?

           很显然停止程序运行是一个问题,只清除也会造成很对内存碎片。

        -- 为什么这2个算法都要暂停程序运行?

          这是因为,如果不暂停,刚才的标记会被运行的程序弄乱

        关于Java垃圾回收算法不存在一个绝对完美的算法。垃圾回收算法还是一个复杂的研究领域,不存在任何简单而通用的解决方案,因此标记-清除只是一个用来理解垃圾回收机制的相对简单的智力模型。每种Java虚拟机都有自己的垃圾回收模型策略。

    4、JVM内存模型

    Java堆的描述如下:

    内存由 Perm 和 Heap 组成. 其中

    Heap = {Old + NEW = { Eden , from, to } }

    JVM内存模型中分两大块,一块是 NEW Generation, 另一块是Old Generation. 在New Generation中,有一个叫Eden的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from,to), 它们用来存放每次垃圾回收后存活下来的对象。在Old Generation中,主要存放应用程序中生命周期长的内存对象,还有个Permanent Generation,主要用来放JVM自己的反射对象,比如类对象和方法对象等。

    垃圾回收描述:

    在New Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个Survivor Space, 当Survivor Space空间满了后, 剩下的live对象就被直接拷贝到Old Generation中去。因此,每次GC后,Eden内存块会被清空。在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求.
    垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收NEW中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

    当一个URL被访问时,内存申请过程如下:
    A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
    B. 当Eden空间足够时,内存申请结束。否则到下一步
    C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
    D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
    E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
    F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”

    JVM调优建议:

    ms/mx:定义YOUNG+OLD段的总尺寸,ms为JVM启动时YOUNG+OLD的内存大小;mx为最大可占用的YOUNG+OLD内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
    NewSize/MaxNewSize:定义YOUNG段的尺寸,NewSize为JVM启动时YOUNG的内存大小;MaxNewSize为最大可占用的YOUNG内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
    PermSize/MaxPermSize:定义Perm段的尺寸,PermSize为JVM启动时Perm的内存大小;MaxPermSize为最大可占用的Perm内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
    SurvivorRatio:设置Survivor空间和Eden空间的比例

     

    内存溢出的可能性
    1. OLD段溢出
    这种内存溢出是最常见的情况之一,产生的原因可能是:
    1) 设置的内存参数过小(ms/mx, NewSize/MaxNewSize)
    2) 程序问题
        单个程序持续进行消耗内存的处理,如循环几千次的字符串处理,对字符串处理应建议使用StringBuffer。此时不会报内存溢出错,却会使系统持续垃圾收集,无法处理其它请求,相关问题程序可通过Thread Dump获取(见系统问题诊断一章)单个程序所申请内存过大,有的程序会申请几十乃至几百兆内存,此时JVM也会因无法申请到资源而出现内存溢出,对此首先要找到相关功能,然后交予程序员修改,要找到相关程序,必须在Apache日志中寻找。
        当Java对象使用完毕后,其所引用的对象却没有销毁,使得JVM认为他还是活跃的对象而不进行回收,这样累计占用了大量内存而无法释放。由于目前市面上还没有对系统影响小的内存分析工具,故此时只能和程序员一起定位。

    2. Perm段溢出
    通常由于Perm段装载了大量的Servlet类而导致溢出,目前的解决办法:
    1) 将PermSize扩大,一般256M能够满足要求
    2) 若别无选择,则只能将servlet的路径加到CLASSPATH中,但一般不建议这么处理
    3. C Heap溢出
    系统对C Heap没有限制,故C Heap发生问题时,Java进程所占内存会持续增长,直到占用所有可用系统内存

    其他:

    JVM有2个GC线程。第一个线程负责回收Heap的Young区。第二个线程在Heap不足时,遍历Heap,将Young 区升级为Older区。Older区的大小等于-Xmx减去-Xmn,不能将-Xms的值设的过大,因为第二个线程被迫运行会降低JVM的性能。

    为什么一些程序频繁发生GC?有如下原因:

     1、 程序内调用了System.gc()或Runtime.gc()。

             2、一些中间件软件调用自己的GC方法,此时需要设置参数禁止这些GC。

             3、Java的Heap太小,一般默认的Heap值都很小。

             4、频繁实例化对象,Release对象。此时尽量保存并重用对象,例如使用StringBuffer()和String()。

    如果你发现每次GC后,Heap的剩余空间会是总空间的50%,这表示你的Heap处于健康状态。许多Server端的Java程序每次GC后最好能有65%的剩余空间。

    经验之谈:

    1.Server端JVM最好将-Xms和-Xmx设为相同值。为了优化GC,最好让-Xmn值约等于-Xmx的1/3[2]。

    2.一个GUI程序最好是每10到20秒间运行一次GC,每次在半秒之内完成[2]。

    注意:

    1.增加Heap的大小虽然会降低GC的频率,但也增加了每次GC的时间。并且GC运行时,所有的用户线程将暂停,也就是GC期间,Java应用程序不做任何工作。

    2.Heap大小并不决定进程的内存使用量。进程的内存使用量要大于-Xmx定义的值,因为Java为其他任务分配内存,例如每个线程的Stack等。

    2.Stack的设定

    每个线程都有他自己的Stack。

    -Xss

    每个线程的Stack大小

    Stack的大小限制着线程的数量。如果Stack过大就好导致内存溢漏。-Xss参数决定Stack大小,例如-Xss1024K。如果Stack太小,也会导致Stack溢漏。

    3.硬件环境

    硬件环境也影响GC的效率,例如机器的种类,内存,swap空间,和CPU的数量。

    如果你的程序需要频繁创建很多transient对象,会导致JVM频繁GC。这种情况你可以增加机器的内存,来减少Swap空间的使用[2]。

    4.4种GC

    第一种为单线程GC,也是默认的GC。,该GC适用于单CPU机器。

    第二种为Throughput GC,是多线程的GC,适用于多CPU,使用大量线程的程序。第二种GC与第一种GC相似,不同在于GC在收集Young区是多线程的,但在Old区和第一种一样,仍然采用单线程。-XX:+UseParallelGC参数启动该GC。

    第三种为Concurrent Low Pause GC,类似于第一种,适用于多CPU,并要求缩短因GC造成程序停滞的时间。这种GC可以在Old区的回收同时,运行应用程序。-XX:+UseConcMarkSweepGC参数启动该GC。

    第四种为Incremental Low Pause GC,适用于要求缩短因GC造成程序停滞的时间。这种GC可以在Young区回收的同时,回收一部分Old区对象。-Xincgc参数启动该GC。

    5、垃圾回收步骤(淘宝培训)

        垃圾回收步骤:

    • • 1、对象在Eden区完成内存分配
    • • 2、当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收
    • • 3、minorGC时,Eden不能被回收的对象被放入到空的survivor(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的
    • • 4、当做第3步的时候,如果发现survivor满了,则这些对象被copy到old区,或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区 XX:MaxTenuringThreshold
    • • 5、当Old区被放满的之后,进行完整的垃圾回收
    • 数据进入年老代的3个途径
    • 直接进入Old区
    • 超过指定size的数据
    • 比较少见,一般一下子申请大片缓冲区
    • minorGC触发时,交换分区S0或者S1放不下
    • 缓存数据
    • 因为线程执行周期缓慢导致未释放的对象的量太多了
    • 足够老的数据,在交换区拷贝次数超过了上限( XX:MaxTenuringThreshold=15)
    • 缓存数据
    • 因为线程执行周期缓慢导致未释放的对象

    6、QPS的三要素(来自淘宝培训)

    1. 1.  线程 
    2. 2.  响应时间
    3. 3.  瓶颈资源

    QPS是由瓶颈资源决定的

    a)   线程:从系统,从硬件来看,单兵作战的时代已经过去了,线程作为业务逻辑执行的载体,和QPS有着紧密的联系,特别是线程数量的多少将直接影响QPS,今天我会告诉你一个线程数量计算的方法

    b)   响应时间:第一个感觉是响应时间越快,那么QPS越高,其实这个也不是一定的,因为我刚刚说的第一句话是QPS是由瓶颈资源决定的,响应时间是一个微妙的因素,我们需要在一定范围内进行理解

    c)   瓶颈资源,连续的3个收费站,由门口最少的收费站决定QPS,为什么举收费站的例子,因为这个例子和实际很像,那个挡板下来的时候你就独占了这个门,然后处理业务,处理完成了就释放了这个资源

    d)   增加对于这3者的理解,可以让我们更加清晰的认识QPS的提升的本质,让我们一眼就可以看出是什么原因导致了QPS的上升和下降,而不是盲目的进行优化 

  • 相关阅读:
    Python 容器用法整理
    C/C++中浮点数格式学习——以IEEE75432位单精度为例
    关于C/C++中的位运算技巧
    [GeekBand] C++11~14
    [GeekBand] 探讨C++新标准之新语法——C++ 11~14
    [GeekBand] 面向对象的设计模式(C++)(2)
    [GeekBand] 面向对象的设计模式(C++)(1)
    [GeekBand] STL与泛型编程(3)
    [GeekBand] STL与泛型编程(2)
    [GeekBand] STL与泛型编程(1)
  • 原文地址:https://www.cnblogs.com/coser/p/2238341.html
Copyright © 2011-2022 走看看