Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

灵魂拷问,如何防止重复提交?

发表于 2020-06-21 | 分类于 springboot

平时开发项目的时候,你是否遇到这样的困惑,用户不停的点击按钮向后端提交数据,而你却束手无策!

阅读全文 »

周六特辑:星球作业之最有意义的技术成长故事

发表于 2020-06-20 | 分类于 感悟

Hello 大家好,我是鸭血粉丝,月初的时候我们再星球发布了一个作业,作业的题目是分享一段对自己最有意义的技术成长故事,并且会对相关回答点赞人数最多的几位星友进行赞赏奖励。作业如下图所示:

阅读全文 »

阿粉给大家总结了如此多的面试题,总有一款你需要的!

发表于 2020-06-20

阿粉最近在Boss上面公开了自己的简历,好多人在疯狂的私聊阿粉,最近是不是有换工作的意向了,要不要考虑一下?于是阿粉躁动的心难以平静,分分钟准备出了大量的面试题,狂刷面试题恶补自己的知识储备,准备新一轮的面试。但是阿粉肯定是不会那么自私的,肯定要给大家也放送上这么多的面试题,让大家一起找到一份高质量高薪资的工作。跟着阿粉来刷一下吧!

阅读全文 »

有的人 28 岁已经退休,而阿粉 28 岁还在“搬砖”

发表于 2020-06-18 | 分类于 感悟

Hello 大家好,我是阿粉,想必大家这几天都被 28 岁字节程序员退休的话题刷屏了。同样作为程序员,阿粉只能默默的表示实名羡慕,但是别人的人生毕竟是别人的人生,我们能做的还是过好自己的人生。

layoff03

最近在跟朋友聊天,说到从毕业开始已经深漂五年了。五年的时光说长不长说短不短,在这五年里的各种艰难只有在一线城市经历过的才懂,有过泪,有过喜,熬过夜,通过宵,有得到也有失去。

阿粉还清晰的记得高考结束填志愿的时候,填的都是计算机相关的专业,那个时候也不知道啥时互联网,只知道计算机听上去挺高大上的。然后就稀里糊涂的进入了计算机学院网络工程专业,刚开始我以为大家都跟我一样是因为对计算机的热爱才选了这个专业,但是慢慢的才发现很多人是调剂过来的,都是因为其他专门满了才被调剂到计算机学院的。虽然阿粉作为科班出身,但是接触电脑还是比较晚的,接触编程也是上大学才开始的,不像有些朋友从小就接触电脑接触编程。

接触的第一门编程语言毫无疑问《谭浩强的 C 语言》,相信很多人都跟阿粉一样编程生涯是从这本书开始的。对于这本书的褒贬阿粉不做评价,有人说不好,有人说好,但是既然能出书并能被纳入大学教材自然有它的道理。

刚接触编程语言的时候,第一感觉就是,这也太神奇好玩了吧!那个时候刚上大学,还没有买电脑,每次都是很期待上机课的到来,因为这样就可以去玩电脑了,可以把书上的代码在机器上运行一遍。还记得那个时候有高年级的学长来给我们辅导,当时在屏幕上输出一个乘法口诀表都很开心。最有成长的是大三的时候,那个时候买了一台笔记本,天天待在老是实验室里面看各种技术视频,跟着视频一行一行敲代码,然后做了一个小网站自己开心了好久。

phone1

后来实验室老师接了项目,让我们帮他做,每个月挣点零花钱,那个时候既能学东西又能挣钱,不用跟家里要生活费,感觉还是很棒的。

现在想想,校园的时光就是那么美好,那个时候的心思也很单纯,真好。

后来大三到深圳的一家公司实习,实习完后公司还不错就就顺利的签了意向书,毕业后可以直接入职,相当于还没毕业就找到了工作,可开心坏了。实习后既然工作已经定了,就没有再找过其他公司,那个时候班里同学整体都在外面跑来跑去,各种面试,而自己则是一门心思的在实验室提升技能,而没有出去面试。(其实这个现在想想挺后悔的,还是要去大公司试试~~)。

