大数据系列之Spark性能优化案例详细剖析

大数据系列之Spark性能优化案例分析

什么是数据倾斜?

数据倾斜是一种很常见的问题(依据二八定律),简单来说,比方WordCount中某个Key对应的数据量非常大的话,就会产生数据倾斜,导致两个后果:
1.OOM内存溢出(单或少数的节点);
2.拖慢整个Job执行时间(其他已经完成的节点都在等这个还在做的节点)。

解决数据倾斜的出发点?

一般需要通过SparkWebUI来综合评判:
1.是否出现内存溢出;
2.运行时间差异很大,总体时间过长;
3.查看是否存在大量相同的key;
4.是否存在触发shuffle的算子如:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等;

导致Spark数据倾斜的本质是什么?

​ Shuffle时,需将各节点的相同key的数据拉取到某节点上的一个task来处理,若某个key对应的数据量很大就会发生数据倾斜。比方说大部分key对应10条数据,某key对应10万条,大部分task只会被分配10条数据,很快做完,个别task分配10万条数据,不仅运行时间长,且整个stage的作业时间由最慢的task决定。

数据倾斜只会发生在Shuffle过程,可能触发Shuffle的算子有哪些?

distinct
distinct的操作其实是把原RDD进行map操作,根据原来的key-value生成为key,value使用null来替换,并对新生成的RDD执行reduceByKey的操作,也就是说,Distinct的操作是根据key与value一起计算不重复的结果.只有两个记录中key与value都不重复才算是不重复的数据。
groupByKey
roupByKey会将RDD[key,value] 按照相同的key进行分组,形成RDD[key,Iterable[value]]的形式, 有点类似于sql中的groupby。
reduceByKey:
reduceByKey就是将key相同的键值对,按照Function进行计算。如代码中就是将key相同的各value进行累加。得到的结果就是类似于[(key2,2), (key3,1), (key1,2)] 形式。
aggregateByKey函数
对PairRDD中相同的Key值进行聚合操作,在聚合过程中同样使用了一个中立的初始值。和aggregate函数类似,aggregateByKey返回值的类型不需要和RDD中value的类型一致。因为aggregateByKey是对相同Key中的值进行聚合操作,所以aggregateByKey’函数最终返回的类型还是PairRDD,对应的结果是Key和聚合后的值,而aggregate函数直接返回的是非RDD的结果。
join
join类似于SQL的inner join操作,返回结果是前面和后面集合中配对成功的,过滤掉关联不上的。
cogroup
对两个RDD中的kv元素,每个RDD中相同key中的元素分别聚合成一个集合。与reduceByKey不同的是针对两个RDD中相同的key的元素进行合并。
repartition
返回一个恰好有numPartitions个分区的RDD,可以增加或者减少此RDD的并行度。内部,这将使用shuffle重新分布数据,如果你减少分区数,考虑使用coalesce,这样可以避免执行shuffle。

数据倾斜典型案例?

1.数据源中数据分布不均匀;
2.数据集中的不同key由于分区方式,导致分区不均匀;
3.数据集中少数几个key数据量很大,其它分布叫均匀;
4.聚合操作中,数据集中的数据分布不均匀;
5.Join操作中,一个数据集中的数据分布不均匀,另一个数据集较小;
6.Join操作中,两个数据集都比较大,其中只有几个Key的数据分布不均;
7.Join操作中,两个数据集都比较大,很多Key的数据分布不均;

如何定位最慢Task源码的位置?

步骤一: 看数据倾斜发生在哪个stage(也就是看以上算子出现在哪个阶段)。yarn-client模式下查看本地log或Spark Web UI中当前运行的是哪个stage;yarn-cluster模式下,通过Spark Web UI查看运行到了哪个Stage。
主要看最慢的Stage各task分配的数据量,来确定是否是数据倾斜。

步骤二:根据Stage划分,推算倾斜发生的代码(必然有Shuffle类算子)。简单实用方法:只要看到shuffle类算子或Spark SQL的SQL语句会有Shuffle类的算子的句子,就可以知道该地方划分为前后两个Stage。(用Python的PySpark接口,Spark Web UI会查看task在源码中的行数,Java或者Scala 同理。)

准备测试数据1000W条数据
/**
 * 生成文件
 */
