精华内容
下载资源
问答
  • 文章给各位介绍在使用JAVA时发现java进程占用高内存了,如果你也碰到这种情况我们可以参考下文来进行优化。首先看一下一个java进程的jmap输出: 代码如下 复制代码 [lex@chou ~]$ jmap -heap 837Attaching to ...

    文章给各位介绍在使用JAVA时发现java进程占用高内存了,如果你也碰到这种情况我们可以参考下文来进行优化。

    首先看一下一个java进程的jmap输出:

     代码如下复制代码

    [lex@chou ~]$ jmap -heap 837

    Attaching to process ID 837, please wait...

    Debugger attached successfully.

    Server compiler detected.

    JVM version is 20.10-b01

    using thread-local object allocation.

    Parallel GC with 2 thread(s)

    Heap Configuration:

    MinHeapFreeRatio = 40

    MaxHeapFreeRatio = 70

    MaxHeapSize      = 4294967296 (4096.0MB)

    NewSize          = 1310720 (1.25MB)

    MaxNewSize       = 17592186044415 MB

    OldSize          = 5439488 (5.1875MB)

    NewRatio         = 2

    SurvivorRatio    = 8

    PermSize         = 21757952 (20.75MB)

    MaxPermSize      = 85983232 (82.0MB)

    Heap Usage:

    PS Young Generation

    Eden Space:

    capacity = 41025536 (39.125MB)

    used     = 18413552 (17.560531616210938MB)

    free     = 22611984 (21.564468383789062MB)

    44.883147900858624% used

    From Space:

    capacity = 4325376 (4.125MB)

    used     = 3702784 (3.53125MB)

    free     = 622592 (0.59375MB)

    85.60606060606061% used

    To Space:

    capacity = 4521984 (4.3125MB)

    used     = 0 (0.0MB)

    free     = 4521984 (4.3125MB)

    0.0% used

    PS Old Generation

    capacity = 539820032 (514.8125MB)

    used     = 108786168 (103.74657440185547MB)

    free     = 431033864 (411.06592559814453MB)

    20.152302906758376% used

    PS Perm Generation

    capacity = 85983232 (82.0MB)

    used     = 60770232 (57.95500946044922MB)

    free     = 25213000 (24.04499053955078MB)

    70.67684080542588% used

    然后再用ps看看:

     代码如下复制代码
    [lex@chou ~]$ ps -p 837 -o vsz,rss

    VSZ   RSS

    7794992 3047320

    关于这里的几个generation网上资料一大把就不细说了,这里算一下求和可以得知前者总共给Java环境分配了644M的内存,而ps输出的VSZ和RSS分别是7.4G和2.9G,这到底是怎么回事呢?

    前面jmap输出的内容里,MaxHeapSize 是在命令行上配的,-Xmx4096m,这个java程序可以用到的最大堆内存。

    VSZ是指已分配的线性空间大小,这个大小通常并不等于程序实际用到的内存大小,产生这个的可能性很多,比如内存映射,共享的动态库,或者向系统申请了更多的堆,都会扩展线性空间大小,要查看一个进程有哪些内存映射,可以使用 pmap 命令来查看:

     代码如下复制代码
    [lex@chou ~]$ pmap -x 837

    837:   java

    Address           Kbytes     RSS   Dirty Mode   Mapping

    0000000040000000      36       4       0 r-x--  java

    0000000040108000       8       8       8 rwx--  java

    00000000418c9000   13676   13676   13676 rwx--    [ anon ]

    00000006fae00000   83968   83968   83968 rwx--    [ anon ]

    0000000700000000  527168  451636  451636 rwx--    [ anon ]

    00000007202d0000  127040       0       0 -----    [ anon ]

    ...

    ...

    00007f55ee124000       4       4       0 r-xs-  az.png

    00007fff017ff000       4       4       0 r-x--    [ anon ]

    ffffffffff600000       4       0       0 r-x--    [ anon ]

    ----------------  ------  ------  ------

    total kB         7796020 3037264 3023928

    这里可以看到很多anon,这些表示这块内存是由mmap分配的。

    RSZ是Resident Set Size,常驻内存大小,即进程实际占用的物理内存大小, 在现在这个例子当中,RSZ和实际堆内存占用差了2.3G,这2.3G的内存组成分别为:

    JVM本身需要的内存,包括其加载的第三方库以及这些库分配的内存

    NIO的DirectBuffer是分配的native memory

    内存映射文件,包括JVM加载的一些JAR和第三方库,以及程序内部用到的。上面 pmap 输出的内容里,有一些静态文件所占用的大小不在Java的heap里,因此作为一个Web服务器,赶紧把静态文件从这个Web服务器中人移开吧,放到nginx或者CDN里去吧。

    JIT, JVM会将Class编译成native代码,这些内存也不会少,如果使用了Spring的AOP,CGLIB会生成更多的类,JIT的内存开销也会随之变大,而且Class本身JVM的GC会将其放到Perm Generation里去,很难被回收掉,面对这种情况,应该让JVM使用ConcurrentMarkSweep GC,并启用这个GC的相关参数允许将不使用的class从Perm Generation中移除, 参数配置: -XX: UseConcMarkSweepGC -X: CMSPermGenSweepingEnabled -X: CMSClassUnloadingEnabled,如果不需要移除而Perm Generation空间不够,可以加大一点: -X:PermSize=256M -X:MaxPermSize=512M

    JNI,一些JNI接口调用的native库也会分配一些内存,如果遇到JNI库的内存泄露,可以使用valgrind等内存泄露工具来检测

    线程栈,每个线程都会有自己的栈空间,如果线程一多,这个的开销就很明显了

    jmap/jstack 采样,频繁的采样也会增加内存占用,如果你有服务器健康监控,记得这个频率别太高,否则健康监控变成致病监控了。

    关于JVM的几个GC堆和GC的情况,可以用jstat来监控,例如监控进程837每隔1000毫秒刷新一次,输出20次:

     代码如下复制代码
    [lex@chou ~]$ jstat -gcutil 837 1000 20

    S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT

    0.00  80.43  24.62  87.44  98.29   7101  119.652    40   19.719  139.371

    0.00  80.43  33.14  87.44  98.29   7101  119.652    40   19.719  139.371

    几个字段分别含义如下:

    S0

    年轻代中第一个survivor(幸存区)已使用的占当前容量百分比

    S1

    年轻代中第二个survivor(幸存区)已使用的占当前容量百分比

    E

    年轻代中Eden(伊甸园)已使用的占当前容量百分比

    O

    old代已使用的占当前容量百分比

    P

    perm代已使用的占当前容量百分比

    YGC

    从应用程序启动到采样时年轻代中gc次数

    YGCT

    从应用程序启动到采样时年轻代中gc所用时间(s)

    FGC

    从应用程序启动到采样时old代(全gc)gc次数

    FGCT

    从应用程序启动到采样时old代(全gc)gc所用时间(s)

    GCT

    从应用程序启动到采样时gc用的总时间(s)

    结论

    因此如果正常情况下jmap输出的内存占用远小于 RSZ,可以不用太担心,除非发生一些严重错误,比如PermGen空间满了导致OutOfMemoryError发生,或者RSZ太高导致引起系统公愤被OOM Killer给干掉,就得注意了,该加内存加内存,没钱买内存加交换空间,或者按上面列的组成部分逐一排除。

    这几个内存指标之间的关系是:VSZ >> RSZ >> Java程序实际使用的堆大小

    展开全文
  • 众所周知,Java是从C++的基础上发展而来的,而C++程序的很大的一个问题就是内存泄露难以解决,尽管Java的JVM有一套自己的垃圾回收机制来回收内存,在许多情况下并不需要java程序开发人员操太多的心,但也是存在泄露...

    前面一篇文章介绍了Java虚拟机的体系结构和内存模型,既然提到内存,就不得不说到内存泄露。众所周知,Java是从C++的基础上发展而来的,而C++程序的很大的一个问题就是内存泄露难以解决,尽管Java的JVM有一套自己的垃圾回收机制来回收内存,在许多情况下并不需要java程序开发人员操太多的心,但也是存在泄露问题的,只是比C++小一点。比如说,程序中存在被引用但无用的对象:程序引用了该对象,但后续不会或者不能再使用它,那么它占用的内存空间就浪费了。

    我们先来看看GC是如何工作的:监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,当该对象不再被引用时,释放对象(GC本文的重点,不做过多阐述)。很多Java程序员过分依赖GC,但问题的关键是无论JVM的垃圾回收机制做得多好,内存总归是有限的资源,因此就算GC会为我们完成了大部分的垃圾回收,但适当地注意编码过程中的内存优化还是很必要的。这样可以有效的减少GC次数,同时提升内存利用率,最大限度地提高程序的效率。

    总体而言,Java虚拟机的内存优化应从两方面着手:Java虚拟机和Java应用程序。前者指根据应用程序的设计通过虚拟机参数控制虚拟机逻辑内存分区的大小以使虚拟机的内存与程序对内存的需求相得益彰;后者指优化程序算法,降低GC负担,提高GC回收成功率。

    通过参数优化虚拟机内存的参数如下所示:

    Xms

    初始Heap大小

    Xmx

    java heap最大值

    Xmn

    young generation的heap大小

    Xss

    每个线程的Stack大小

    上面是三个比较常用的参数,还有一些:

    XX:MinHeapFreeRatio=40

    Minimum percentage of heap free after GC to avoid expansion.

    XX:MaxHeapFreeRatio=70

    Maximum percentage of heap free after GC to avoid shrinking.

    XX:NewRatio=2

    Ratio of new/old generation sizes. [Sparc -client:8; x86 -server:8; x86 -client:12.]-client:8 (1.3.1+), x86:12]

    XX:NewSize=2.125m

    Default size of new generation (in bytes) [5.0 and newer: 64 bit VMs are scaled 30% larger; x86:1m; x86, 5.0 and older: 640k]

    XX:MaxNewSize=

    Maximum size of new generation (in bytes). Since 1.4, MaxNewSize is computed as a function of NewRatio.

    XX:SurvivorRatio=25

    Ratio of eden/survivor space size [Solaris amd64: 6; Sparc in 1.3.1: 25; other Solaris platforms in 5.0 and earlier: 32]

    XX:PermSize=

    Initial size of permanent generation

    XX:MaxPermSize=64m

    Size of the Permanent Generation. [5.0 and newer: 64 bit VMs are scaled 30% larger; 1.4 amd64: 96m; 1.3.1 -client: 32m.]

    下面所说通过优化程序算法来提高内存利用率,并降低内存风险,完全是经验之谈,仅供参考,如有不妥,请指正,谢谢!

    1.尽早释放无用对象的引用(XX = null;)

    看一段代码:

    public List parse(HtmlPage page) {

    List list = null;

    try {

    List valueList = page.getByXPath(config.getContentXpath());

    if (valueList == null || valueList.isEmpty()) {

    return list;

    }

    //需要时才创建对象,节省内存,提高效率

    list = new ArrayList();

    PageData pageData = new PageData();

    StringBuilder value = new StringBuilder();

    for (int i = 0; i < valueList.size(); i++) {

    HtmlElement content = (HtmlElement) valueList.get(i);

    DomNodeList imgs = content.getElementsByTagName("img");

    if (imgs != null && !imgs.isEmpty()) {

    for (HtmlElement img : imgs) {

    try {

    HtmlImage image = (HtmlImage) img;

    String path = image.getSrcAttribute();

    String format = path.substring(path.lastIndexOf("."), path.length());

    String localPath = "D:/images/" + MD5Helper.md5(path).replace("\\", ",").replace("/", ",") + format;

    File localFile = new File(localPath);

    if (!localFile.exists()) {

    localFile.createNewFile();

    image.saveAs(localFile);

    }

    image.setAttribute("src", "file:///" + localPath);

    localFile = null;

    image = null;

    img = null;

    } catch (Exception e) {

    }

    }

    //这个对象以后不会在使用了,清除对其的引用,等同于提前告知GC,该对象可以回收了

    imgs = null;

    }

    String text = content.asXml();

    value.append(text).append("
    ");

    valueList=null;

    content = null;

    text = null;

    }

    pageData.setContent(value.toString());

    pageData.setCharset(page.getPageEncoding());

    list.add(pageData);

    //这里 pageData=null; 是没用的,因为list仍然持有该对象的引用,GC不会回收它

    value=null;

    //这里可不能 list=null; 因为list是方法的返回值,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现、排除

    } catch (Exception e) {

    }

    return list;

    }

    2.谨慎使用集合数据类型,如数组,树,图,链表等数据结构,这些数据结构对GC来说回收更复杂。

    3.避免显式申请数组空间,不得不显式申请时,尽量准确估计其合理值。

    4.尽量避免在类的默认构造器中创建、初始化大量的对象,防止在调用其自类的构造器时造成不必要的内存资源浪费

    5.尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间

    6.尽量做远程方法调用类应用开发时使用瞬间值变量,除非远程调用端需要获取该瞬间值变量的值。

    7.尽量在合适的场景下使用对象池技术以提高系统性能

    展开全文
  • 近日看了字节跳动技术团队介绍Android内存优化的文章,决定理解并搬运下,希望可以为做内存优化的朋友提供思路。 内存作为计算机程序运行最...本文从抖音 Java OOM 内存优化的治理实践出发,尝试给大家分享一下抖音团

    近日看了字节跳动技术团队介绍Android内存优化的文章,决定理解并搬运下,希望可以为做内存优化的朋友提供思路。

    内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of memory)崩溃。抖音作为一款用户使用广泛的产品,需要在各种机器资源上保持优秀的流畅性和稳定性,内存优化是必须要重视的环节。

    本文从抖音 Java OOM 内存优化的治理实践出发,尝试给大家分享一下抖音团队关于 Java 内存优化中的一些思考,包括工具建设、优化方法论

    抖音 Java OOM 背景

    在未对抖音内存进行专项治理之前我们梳理了一下整体内存指标的绝对值和相对崩溃,发现占比都很高。另外,内存相关指标在去年春节活动时又再次激增达到历史新高,所以整体来看内存问题相当严峻,必须要对其进行专项治理。抖音这边通过前期归因、工具建设以及投入一个双月的内存专项治理将整体 Java OOM 优化了百分之 80。(可见大厂的平常开发中,内存控制也并非完美,也是需要专门进行优化工作的)

    Java OOM Top 堆栈归因

    在对抖音的 Java 内存优化治理之前我们先根据平台上报的堆栈异常对当前的 OOM 进行归因,主要分为下面几类:

    图 1. OOM 分类

    其中 pthread_create 问题占到了总比例大约在百分之 50,Java 堆内存超限为百分之 40 多,剩下是少量的 fd 数量超限。其中 pthread_create 和 fd 数量不足均为 native 内存限制导致的 Java 层崩溃,我们对这部分的内存问题也做了针对性优化,主要包括:

    • 线程收敛、监控
    • 线程栈泄漏自动修复
    • FD 泄漏监控
    • 虚拟内存监控、优化
    • 抖音 64 位专项

    治理之后 pthread_create 问题降低到了 0.02%以下,这方面的治理实践会在下一篇抖音 Native 内存治理实践中详细介绍,大家敬请期待。本文重点介绍 Java 堆内存治理。

    堆内存治理思路

    从 Java 堆内存超限的分类来看,主要有两类问题:

    1. 堆内存单次分配过大,多次分配累计过大

    触发这类问题的原因有数据异常导致单次内存分配过大超限,也有一些是 StringBuilder 拼接累计大小过大导致等等。这类问题的解决思路比较简单,问题就在当前的堆栈。

    2. 堆内存累积分配触顶

    这类问题的问题堆栈会比较分散,在任何内存分配的场景上都有可能会被触发,那些高频的内存分配节点发生的概率会更高,比如 Bitmap 分配内存。这类 OOM 的根本原因是内存累积占用过多,而当前的堆栈只是压死骆驼的最后一根稻草,并不是问题的根本所在。所以这类问题我们需要分析整体的内存分配情况,从中找到不合理的内存使用(比如内存泄露、大对象、过多小对象、大图等)。

    工具建设

    工具思路

    工欲善其事,必先利其器。从上面的内存治理思路看,工具需要主要解决的问题是分析整体的内存分配情况,发现不合理的内存使用(比如内存泄露、大对象、过多小对象等)。

    我们从线下和线上两个维度来建设工具:

    线下

    线下工具是最先考虑的,在研发和测试的时候能够提前发现内存泄漏问题。业界的主流工具也是这个思路,比如 Android Studio Memory Profiler、LeakCanary、Memory Analyzer (MAT)。

    我们基于 LeakCanary 核心库在线下设计了一套自动分析上报内存泄露的工具,主要流程如下:

    图 2.线下自动分析流程

    抖音在运行了一段线下的内存泄漏工具之后,发现了线下工具的各种弊端:

    1. 检测出来的内存泄漏过多,并且也没有比较好的优先级排序,研发消费不过来,历史问题就一直堆积。另外也很难和业务研发沟通问题解决的收益,大家针对解决线下的内存泄漏问题的 ROI(投入产出比)比较难对齐。
    2. 线下场景能跑到的场景有限,很难把所有用户场景穷尽。抖音用户基数很大,我们经常遇到一些线上的 OOM 激增问题,因为缺少线上数据而无从查起。
    3. Android 端的 HPORF 的获取依赖原生的 Debug.dumpHporf,dump 过程会挂起主线程导致明显卡顿,线下使用体验较差,经常会有研发反馈影响测试。
    4. LeakCanary 基于 Shark 分析引擎分析,分析速度较慢,通常在 5 分钟以上才能分析完成,分析过程会影响进程内存占用。
    5. 分析结果较为单一,仅仅只能分析出 Fragment、Activity 内存泄露,像大对象、过多小对象问题导致的内存 OOM 无法分析。

    线上

    正是由于上述一些弊端,抖音最早的线下工具和治理流程并没有起到什么太大作用,我们不得不重新审视一下,工具建设的重心从线下转成了线上。

    线上工具的核心思路是:

    在发生 OOM 或者内存触顶等触发条件下,dump 内存的 HPROF 文件,对 HPROF 文件进行分析,分析出内存泄漏、大对象、小对象、图片问题并按照泄露链路自动归因,将大数据问题按照用户发生次数、泄露大小、总大小等纬度排序,推进业务研发按照优先级顺序来建立消费流程。为此我们研发了一套基于 HPORF 分析的线下、线上闭环的自动化分析工具 Liko(寓意 ko 内存 Leak 问题)。

    Liko 介绍

    Liko 整体架构

    图 3. Liko 架构图

    整体架构由客户端、Server 端和核心分析引擎三部分构成。

    客户端

    在客户端完成 HPROF 数据采集和分析(针对端上分析模式),这里线上和线下策略不同。

    线上:主要在 OOM 和内存触顶时通过用户无感知 dump 来获取 HPROF 文件,当 App 退出到后台且内存充足的情况进行分析,为了尽量减少对 App 运行时影响,主要通过裁剪 HPROF 回传进行分析,减轻服务器压力,对部分比例用户采用端上分析作为 Backup。

    线下:dump 策略配置较为激进,在 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值多种场景下触发 dump,并实时在端上分析上传至后台并在本地自动生成 html 报表,帮助研发提前发现可能存在的内存问题。

    Server 端

    Server 端根据线上回传的大数据完成链路聚合、还原、分配,并根据用户发生次数、泄露大小、总大小等纬度促进研发测消费,对于回传分析模式则会另外进行 HPORF 分析。

    分析引擎

    基于 MAT 分析引擎完成内存泄露、大对象、小对象、图片等自动归因,同时支持在线下自动生成 Html 报表。

    Liko 流程图

    图 4. Liko 流程图

    整体流程分为

    1. Hprof 收集
    2. 分析时机
    3. 分析策略

    Hprof 收集

    收集过程我们设置了多种策略可以自由组合,主要有 OOM、内存触顶、内存激增、监测 Activity、Fragment 泄漏数量达到一定阈值时触发,线下线上策略配置不同。为了解决 dump 挂起进程问题,我们采用了子进程 dump+fileObsever 的方式完成 dump 采集和监听。

    在 fork 子进程之前先 Suspend 获取主进程中的线程拷贝,通过 fork 系统调用创建子进程让子进程拥有父进程的拷贝,然后 fork 出的子进程中调用 Hprof 的 DumpHeap 函数即可完成把耗时的 dump 操作在放在子进程。由于 suspendresume 是系统函数,我们这里通过自研的 native hook 工具对 libart.so hook 获取系统调用。由于写入是在子进程完成的,我们通过 Android 提供的 fileObsever 文件写入进行监控获取 dump 完成时机。

    图 5.子进程 dump 流程图

    Hprof 分析时机

    为了达到分析过程对于用户无感,我们在线上、线下配置了不同的分析时机策略,线下在 dump 分析完成后根据内存状态主动触发分析,线上当用户下次冷启退出应用后台且内存充足的情况下触发分析。

    分析策略

    分析策略我们提供了两种,一种在 Android 客户端分析,一种回传至 Server 端分析,均通过 MAT 分析引擎进行分析。

    1.端上分析

    1.1分析引擎:端上分析引擎的性能很重要,这里我们主要对比了 LeakCanary 的分析引擎 Shark 和 Haha 库的 MAT。

    图 6. Shark VS MAT

    我们在相同客户端环境对 160M 的 HPROF 多次分析对比发现 MAT 分析速度明显优于 Shark,另外针对 MAT 分析后仍持有统治者树占用内存我们也做了主动释放,对比性能收益后采用基于 MAT 库的分析引擎进行分析,对内存泄漏引用链路自动归并、大对象小对象引用链自动分析、大图线下自动还原线上过滤无用链路,分析结果如下:

    图 7. 内存泄漏链路

    对泄漏的 Activity 的引用链进行了聚合分析,方便一次性解决该 Activity 的泄漏链释放内存。

    1.2大对象

    图 8. 大对象链路

    大对象不止分析了引用链路,还递归分析了内部 top 持有对象(InRefrenrece)的 RetainedSize。

    1.3小对象

    图 9. 小对象链路

    小对象我们对 top 的外部持有对象(OutRefrenrece)进行聚合得到占有小对象最多的链路。

    1.4图片

    图 10. 图片链路

    图片我们过滤了图片库等无效引用且对 Android 8.0 以下的大图在线下进行了还原。

    2.回传分析

    为了最大限度的节省用户流量且规避隐私风险,我们通过自研 HPROF 裁剪工具 Tailor 在 dump 过程对 HPROF 进行了裁剪。

    2.1裁剪过程

    图 11. Tailor 裁剪流程

    去除了无用信息

    • 跳过 header
    • 分 tag 裁剪
      • 裁剪无用信息:char[]; byte[]; timestamp; stack trace serial number; class serial number;
      • 压缩数据信息

    同时对数据进行 zlib 压缩,在 server 端数据还原,整体裁剪效果:180M--->50M---->13M

    优化实践

    内存泄漏

    除了通过后台根据 GCROOT+引用链自动分配研发跟进解决我们常见的内存泄漏外,我们还对系统导致一些内存泄漏进行了分析和修复。

    系统异步 UI 泄漏

    据上传聚合的引用链我们发现在 Android 6.0 以下有一个 HandlerThread 作为 GCROOT 持有大量 Activity 导致内存泄漏,根据引用发现这些泄漏的 Activity 都被一个 Runnable(这里是 Runnable 是一个系统事件 SendViewStateChangedAccessibilityEvent)持有,这些 Runnable 被添加到一个 RunQueuel 中,这个队列本身被 TheadLocal 持有。

    图 12. HandlerThread 泄露链路

    我们从 SendViewStateChangedAccessibilityEvent 入手对源码进行了分析发现它在 notifyViewAccessibilityStateChangedIfNeeded 中被抛出,系统的大量 view 都会在自身的一些 UI 方法(eg: setChecked)中触发改函数。

    SendViewStateChangedAccessibilityEventrunOrPost 方法会走到我们常用的 View 的 postDelay 方法中,这个方法在当 view 还未被 attched 到根 view 的时候会加入到一个 runQueue 中。

    这个 runQueue 会在主线程下一次的 performTraversals() 中消费掉。

     

    如果这个 runQueue 不在主线程那就没有消费的机会。

    根据上面的分析发现造成这种内存泄漏需要满足一些条件:

    1. view 调用了 postDelay 方法 (这里是 notifyViewAccessisbilityStateChangeIfNeeded 触发)
    2. view 处于 detached 状态
    3. 上述过程是在非主线程里面操作的,ThreadLocal 非 UIThread,持有的 runQueue 不会走 performTraversals 消费掉。

    抖音这边大量使用了异步 UI 框架来优化渲染性能,框架内部由一个 HandlerThread 驱动,完全符合上述条件。针对该问题,我们通过反射获取非主线程的 ThreadLocal,在每次异步渲染完主动清理内部的 RunQueue。

    图 13. 反射清理流程

    另外,Google 在 6.0 上也修复了 notifyViewAccessisbilityStateChangeIfNeeded 的判断不严谨问题。

    内存泄漏兜底

    大量的内存泄漏,如果我们都靠推进研发解决,经常会出现生产大于消费的情况,针对这些未被消费的内存泄漏我们在客户端做了监控和止损,将 onDestory 的 Activity 添加到 WeakRerefrence 中,延迟 60s 监控是否回收,未回收则主动释放泄漏的 Activity 持有的 ViewTree 的背景图和 ImageView 图片。

    大对象

    主要对三种类型的大对象进行优化

    • 全局缓存:针对全局缓存我们按需释放和降级了不需要的缓存,尽量使用弱引用代替强引用关系,比如针对频繁泄漏的 EventBus 我们将内部的订阅者关系改为弱引用解决了大量的 EventBus 泄漏。
    • 系统大对象:系统大对象如 PreloadDrawableJarFile 我们通过源码分析确定主动释放并不干扰原有逻辑,在启动完成或在内存触顶时主动反射释放。
    • 动画:用原生动画代替了内存占用较大的帧动画,并对 Lottie 动画泄漏做了手动释放。

    图 14. 大对象优化点

    小对象

    小对象优化我们集中在字段优化、业务优化、缓存优化三个纬度,不同的纬度有不同的优化策略。

    图 15. 小对象优化思路

    通用类优化

    在抖音的业务中,视频是最核心且通用的 Model,抖音业务层的数据存储分散在各个业务维护了各自视频的 Model,Model 本身由于聚合了各个业务需要的属性很多导致单个实例内存占用就不低,随着用户使用过程实例增长内存占用越来越大。对 Model 本身我们可以从属性优化拆分这两种思路来优化。

    • 字段优化:针对一次性的属性字段,在使用完之后即使清理掉缓存,比如在视频 Model 内部存在一个 Json 对象,在反序列完成之后 Json 对象就没有使用价值了,可以及时清理。
    • 类拆分:针对通用 Model 冗杂过多的业务属性,尝试对 Model 本身进行治理,将各个业务线需要用到的属性进行梳理,将 Model 拆分成多个业务 Model 和一个通用 Model,采用组合的方式让各个业务线最小化依赖自己的业务 Model,减少大杂烩 Model 不必要的内存浪费。

    业务优化

    • 按需加载:抖音这边 IM 会全局保存会话,App 启动时会一次性 Load 所有会话,当用户的会话过多时相应全局占用的内存就会较大,为了解决该问题,会话列表分两次加载,首次只加载一定数量到内存,需要时再加载全部。
    • 内存缓存限制或清理:首页推荐列表的每一次 Loadmore 操作,都不会清理之前缓存起来的视频对象,导致用户长时间停留在推荐 Feed 时,缓存起来的视频对象过多会导致内存方面的压力。在通过实验验证不会对业务产生负面影响情况下对首页的缓存进行了一定数量的限制来减小内存压力。

    缓存优化

    上面提到的视频 Model,抖音最早使用 Manager 来管理通用的视频实例。Manager 使用 HashMap 存储了所有的视频对象,最初的方案里面没有对内存大小进行限制且没有清除逻辑,随着使用时间的增加而不断膨胀,最终出现 OOM 异常。为了解决视频 Model 无限膨胀的问题设计了一套缓存框架主要流程如下:

    图 16. 视频缓存框架

    使用 LRU 缓存机制来缓存视频对象。在内存中缓存最近使用的 100 个视频对象,当视频对象从内存缓存中移除时,将其缓存至磁盘中。在获取视频对象时,首先从内存中获取,若内存中没有缓存该对象,则从磁盘缓存中获取。在退出 App 时,清除 Manager 的磁盘缓存,避免磁盘空间占用不断增长。

     图片

    关于图片优化,我们主要从图片库的管理图片本身优化两个方面思考。同时对不合理的图片使用也做了兜底监控

    图片库

    针对应用内图片的使用状况对图片库设置了合理的缓存,同时在应用 or 系统内存吃紧的情况下主动释放图片缓存。

    图片自身优化

    我们知道图片内存大小公式 = 图片分辨率 * 每个像素点的大小。图片分辨率我们通过设置合理的采样来减少不必要的像素浪费。

    //开启采样
    ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
        .setDownsampleEnabled(true)
        .build();
    Fresco.initialize(context, config);
    
    //请求图片时,传入resize的大小,一般直接取View的宽高
    ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
        .setResizeOptions(new ResizeOptions(50, 50))
        .build();mSimpleDraweeView.setController(
        Fresco.newDraweeControllerBuilder()
            .setOldController(mSimpleDraweeView.getController())
            .setImageRequest(request)
            .build());
    

    而单个像素大小,我们通过替换系统 drawable 默认色彩通道,将部分没有透明通道的图片格式由 ARGB_8888 替换为 RGB565,在图片质量上的损失几乎肉眼不可见,而在内存上可以直接节省一半。

    图片兜底

    针对因 activity、fragment 泄漏导致的图片泄漏,我们在 onDetachedFromWindow 时机进行了监控和兜底,具体流程如下:

    图 17. 图片兜底流程

    图片监控

    关于对不合理的大图 or 图片使用我们在字节码层面进行了拦截和监控,在原生 Bitmap or 图片库创建时机记录图片信息,对不合理的大图进行上报;另外在 ImageView 的设置过程中针对 Bitmap 远超过 view 本身超过大小的场景也进行了记录和上报。

    图 18. 图片字节码监控方案

    更多思考

    是不是解决了 OOM 内存问题就告一段落了呢?作为一只追求极致的团队,我们除了解决静态的内存占用外也自研了 Kenzo(Memory Insight)工具尝试解决动态内存分配造成的 GC 卡顿。

    至于这个工具,感兴趣的朋友可以移步到原文查看

     

     

     

     

     

    展开全文
  • 1.没有必要时请不用使用静态变量使用Java的开发者都知道,当某个对象被定义为stataic变量所引用,这个对象所占有的内存将不会被回收。有时,开发者会将经常调用的对象或者变量定义为static,以便提高程序的运行性能...

    1.没有必要时请不用使用静态变量

    使用Java的开发者都知道,当某个对象被定义为stataic变量所引用,这个对象所占有的内存将不会被回收。有时,开发者会将经常调用的对象或者变量定义为static,以便提高程序的运行性能。因此,不是常用到的对象或者变量,不要定义为static类型的变量,尤其是静态类对象的定义,一定要仔细考虑是否有必要。例如

    public class X{

    static Y a = new Y();

    }

    类X创建了,没有被回收的话,静态变量a一直占用内存。

    2.充分利用单例机制

    实用单例可以减少对资源的加载,缩短运行的时间,提高系统效率。但是,单例并不是所有地方都适用于。简单来说,单例可以适用于以下两个方面:

    1.   控制资源的使用,通过线程同步来控制资源的并发访问;

    2.   控制实例的产生,以达到节约资源的目的;

    3. 减少对象的创建

    尽量避免在经常调用的方法中循环使用new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理。设计模式中的享元模式就是为了减少对象的多次创建而来的。在我们可以控制的范围内,最大限度的重用对象;在有些时候,最好能用基本的数据类型或数组来替代对象。

    4.使用final修饰符

    带有final修饰符的类是不可派生的。在Java核心API中,有许多应用 final的例子,例如java.lang.String。为String类指定final防止了使用者覆盖length()方法。另外,如果一个类是 final的,则该类所有方法都是final的。Java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。

    5.尽量使用局部变量

    调用方法时传递的参数以及在调用中创建的临时变量都保存在分配给改方法的栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

    6.处理好包装类型和基本类型两者的使用场所

    虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,而包装类型是对象,是在堆中产生实例。在集合类对象,有对象方面需要的处理适用包装类型,其他的情况,建议提倡使用基本类型。

    7.学会用StringBuilder和StringBuffer

    这个两个类的区别就不用说了吧,单线程使用StringBuilder,多线程情况下使用StringBuffer,这样性能会有很大提升。

    7.尽量不要使用finalize方法

    实际上,将资源清理放在finalize方法中完成是非常不好的选择。由于GC的工作量很大,尤其是回收Young代内存时,大都会引起应用程序暂停,所以再选择使用finalize方法进行资源清理,会导致GC负担更大,程序运行效率更差。

    8.尽量使用基本数据类型代替对象

    String str = "hello";

    上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串。

    String str = new String("hello");

    此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o

    这个问题我也写个一个博客,请查看我写的有关性能优化的博客。

    9.学会使用HashMap、ArrayList

    HashTable、Vector等使用在多线程的场合,内部使用了同步机制,这个会降低程序的性能。

    10. 深入理解HashMap原理

    当你要创建一个比较大的hashMap时,充分利用另一个构造函数

    public HashMap(int initialCapacity, float loadFactor)避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认initialCapacity只有16,而 loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。

    11.减少对变量的重复计算

    for(int i=0;i

    应该改写为:

    for(int i=0,len=list.size();i

    或者

    for(int i = list.size(); I > -1; i--)

    并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。

    12.避免不必要的对象创建

    A a = new A();

    if(i==1){list.add(a);}

    应该改为

    if(i==1){

    A a = new A();

    list.add(a);}

    13.尽量在finally块中释放资源

    程序中使用到的资源应当被释放,以避免资源泄漏。这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。

    14.使用移位来代替乘法或者除法('a/b',仅适合2^n情况)的操作

    "/"是一个代价很高的操作,使用移位的操作将会更快和更有效

    int num = a / 4;

    int num = a / 8;

    应该改为

    int num = a >> 2;

    int num = a >> 3;

    但注意的是使用移位应添加注释,因为移位操作不直观,比较难理解

    同样的,对于'*'操作,使用移位的操作将会更快和更有效

    int num = a * 4;

    int num = a * 8;

    应该改为

    int num = a << 2;

    int num = a << 3;

    15. 确定StringBuffer的容量

    StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。

    如:StringBufferbuffer = new StringBuffer(1000);

    16.尽量早释放无用对象的引用

    大部分时,方法局部引用变量所引用的对象 会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。

    例如:

    Public void doJob(){

    Object obj =new Object();

    ……

    Obj=null;

    }

    上面这个就没必要了,随着方法doJob()的执行完成,程序中obj引用变量的作用域会被gc回收。但是如果是改成下面:

    public void doJob(){

    Object obj =new Object();

    ……

    Obj=null;

    //执行耗时,耗内存操作;或调用耗时,耗内存的方法

    ……

    }

    这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。

    17.尽量避免使用split

    除非是必须的,否则应该避免使用split,split由于支持正则表达式,所以效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果确实需要频繁的调用split,可以考虑使用apache的 StringUtils.split(string,char),频繁split的可以缓存结果。

    18.ArrayList & LinkedList

    一个是线性表,一个是链表,一句话,随机查询尽量使用 ArrayList,ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于 ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好数据结构。

    19.尽量使用System.arraycopy ()代替通过来循环复制数组

    System.arraycopy()要比通过循环来复制数组快的多

    20.尽量缓存经常使用的对象

    尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。

    21.尽量避免非常大的内存分配

    有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配的内存块都必须是连续的,而随着堆heap越来越满,找到较大的连续块越来越困难。

    22.慎用异常

    当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说:先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。

    如果您创建一个 Exception ,就得付出代价。好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,您甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常。幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。

    展开全文
  • 本文收集网上关于减少java程序占用的一些小知识点(1)别用new Boolean()。在很多场景中Boolean类型是必须的,比如JDBC中boolean类型的set与get都是通过Boolean封装传递的,大部分ORM也是用Boolean来封装boolean类型的...
  • 对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不需要为每一个new操作去写配对的delete/free,不容易出现内存泄漏和内存溢出错误,看起来由虚拟机管理内存这一切都很美好。但是也正是因为Java程序员把内存...
  • 程序员:为什么程序总是那么慢?时间都花到哪里去了?面试官:若你写的 Java 程序,出现了性能问题,该怎么去排查呢?...可以监控 Java 程序在运行时占用的 CPU,及统计堆内存使用等。例如:每隔 10 毫秒采样 CP...
  • 使用jstat查看Java内存分布及回收情况通常运行命令如下:jstat -gc 15712 5000即会每5秒一次显示进程号为15712的java进成的GC情况,- S0C: Young Generation第一个survivor space的内存大小 (kB).- S1C: Young ...
  • 如果您查看一个典型的大型Java应用程序内存快照,您将看到数以千计甚至数百万个Java .util.ArrayList,java.util.HashMap的实例。集合对于内存中的数据存储和操作是必不可少的。但你有没有想过你的应用程序中的所有...
  • Java程序性能优化是一个永远逃不开的话题,优化无处不在!一个偶然间,小编得到了一份Java程序性能优化的笔记,小编读完这份笔记后大为惊叹,实在是写的太好了,不忍独乐乐,于是把这份笔记免费分享出来,供大家参阅...
  • Java性能优化权威指南主要为Java SE 和Java EE 应用的性能调优提供建议。主要包括以下几方面:性能监控、性能分析、Java HotSpot VM 调优、高效的基准测试以及Java EE 应用的性能调优。《Java性能优化权威指南》适合...
  • 我们分析了著名的spring boot pet诊所应用程序,以查看浪费了多少内存。该应用程序是由spring社区设计的,旨在显示spring应用程序框架如何用于构建简单但功能强大的面向数据库的应用程序。环境准备Spring Boot ...
  • 作为Android工程师,我看过很多关于Android内存泄漏的相关优化的文章,其中大部分都是告诉你该怎么做,做哪些,列...而想要能基本理解GC,就必须了解Java程序运行时的内存区域。没办法,学习学全套嘛。况且,我是...
  • 很多人错误的认为运行Java程序时使用-Xmx和-Xms参数指定的就是程序将会占用的内存,但是这实际上只是Java堆对象将会占用的内存。堆只是影响Java程序占用内存数量的一个因素。要更好的理解你的Java程序将会占用多大的...
  • 滴滴资深架构师深度分享Java程序性能优化的宝贵经验,从软件设计、编码和JVM等维度阐述性能优化的方法和技巧。 总览: 篇幅限制,这里就不全部展示出来了。需要获取完整版Java程序性能优化手册...
  • Java程序性能优化

    2021-03-01 10:48:12
    它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的程序的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java ...
  • linux 设置java内存

    2021-05-15 03:50:13
    linux 设置java内存[2021-02-03 18:47:06]简介:php去除nbsp的方法:首先创建一个PHP代码示例文件;然后通过“preg_replace("/(\s|\&nbsp\;| |\xc2\xa0)/", " ", strip_tags($val));”方法去除所有nbsp即可。...
  • javaFx 内存优化

    2021-04-16 11:18:40
    在做我的开源项目:IceoLogy壁纸时,第二版加入了屏保之后,内存竟然要用到800多M,稳定...于是就加入了漫长的优化之路,几乎把代码都重构了,但内存还是居高不下,但是我看到SecretOpen的开源secret-performance-d.
  • 读取文件就是对代码与算法上进行了一些细节上的修改了,下文我们来看一段java读取文件性能优化的例子。在执行IO时,Java的InputStream被广泛使用,比如DataInputStream.readInt等等。事实上,这些高度封装的接口奇慢...
  • java内存与回收调优

    2021-02-26 08:25:57
    要了解Java垃圾收集机制,先理解JVM内存模式是非常重要的。今天我们将会了解JVM内存的各个部分、如何监控以及垃圾收集调优。Java(JVM)内存模型正如你从上面的图片看到的,JVM内存被分成多个独立的部分。广泛地说,...
  • 该报告显示了由于重复字符串而浪费了多少内存,它们是什么字符串,谁在创建它们以及如何对其进行优化。 您会注意到由于重复的字符串浪费了15.6%的内存。请注意 'Goldi'字符串已被创建207,481次。 “Visit”字符串已...
  • java常用内存设置

    2021-03-01 08:40:14
    非堆就是JVM留给自己用的(方法区,JVM内部处理或优化所需的内存,运行时的常量池,字段方法数据等)。JVM初始分配的内存由-Xms指定,默认为物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认...
  • 查看使用dstat和top查看内存使用最高的应用使用dstat查到内存占用最高的是java应用,使用2253M内存,但是这台服务器跑了好几个java,具体哪个进程使用top看下资源情况使用top可以看到java应用整体内存使用率超过了70...
  • 想了解java应用占用内存过高排查的解决方案的相关内容吗,zhaixing_0307在本文为您仔细讲解java内存高排查的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:java,占内存过高,排查,下面大家一起来学习吧...
  • 1、在解决问题之前,先看下jvm堆内存结构,如下图所示: 对于Java应用,虚拟机管理的内存,可以参考如下图所示: 一般对于一个应用来说,如果内存使用过大,可以从两块来分析,第一:堆内存,第二:堆外内存。...
  • Java程序性能优化-空间换时间[1]以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!空间换时间与时间换空间的方法相反 空间换时间则是尝试使用更多的内存...
  • 模拟Java内存溢出

    2021-03-16 21:38:10
    本文通过修改虚拟机启动参数,来剖析常见的java内存溢出异常(基于jdk1.8)。修改虚拟机启动参数这里我们使用的是IDEA集成开发环境,选择Run/Debug Configurations然后选择Configuration,修改VM options配置,就可以...
  • 今天要和大家分享的是关于抖音 Android 性能优化的内存优化,希望对大家的学习和工作有所启发和帮助。 正文 内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 322,596
精华内容 129,038
关键字:

java程序内存优化

java 订阅