刚开始工作的时候,那个时候的自己没有什么其他想法,每天只要好好写自己的代码就可以了,不需要管什么乱七八糟的事情,老大安排什么就做什么,闲暇时候自己写写博客,学习一些新的东西。那个时候考虑的只是如果将老大安排的任务用最好的方式进行实现,保证系统的功能能正常使用和快速提升自己的能力。

那个时候每天都是充实的,因为刚毕业,项目里面能学习的东西很多,天天都是满满的收获。每天十点十一点下班,但是并不觉得累,反正住公司员工宿舍,回去早了也没有事情做,还不如在公司多学习点东西。那个时候跟自己一起进来的小伙伴们还有好多,大家一起学习,一起进步;周末的时候一起加班,一起徒步,一起吃石锅鱼,一起看电影,大家还是很开心的。

慢慢的到后来随着自己参与的项目越来越多,接触的模块和业务也越来越复杂,渐渐的成为小组内的核心人员,这个时候考虑的和思考的已经从个人变成团队了。需要考虑一个模块怎么样设计才合理,框架的选型怎么样才更符合业务需求,系统的功能怎么样设计才能更方便用户使用。很多时候因为一个小小的改动都要纠结很久,因为虽然是系统上的一个小小改动,但是影响的却是几百万的用户。

工作了这么久有很多当时一起工作的小伙伴有的创业了,有的转行了,有的离开深圳回老家发展了。如果要问是什么让阿粉坚持下来,那应该是热爱吧,自己本身还是挺喜欢互联网行业的,而且阿粉觉得程序开发还是很有意思的,喜欢自己折腾完成一个东西。It 这一行学无止境,未来的路还有很长,继续加油。

程序员这行技术的更新迭代太快,我们要时刻准备接受新的知识,才能不被时代淘汰。

jdk01

前段时间看了微笑哥送的《学会写作》, 书中提到在《孙子兵法》中有一句:“胜者先胜而后求战,败者先战而后求胜”。

胜者之所以能常胜是因为在开战之前就已经洞悉了一切,做好了充分的准备,有着百分百必胜的把握,而后才会开战,自然必胜。而败者在开战前没有任何准备,直到战争开始才想着怎样取胜,这样失败的概率必然很大。

总结给我们的知识就是在做任何事情的时候都要做好准备,只有自己准备的充分,才能取得最大的胜利,也就是我们常说的知己知彼,百战不殆。不仅是对待某件事情,对待我们的人生都应该这样,前期做好准备,集聚能量,蓄势待发,在关键时刻必然一飞冲天。

阅读全文 »

口述完SpringMVC的执行流程后,面试官说兄弟,你是培训的吧!

发表于 2020-06-18

前几天阿粉的一个朋友去面试,面试官问他,你知道SpringMVC的执行流程么,我这个朋友在回答完之后,面试官相继问了几个问题,之后面试官说,兄弟你是培训出来的吧?朋友懵了,我培训都是一年前的事情了,这都能知道,于是,找阿粉来吐槽这个事情,结果,阿粉听完之后,分分钟觉得,确实不冤枉呀。

阅读全文 »

谁再说端午节不能说节日快乐,你就把这篇文章甩给他

发表于 2020-06-16 | 分类于 程序人生

阿粉扒一扒端午节的由来

阅读全文 »

在这一场 Black Lives Matter 运动中,程序员做了些什么?

发表于 2020-06-13 | 分类于 Java

Hello 大家好,我是鸭血粉丝,想必大家跟阿粉一样,最近刷抖音或者看新闻的时候都发现最美国的形式特别混乱,各个地方抗议游行,抢砸。这所有的一切都源于在美国根深蒂固的种族歧视,阿粉真不明白这些人是咋回事总以为自己高人一等。事情的缘由相比大家都很清楚,简单来说就是美国白人警察暴力执法导致非洲裔男子乔治·弗洛伊德死亡,今天我们不谈政治,我们聊聊程序员在这起事件中做了什么举措。

Chrome 取消 blacklist 和 whitelist