private static void genFileData(){
    try {
        FileOutputStream fos = new FileOutputStream("./keywords.txt",true);
        for (int i = 1; i <= 10000000 ; i++) {
            //String msg = String.format("%010d", InfoUtils.getNum(1,10000)) + "\t" + InfoUtils.getSearchWords() +"\n";
            String uid = String.format("%010d", InfoUtils.getNum(1,10000));
            String keyWord = InfoUtils.getSearchWords();
            long time = System.currentTimeMillis();
            String url = InfoUtils.getURL();
            String msg = i+"\t"+uid+"\t"+keyWord+"\t"+time+"\t"+url+"\n";
            fos.write(msg.getBytes());
        }
        fos.close();
    }catch (Exception e){
        e.printStackTrace();
    }
}
将测试数据上传至hdfs文件系统
hdfs dfs -mkdir -p /spark-data/source-data
hdfs dfs -put keywords.txt /spark-data/source-data
数据初步加工,分成12个分区第八个最多
//样例类
case class KeyWordLog(id: Long, uid: String, keyWord: String, time: Long, url: String)
val sourceRdd = sc.textFile("hdfs://node2.com:8020//spark-data/source-data/spark.txt")
val df = sourceRdd.map(_.split("\t")).map(attr => KeyWordLog(attr(0).toLong, attr(1), attr(2), attr(3).toLong, attr(3))).toDF()
df.createOrReplaceTempView("sourceTable")
//设置12个分区,大部分会落在第八个任务
val newSourceRdd = spark.sql("SELECT CASE WHEN id < 9000000 THEN (8+(CAST( RAND() * 50000 AS BIGINT )) * 12) ELSE id END id,uid,keyWord,time url FROM sourceTable")
newSourceRdd.rdd.map(_.mkString("\t")).saveAsTextFile("hdfs://node2.com:8020//spark-data/new-source-data")
1.调整并行度解决数据倾斜案例
val sourceRdd = sc.textFile("hdfs://node2.com:8020//spark-data/new-source-data/p*")
val kvRDD = sourceRdd.map(_.split("\t")).map(attr =>(attr(0).toLong,attr(1)) )
//数据倾斜 第八个任务运行时间过长
kvRDD.groupByKey(12).count()
//增大分区数量减少数据倾斜
kvRDD.groupByKey(17).count()
//减少分区数量减少数据倾斜
kvRDD.groupByKey(5).count()
分区数量为12运行结果

在这里插入图片描述

增大分区数量为17运行结果

在这里插入图片描述

减小分区数量为5运行结果

在这里插入图片描述

2.自定义分区解决数据倾斜案例
//自定义分区
class CustomerPartitioner(numSize: Int) extends org.apache.spark.Partitioner {
  override def numPartitions: Int = numSize
  override def getPartition(key: Any): Int = {
    val id = key.toString.toInt
    return id % numSize
  }
}
使用自定义分区
val sourceRdd = sc.textFile("hdfs://node2.com:8020//spark-data/new-source-data/p*")
val kvRDD = sourceRdd.map(_.split("\t")).map(attr =>(attr(0).toLong,attr(1)) )
kvRDD.groupByKey(new CustomerPartitioner(17)).count
3.Reduce side Join转变为Map side Join案例

方案适用场景:在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M),
比较适用此方案。
方案实现原理:普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,
此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,
此时就不会发生shuffle操作,也就不会发生数据倾斜。
方案优点:对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就根本不会发生数据倾斜。
方案缺点:适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。

数据准备
//加载原始数据
val sourceRdd = sc.textFile("hdfs://node2.com:8020//spark-data/new-source-data/p*")
val kvRdd = sourceRdd.map(_.split("\t")).map(attr => (attr(0).toLong, attr(1)))
//大表数据 大量的key相同
val bigRdd = kvRdd.map(x => {
    if (x._1 < 9900001) (9900001, x._2) else x
}).map(x => x._1 + "," + x._2)
bigRdd.saveAsTextFile("hdfs://node2.com:8020//spark-data/join/big-table")
//小表数据
val smallRdd = kvRdd.filter(_._1 > 9900000).map(x => x._1 + "," + x._2)
smallRdd.saveAsTextFile("hdfs://node2.com:8020//spark-data/join/small-table")
测试 join
val bigSource = sc.textFile("hdfs://node2.com:8020//spark-data/join/big-table/p*")
val bigKvRdd = bigSource.map(_.split(",")).map(attr => (attr(0).toLong, attr(1)))
val smallSource = sc.textFile("hdfs://node2.com:8020//spark-data/join/small-table/p*")
val smallKvRdd = smallSource.map(_.split(",")).map(attr => (attr(0).toLong, attr(1)))
bigKvRdd.join(smallKvRdd).count()
此时出现了数据倾斜

在这里插入图片描述

优化方案小表添加到广播变量
val broadcastVar = sc.broadcast(smallKvRdd.collectAsMap())
bigKvRdd.map(x => (x._1, (x._2, broadcastVar.value.getOrElse(x._1, "")))).count()
优化结果

在这里插入图片描述

4.阶段聚合(局部聚合+全局聚合)案例

使用场景:对RDD执行reduceByKey等聚合类shuffle算子 或者 SparkSQL使用group by 语句进行分组聚合。
方案实现原理:将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。
接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。
方案优点:对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。
方案缺点:仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。

原理图如下

在这里插入图片描述