Google 最近发布通知,在 Chrome 浏览器源代码中将取消使用黑名单(blacklist)和白名单(whitelist)取而代之的是采用 blocklist 和 allowlist。其实从 2018 年开始 Google 就在尽量避免使用 blacklist 这个词,这次的事件只不过是个导火索而已,据了解在 Chrome 的源码中大概有 2000 处使用了 blacklist,这个单词的调整无疑是对程序员的一个巨大考验。万一某个地方漏掉了或者改错了产生了 bug,对程序员来说都是坑。不得不说我们程序员太难了,哈哈哈。

GitHub 默认分支名从 master 修改成 main

微软公司的程序员 Scott Hanselman 在其博客上发文,说希望取消 Git 版本控制软件中默认的 master 分支名称,而是修改成 main。大家都知道现在全球最大的程序员同性网站 GitHub 上默认的分支名称是 master,而作者倡议将默认的分支名称修改为 main。说实话阿粉觉得改成 main 也挺好的,毕竟 master 一词在英文中,因为历史的缘故并不友好。

但是这个也还是要结合实际的,首先修改的难度先不说,毕竟可能会因为修改一个名称而导致很多连锁反应,这就得不偿失了。

image-20200613231540939

master-slave 主从名称受到挑战

相比 GitHub 的分支名称的调整,对于大多数分布式环境来说,调整 master-slave 就更有挑战了。大家都知道在软件帝国里面很多软件在分布式环境中都有主从的概念,而主从的英文是 master-slave,这个词汇很明显最近也被热议,很多人觉得这个表述不好。想必大家都知道日常使用最多的 Redis,在集群的环境下就涉及的主从的概念,如果说要对世面上所以的软件都进行词汇的调整的话,无疑会是一个巨大的工程,这个对于 Redis 的贡献者毫无疑问又是一个巨大的坑。

不管对于 blacklist,master 分支名称还是 master-slave,在阿粉看来有些事情并不是说修改一个名称就能解决的。历史遗留问题是需要每个人经过时间的累积才能慢慢改变,对于那些思想不正确的人需要日积月累的去改变自己的想法。如果本身思想没问题,及时天天使用相关词汇也不会有问题,而对于思想不正确的人来说,总能从其他地方找到理由。

延伸部分

从之前的美国对华为,中兴的抵制开始,以及的最近的国内部分大学被禁止使用Matlab,都说科学无国界,但是很多时候事实并不是这样。在技术部分,我们还有很多要依赖国外的技术,所以阿粉作为一个程序员深知要提高自身能力的重要性,为国内的技术贡献一点自己的力量!

写在最后


最后邀请你加入我们的知识星球,这里有 1700+ 优秀的人与你一起进步,如果你是小白那你是稳赚了,很多业内经验和干活分享给你;如果你是大佬,那可以进来我们一起交流分享你的经验,说不定日后我们还可以有合作,给你的人生多一个可能。

阅读全文 »

手把手教你,本地搭建虚拟机部署微服务

发表于 2020-06-11 | 分类于 虚拟机

关于虚拟机这块以前玩的也很多,但很少总结,容易遗忘,今天索性一条龙总结搞定!

阅读全文 »

高效读取大文件,再也不用担心 OOM 了!

发表于 2020-06-10 | 分类于 Java

最近阿粉接到一个需求,需要从文件读取数据,然后经过业务处理之后存储到数据库中。这个需求,说实话不是很难,阿粉很快完成了第一个版本。

阅读全文 »

Read Large File

发表于 2020-06-10
高效读取大文件,再也不用担心 OOM 了!

最近阿粉接到一个需求,需要从文件读取数据,然后经过业务处理之后存储到数据库中。这个需求,说实话不是很难,阿粉很快完成了第一个版本。

内存读取

第一个版本,阿粉采用内存读取的方式,所有的数据首先读读取到内存中,程序代码如下:

Stopwatch stopwatch = Stopwatch.createStarted();
// 将全部行数读取的内存中
List<String> lines = FileUtils.readLines(new File("temp/test.txt"), Charset.defaultCharset());
for (String line : lines) {
    // pass
}
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");
// 计算内存占用
logMemory();

logMemory方法如下:

MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
//椎内存使用情况
MemoryUsage memoryUsage = memoryMXBean.getHeapMemoryUsage();
//初始的总内存
long totalMemorySize = memoryUsage.getInit();
//已使用的内存
long usedMemorySize = memoryUsage.getUsed();

System.out.println("Total Memory: " + totalMemorySize / (1024 * 1024) + " Mb");
System.out.println("Free Memory: " + usedMemorySize / (1024 * 1024) + " Mb");

上述程序中,阿粉使用 Apache Common-Io 开源第三方库,FileUtils#readLines将会把文件中所有内容,全部读取到内存中。

这个程序简单测试并没有什么问题,但是等拿到真正的数据文件,运行程序,很快程序发生了 OOM。

之所以会发生 OOM,主要原因是因为这个数据文件太大。假设上面测试文件 test.txt总共有 200W 行数据,文件大小为:740MB。

通过上述程序读取到内存之后,在我的电脑上内存占用情况如下:

可以看到一个实际大小为 700 多 M 的文件,读到内存中占用内存量为 1.5G 之多。而我之前的程序,虚拟机设置内存大小只有 1G,所以程序发生了 OOM。

当然这里最简单的办法就是加内存呗,将虚拟机内存设置到 2G,甚至更多。不过机器内存始终有限,如果文件更大,还是没有办法全部都加载到内存。

不过仔细一想真的需要将全部数据一次性加载到内存中?

很显然,不需要!

在上述的场景中,我们将数据到加载内存中,最后不还是一条条处理数据。

所以下面我们将读取方式修改成逐行读取。

逐行读取

逐行读取的方式比较多,这里阿粉主要介绍两种方式:

  • BufferReader

  • Apache Commons IO

  • Java8 stream

BufferReader

我们可以使用 BufferReader#readLine 逐行读取数据。