测试案例
def main(args: Array[String]): Unit = {
  val conf = new SparkConf()
    .setAppName("NewSourceTest4")
    .setMaster("local[2]")
  val sc = new SparkContext(conf)
  //准备数据
  val array = new Array[Int](10000)
  for (i <- 0 to 9999) {
    array(i) = new Random().nextInt(10)
  }
  //生成一个rdd
  val rdd = sc.parallelize(array)
  //数据量很大就先取样
  //rdd.sample(false,0.1)
  //所有key加一操作
  val mapRdd = rdd.map((_, 1))
  //没有加随机前缀的结果
  mapRdd.countByKey.foreach(print) //(0,993)(5,998)(1,974)(6,1030)(9,997)(2,1006)(7,967)(3,970)(8,1043)(4,1022)
  //两阶段聚合(局部聚合+全局聚合)处理数据倾斜
  //加随机前缀
  val prifixRdd = mapRdd.map(x => {
    val prifix = new Random().nextInt(10)
    (prifix + "_" + x._1, x._2)
  })
  //加上随机前缀的key进行局部聚合
  val tmpRdd = prifixRdd.reduceByKey(_ + _)
  //去除随机前缀
  val newRdd = tmpRdd.map(x => (x._1.split("_")(1), x._2))
  //最终聚合
  newRdd.reduceByKey(_ + _).foreach(print)//(4,1022)(7,967)(8,1043)(5,998)(6,1030)(9,997)(0,993)(3,970)(2,1006)(1,974)
  sc.stop()
}
5.为倾斜key增加随机前/后缀案例

适用场景:两张表都比较大,无法使用Map则Join。其中一个RDD有少数几个Key的数据量过大,另外一个RDD的Key分布较为均匀。
解决方案:将有数据倾斜的RDD中倾斜Key对应的数据集单独抽取出来加上随机前缀,另外一个RDD每条数据分别与随机前缀结合形成新的RDD(笛卡尔积,相当于将其数据增到到原来的N倍,N即为随机前缀的总个数),然后将二者Join后去掉前缀。然后将不包含倾斜Key的剩余数据进行Join。最后将两次Join的结果集通过union合并,即可得到全部Join结果。
优势:相对于Map侧Join,更能适应大数据集的Join。如果资源充足,倾斜部分数据集与非倾斜部分数据集可并行进行,效率提升明显。且只针对倾斜部分的数据做数据扩展,增加的资源消耗有限。
劣势:如果倾斜Key非常多,则另一侧数据膨胀非常大,此方案不适用。而且此时对倾斜Key与非倾斜Key分开处理,需要扫描数据集两遍,增加了开销。

6.过滤少数倾斜Key案例

适用场景:如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。
比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。
方案优点:实现简单,而且效果也很好,可以完全规避掉数据倾斜。
方案缺点:适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。
实践经验:在项目中我们也采用过这种方案解决数据倾斜。有一次发现某一天Spark作业在运行的时候突然OOM了,
追查之后发现,是Hive表中的某一个key在那天数据异常,导致数据量暴增。因此就采取每次执行前先进行采样,计算出样本中数据量最大的几个key之后,直接在程序中将那些key给过滤掉。

7.参数优化
./bin/spark-submit \
--master yarn-cluster \
--num-executors 100 \
--executor-memory 6G \
-executor-cores 4 \
--driver-memory 1G \
--conf spark.default.parallelism=1000 \
--conf spark.storage.memoryFraction=0.5 \
--conf spark.shuffle.memoryFraction=0.2 \
--class com.xx \
test.jar


参数解释:
num-executors
参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。
参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。
设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

executor-memory
参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。
参数调优建议:每个Executor进程的内存设置4G~8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。
可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。
此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同事的作业无法运行。

executor-cores
参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,
因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。
参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,
再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,
那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同事的作业运行。

driver-memory
参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,
那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism
参数说明:该参数用于设置每个stage的默认task数量,也可以认为是分区数。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。
参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多人常犯的一个错误就是不去设置这个参数,
那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。
通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。
试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!
因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,
比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction
参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,
可能数据就不会持久化,或者数据会写入磁盘。
参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。
但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),
意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

Shuffle优化配置 spark.shuffle.memoryFraction
参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,
Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。
参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,
必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

Shuffle优化配置 spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。
在实践中发现,合理调节该参数,性能会有1%~5%的提升。

Shuffle优化配置 spark.reducer.maxSizeInFlight
默认值:48m
参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现
,合理调节该参数,性能会有1%~5%的提升。

Shuffle优化配置 spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。
如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。
在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。

Shuffle优化配置 spark.shuffle.io.retryWait
默认值:5s
参数说明: shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

Shuffle优化配置 spark.shuffle.manager
默认值:sort
参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,
但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,
那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

Shuffle优化配置 spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,
而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,
map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

Shuffle优化配置 spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,
这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。
在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。
8.程序开发优化

1.尽可能复用同一个RDD;
2.对多次使用的RDD进行持久化;
3.尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子;
4.使用map-side预聚合的shuffle操作,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子;
5.使用高性能的算子:
6.使用reduceByKey/aggregateByKey替代groupByKey;
7.使用mapPartitions替代普通map;
8.使用foreachPartitions替代foreach;
9.使用filter之后进行coalesce操作;
10.使用repartitionAndSortWithinPartitions替代repartition与sort类操作;
11.使用Kryo优化序列化性能;
12.分区Shuffle优化,当遇到userData和events进行join时,userData比较大,而且join操作比较频繁,这个时候,可以先将userData调用了 partitionBy()分区,可以极大提高效率。
13.优化数据结构,Java中,有三种类型比较耗费内存:
a、对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
b、字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
c、集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。
Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

猜你喜欢

转载自blog.csdn.net/qq_30003943/article/details/107822047