try (BufferedReader fileBufferReader = new BufferedReader(new FileReader("temp/test.txt"))) {
    String fileLineContent;
    while ((fileLineContent = fileBufferReader.readLine()) != null) {
        // process the line.
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

Apache Commons IO

Common-IO 中有一个方法 FileUtils#lineIterator可以实现逐行读取方式,使用代码如下:

Stopwatch stopwatch = Stopwatch.createStarted();
LineIterator fileContents = FileUtils.lineIterator(new File("temp/test.txt"), StandardCharsets.UTF_8.name());
while (fileContents.hasNext()) {
    fileContents.nextLine();
    //  pass
}
logMemory();
fileContents.close();
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");

这个方法返回一个迭代器,每次我们都可以获取的一行数据。

其实我们查看代码,其实可以发现 FileUtils#lineIterator,其实用的就是 BufferReader,感兴趣的同学可以自己查看一下源码。

由于公号内无法插入外链,关注『Java 极客技术』,回复『20200610』 获取源码

Java8 stream

Java8 Files 类新增了一个 lines,可以返回 Stream我们可以逐行处理数据。

Stopwatch stopwatch = Stopwatch.createStarted();
// lines(Path path, Charset cs)
try (Stream<String> inputStream = Files.lines(Paths.get("temp/test.txt"), StandardCharsets.UTF_8)) {
    inputStream
            .filter(str -> str.length() > 5)// 过滤数据
            .forEach(o -> {
                // pass do sample logic
            });
}
logMemory();
stopwatch.stop();
System.out.println("read all lines spend " + stopwatch.elapsed(TimeUnit.SECONDS) + " s");

使用这个方法有个好处在于,我们可以方便使用 Stream 链式操作,做一些过滤操作。

注意:这里我们使用 try-with-resources 方式,可以安全的确保读取结束,流可以被安全的关闭。

并发读取

逐行的读取的方式,解决我们 OOM 的问题。不过如果数据很多,我们这样一行行处理,需要花费很多时间。

上述的方式,只有一个线程在处理数据,那其实我们可以多来几个线程,增加并行度。

下面在上面的基础上,阿粉就抛转引玉,介绍下阿粉自己比较常用两种并行处理方式。

逐行批次打包

第一种方式,先逐行读取数据,加载到内存中,等到积累一定数据之后,然后再交给线程池异步处理。

@SneakyThrows
public static void readInApacheIOWithThreadPool() {
    // 创建一个 最大线程数为 10,队列最大数为 100 的线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
    // 使用 Apache 的方式逐行读取数据
    LineIterator fileContents = FileUtils.lineIterator(new File("temp/test.txt"), StandardCharsets.UTF_8.name());
    List<String> lines = Lists.newArrayList();
    while (fileContents.hasNext()) {
        String nextLine = fileContents.nextLine();
        lines.add(nextLine);
        // 读取到十万的时候
        if (lines.size() == 100000) {
            // 拆分成两个 50000 ,交给异步线程处理
            List<List<String>> partition = Lists.partition(lines, 50000);
            List<Future> futureList = Lists.newArrayList();
            for (List<String> strings : partition) {
                Future<?> future = threadPoolExecutor.submit(() -> {
                    processTask(strings);
                });
                futureList.add(future);
            }
            // 等待两个线程将任务执行结束之后,再次读取数据。这样的目的防止,任务过多,加载的数据过多,导致 OOM
            for (Future future : futureList) {
                // 等待执行结束
                future.get();
            }
            // 清除内容
            lines.clear();
        }

    }
    // lines 若还有剩余,继续执行结束
    if (!lines.isEmpty()) {
        // 继续执行
        processTask(lines);
    }
  threadPoolExecutor.shutdown();
}
    private static void processTask(List<String> strings) {
        for (String line : strings) {
            // 模拟业务执行
            try {
                TimeUnit.MILLISECONDS.sleep(10L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

上述方法,等到内存的数据到达 10000 的时候,拆封两个任务交给异步线程执行,每个任务分别处理 50000 行数据。

后续使用 future#get(),等待异步线程执行完成之后,主线程才能继续读取数据。

之所以这么做,主要原因是因为,线程池的任务过多,再次导致 OOM 的问题。

大文件拆分成小文件

第二种方式,首先我们将一个大文件拆分成几个小文件,然后使用多个异步线程分别逐行处理数据。

public static void splitFileAndRead() throws Exception {
    // 先将大文件拆分成小文件
    List<File> fileList = splitLargeFile("temp/test.txt");
    // 创建一个 最大线程数为 10,队列最大数为 100 的线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 60l, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
    List<Future> futureList = Lists.newArrayList();
    for (File file : fileList) {
        Future<?> future = threadPoolExecutor.submit(() -> {
            try (Stream inputStream = Files.lines(file.toPath(), StandardCharsets.UTF_8)) {
                inputStream.forEach(o -> {
                    // 模拟执行业务
                    try {
                        TimeUnit.MILLISECONDS.sleep(10L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        futureList.add(future);
    }
    for (Future future : futureList) {
        // 等待所有任务执行结束
        future.get();
    }
    threadPoolExecutor.shutdown();


}

private static List<File> splitLargeFile(String largeFileName) throws IOException {
    LineIterator fileContents = FileUtils.lineIterator(new File(largeFileName), StandardCharsets.UTF_8.name());
    List<String> lines = Lists.newArrayList();
    // 文件序号
    int num = 1;
    List<File> files = Lists.newArrayList();
    while (fileContents.hasNext()) {
        String nextLine = fileContents.nextLine();
        lines.add(nextLine);
        // 每个文件 10w 行数据
        if (lines.size() == 100000) {
            createSmallFile(lines, num, files);
            num++;
        }
    }
    // lines 若还有剩余,继续执行结束
    if (!lines.isEmpty()) {
        // 继续执行
        createSmallFile(lines, num, files);
    }
    return files;
}

由于公号内无法插入外链,关注『Java 极客技术』,回复『20200610』 获取源码

上述方法,首先将一个大文件拆分成多个保存 10W 行的数据的小文件,然后再将小文件交给线程池异步处理。

由于这里的异步线程每次都是逐行从小文件的读取数据,所以这种方式不用像上面方法一样担心 OOM 的问题。

另外,上述我们使用 Java 代码,将大文件拆分成小文件。这里阿粉还有一个简单的办法,我们可以直接使用下述命令,直接将大文件拆分想小文件:

 # 将大文件拆分成 100000 的小文件
 split -l 100000 test.txt

后续 Java 代码只需要直接读取小文件即可。

总结

当我们从文件读取数据时,如果文件不是很大,我们可以考虑一次性读取到内存中,然后快速处理。

如果文件过大,我们就没办法一次性加载到内存中,所以我们需要考虑逐行读取,然后处理数据。但是单线程处理数据毕竟有限,所以我们考虑使用多线程,加快处理数据。

本篇文章我们只是简单介绍了下,数据从文件读取几种方式。数据读取之后,我们肯定还需要处理,然后最会会存储到数据库中或者输出到另一个文件中。

这个过程,说实话比较麻烦,因为我们的数据源文件,可能是 txt,也可能是 excel,这样我们就需要增加多种读取方法。同样的,当数据处理完成之后,也有同样的问题。

不过好在,上述的问题我们可以使用 Spring Batch 完美解决。

下一篇文章我们就来看看 Spring Batch 如何解决,敬请期待~

阅读全文 »

荐剧:如果你是柯南迷,这部剧场版一定要再看看

发表于 2020-06-10 | 分类于 程序人生

一部阿粉非常喜欢的剧场版

阅读全文 »

学习Redis好一阵了,我对它有了一些新的看法

发表于 2020-06-08

前言

本篇文章不是一篇具体的教程,阿粉打算记录一下自己对Redis的一些思考。说来惭愧,阿粉刚接触Redis的时候只是简单地使用了一下,背了一些面试题,就在简历上写下了Redis这个技能点。

我们能在网络上轻易地找到关于Redis具体知识点的讲解,但很少有文字说明为什么会有这项技术,阿粉希望通过本文总结一下个人目前对Redis的理解。

阅读全文 »

【Spring 二】面试官:兄弟你来阐述一下Spring框架中Bean的生命周期?

发表于 2020-06-07

前几天在阿粉CSDN上看一个文章,因为一个Spring的问题,期望薪资三万却被生生的压榨成了两万五,高于两万五人家都不要,让我感觉到了Spring的强大,不学习Spring是会吃亏的,那么我们就从各种高频面试来一点点深入吧。

阅读全文 »

关于参数合法性验证,阿粉有话要说

发表于 2020-06-04 | 分类于 springboot

如何优雅的进行参数验证,需要了解的都在这里!

阅读全文 »

面试的时候按照这个套路回答 Java GC 的相关问题一定能过!

发表于 2020-06-04 | 分类于 Java

Hello 大家好,我是鸭血粉丝,2020 注定是一个不平凡的一年,很多小伙伴在后台和星球留言都说今年的工作不好找,也有应届生小伙伴给阿粉发消息问阿粉所在的公司今年是否招应届生,阿粉也只能帮小伙伴问问 HR,但是阿粉也做不了主。

刚好前几天一个小伙伴在微信上问阿粉,说是面试一家公司被问到 Java GC 相关的东西,虽然平时也有准备,但是回答起来总是零零散散,感觉没有逻辑。其实阿粉也能理解,现在面试都是一种问答模式,一个随便问问,一个死记硬背,很多时候面试前准备的好好的一到面试的时候可能紧张就给忘了,事后感觉自己表现的不好。

这篇文章给大家举个例子,在遇到一个问题或者知识点的时候要怎么去理解和学习。

Java GC

目标

遇到一个问题或者一个知识点,我们要理解和明白是要解决什么问题的。说到 Java GC 那这个 GC 的目的是什么呢?很显然是回收内存,因为内存是有限的,随着程序中创建的对象越来越多,如果进行回收就会导致内存越来越大,最后程序就会出现异常。既然目的是为了回收内存,那么新的问题来了,哪些对象可以被回收呢?什么时候进行回收呢?怎么回收呢?

哪些对象可以被回收

简单来说就是无用的对象可以被回收,那么换句话说,如果定义一个对象是无用的呢?这里主要有两种方法,一个叫引用计数法,一个叫可达性分析法。

引用计数

引用计数说的是如果一个对象被别的对象进行了一次引用,那么该对象会有一个引用计数器,这个计数器就会加一;如果被释放一下,引用计数器就会减一。当引用计数器的计数为 0 的时候就表示这个对象是无用的,此时就可以对这样对象进行回收了。表面上看好像挺合理的,实现起来也很方便,但是仔细一想就会发现有问题。既循环引用的问题,比如对象 A 引用了对象 B,但是对象 B 当中也引用了对象 A,那么这个时候对象 A 和对象 B 的引用计数器的计数都不会是 0,但是这两个对象都没有被其他对象引用,理论上来说这两个对象都是可以被回收的。

从上面看到,这种方案是有问题的会导致内存泄露。随之而来的就出现了另一种方案,可就是可达性分析。

可达性分析

可达性分析说的是从 GCRoots 的点作为起点,向下搜索,当找不到任何引用链的时候表示该对象为垃圾对象。那么哪些对象可以被认为是 Roots 节点呢?有 Java 栈中的对象,方法区的静态属性和常量以及本地方法栈中的对象。从这几种对象依次向下搜索,如果没有能达到 Roots 节点的对象就是垃圾对象,就说明可以被回收。

如下图所有,对象 A,B,C都能找到与 Roots 节点的联系,但是对象 D,E,F 三个并不能找到与 Roots 节点的联系,也就是不可达,所以 DEF 这三个对象就是垃圾对象。

什么时候回收

上面的两种方案解决了哪些对象能被回收,那么下个问题,就是什么时候进行垃圾回收呢?在排除人为调用的时候,垃圾回收都是发生在为新生对象进行内存分配的时候,这个时候如果内存空间不足就会触发 GC 进行垃圾回收。

怎么回收

上面我们知道了哪些对象可以被回收,也知道我们应该什么时候进行回收,那下面要解决的就是如何进行垃圾回收了。垃圾回收根据实现的方式不同有多种不同的算法实现。比如有标记清除算法,复制算法,标记整理算法,分代回收算法,下面简单介绍一下,想深入了解的可以自行去研究一下。

标记清除算法

标记清除算法很好理解,主要就是执行两个动作,一个是标记,另一个是对进行标记的对象内存进行清除回收。这个算法有个问题就是会出现内存碎片化严重。如下图所示:

从上图中可以看到,在进行内存回收后出现了严重的内存碎片化,这就导致在分配某些大对象的时候仍然会出现内存不够的情况,但是总体内存确是够的。

复制算法

复制算法的实现方式比较简洁明了,就是霸道的把内存分成两部分,在平时使用的时候只用其中的固定一份,在当需要进行 GC 的时候,把存活的对象复制到另一部分中,然后将已经使用的内存全部清理掉。如下图:

从上图可以看到解决了标记清除的内存碎片化问题,但是很明显复制算法有另一个问题,那就是内存的使用率大大下降,能使用的内存只有原来的一半了。

标记整理算法

既然标记清除和复制算法各有优缺点,那自然的我们就想到是否可以把这两种算法结合起来,于是就出现了标记整理算法。标记阶段是标记清除算法一样,先标记出需要回收的部分,不过清除阶段不是直接清除,而是把存活的对象往内存的一端进行移动,然后清除剩下的部分。如下图:

标记整理的算法虽然可以解决上面两个算法的一些问题,但是还是需要先进行标记,然后进行移动,整个效率还是偏低的。

分代回收算法

分代回收算法是目前使用较多的一种算法,这个不是一个新的算法,只是将内存进行的划分,不同区域的内存使用不同的算法。根据对象的存活时间将内存的划分为新生代和老年代,其中新生代包含 Eden 区和 S0,S1。在新生代中使用是复制算法,在进行对象内存分配的时候只会使用 Eden 和 S0 区,当发生 GC 的时候,会将存活的对象复制到 S1 区,然后循环往复进行复制。当某个对象在进行了 15 次GC 后依旧存活,那这个对象就会进入老年代。老年代因为每次回收的对象都会比较少,因此使用的是标记整理算法。

垃圾回收器

讲完了垃圾回收算法,我们再看下垃圾回收器,每一种垃圾回收器都是不同时代的不同产物,都有其独特性。

  • Serial 垃圾收集器(单线程、复制算法)
  • ParNew垃圾收集器(Serial+多线程)
  • Parallel Scavenge 收集器(多线程复制算法、高效)
  • SerialOld收集器(单线程标记整理算法)
  • ParallelOld收集器(多线程标记整理算法)
  • CMS收集器(多线程标记清除算法)
  • G1收集器

各个垃圾收集器的配合使用情况可以参考下图,个人觉得对这么多的收集器没有必要全部精通,可以注重关注一下 CMS 和 G1 就可以了。感兴趣的小伙伴可以自己的研究一下。

关于本文的高清脑图已经放到公众号上面了,欢迎关注公众号回复【gc】获得。

写在最后


最后邀请你加入我们的知识星球,这里有 1700+ 优秀的人与你一起进步,如果你是小白那你是稳赚了,很多业内经验和干活分享给你;如果你是大佬,那可以进来我们一起交流分享你的经验,说不定日后我们还可以有合作,给你的人生多一个可能。

阅读全文 »

阿粉写了八千多字,就是为了把 ReentrantLock 讲透

发表于 2020-05-31 | 分类于 Java并发

关于 ReentrantLock 看这篇文章就够了

阅读全文 »

AQS 是个啥?

发表于 2020-05-30 | 分类于 Java并发

啥?你连 AQS 是啥都不知道?

阅读全文 »

MySQL如何有效的存储IP地址?

发表于 2020-05-27 | 分类于 java

前几天,阿粉的一个朋友去面试,他回来告诉我,面试官问他 IP 地址是怎么存在数据库的?他当时也没多想,直接就回答的存字符串啊(心想:这么简单的问题,怕不是看不起我吧)

阅读全文 »

网传三星手机大半夜黑屏乱码,原因竟然是闰四月?

发表于 2020-05-26 | 分类于 Java

Hello 大家好,我是鸭血粉丝,今年闰四月,所以阿粉非常羡慕那些农历四月过生日的朋友,因为今年可以过两个,想想都很激动,但是激动归激动,最激动应该是那些用着三星手机的用户了,因为一个闰四月让他们的手机差点又“爆炸”了。

最近微博上有网友曝出自己的三星手机在 5 月 23 号凌晨的时候突然出现黑屏乱码,自动循环重启的问题。维修手机都用户都需要排队了,客服表示是农历闰年 4 月计算逻辑错误的原因(这个锅不知道哪个程序员要背了),而且升级后数据可能会丢失。

微博上很多用户都表示自己的手机出现了这个问题,出现如下乱码(感觉回到了交互模式的年代,有没有很亲切啊)。

就连专柜里面的手机也没有幸免,堪称尴尬翻车现场,感觉就像出师未捷身先死。

问题出现不就后三星官方微博就发了通知,表示收到了反馈,积极排查中,有点略显尴尬,就像聊天的过程中等了半天等来了一句“嗯,我知道,你等等”。

有网友提供了一个临时解决方案如下,但是阿粉没有用三星手机,不知道是否有效果,只能说慎用。保证安全还是到官方售后去维修吧。

重启之后双击电源键,来到拍照界面,这时候下拉通知栏,点击设置按钮,搜索时间,把日期往前改一个月,就可以解决了,亲测有效。然后就静候官方修复吧。

写在最后


最后邀请你加入我们的知识星球,这里有 1700+ 优秀的人与你一起进步,如果你是小白那你是稳赚了,很多业内经验和干活分享给你;如果你是大佬,那可以进来我们一起交流分享你的经验,说不定日后我们还可以有合作,给你的人生多一个可能。

阅读全文 »

为什么会有重排序?和 happens-before 有啥关系

发表于 2020-05-26 | 分类于 Java并发

小朋友,你是不是有很多问号?

阅读全文 »
1 … 6 7 8 … 26
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

514 日志
113 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2021 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist