得物深入浅出解析JVM中的Safepoint

news2024/11/25 7:13:12

1.初识Safepoint-GC中的Safepoint

最早接触JVM中的安全点概念是在读《深入理解Java虚拟机》那本书垃圾回收器章节的内容时。相信大部分人也一样,都是通过这样的方式第一次对安全点有了初步认识。不妨,先复习一下《深入理解Java虚拟机》书中安全点那一章节的内容。

书中是在讲解垃圾收集器-垃圾收集算法的章节引入安全点的介绍,为了快速准确地完成GC Roots枚举,避免为每条指令都生成对应的OopMap造成大量存储空间的浪费,只在“特定的位置”生成对应的OopMap,这些位置被称为安全点。然后,书中提到了安全点位置的选择标准是:是否能让程序长时间执行;所以会在方法调用、循环跳转、异常跳转等处才会产生安全点。

书中还提到了JVM如何在GC时让用户线程在最近的安全点处停顿下来:抢先式中断和主动式中断。抢先式中断不需要线程的执行代码主动去配合,在GC发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。而主动式中断的思想是当GC需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时不停地主动去轮询这个标志,一旦发现中断标志为真就自己在最近的安全点上主动中断挂起。现在基本上所有虚拟机实现都采用主动式中断方式来暂停线程响应GC事件。

总结一下初识安全点学到的知识点:

  • JVM GC时需要让用户线程在安全点处停顿下来(Stop The World)

  • JVM会在方法调用、循环跳转、异常跳转等处放置安全点

  • JVM通过主动中断方式到达全局STW:设置一个标志位,各个线程执行过程时不停地主动去轮询这个标志,一旦发现中断标志为真就自己在最近的安全点上主动中断挂起。

以上基本上就是《深入理解Java虚拟机》这本书对JVM安全点的所有介绍了,当时觉得安全点还是很好理解,认为安全点就是在垃圾回收时为了STW而设计的。

后来发现,经过一些线上问题和网上看到有关安全点有趣的示例,发现安全点其实也不简单,不只有GC才会用到安全点;简单的代码如果写的不当,安全点也会带来一些莫名其妙的问题;其在JVM内部的实现以及JIT对它的优化,也经常让人摸不着头脑。本文尝试在初识安全点后已知知识点的基础上,通过一段简单的示例代码,多问几个为什么,来进一步更全面的了解一下安全点。

2.通过一段示例代码深入剖析Safepoint

2.1  示例代码

这段示例代码可直接复制到本地运行,本文所有对示例代码的运行环境都是jdk 1.8。

public class SafePointTest {

    public static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws Exception{
        long startTime = System.currentTimeMillis();
        Runnable runnable = () -> {
            System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子线程开始运行");
            for(int i = 0; i < 100000000; i++) {
                counter.getAndAdd(1);
            }
            System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子线程结束运行, counter=" + counter);
        };

        Thread t1 = new Thread(runnable, "zz-t1");
        Thread t2 = new Thread(runnable, "zz-t2");

        t1.start();
        t2.start();

        System.out.println(interval(startTime) + "ms后,主线程开始sleep.");

        Thread.sleep(1000L);

        System.out.println(interval(startTime) + "ms后,主线程结束sleep.");
        System.out.println(interval(startTime) + "ms后,主线程结束,counter:" + counter);
    }

    private static long interval(Long startTime) {
        return System.currentTimeMillis() - startTime;
    }
}

示例代码中主线程启动两个子线程,然后主线程睡眠1s,通过打印时间来观察主线程和子线程的执行情况。

按道理来说这里主线程和两个子线程独立并发,没有任何显性的依赖,主线程的执行是不会受子线程影响的:主线程睡眠结束后会直接结束。但是执行结果却和期望不一样。

执行结果如下方动图展示:

 

从执行结果看,主线程在启动两个线程后进入睡眠状态,代码中指定睡眠时间为1s,但是主线程却在3s多之后才睡眠结束。是什么导致了主线程睡过头了呢,从结果来看主线程睡觉结束时间和子线程结束时间是一致的。所以,我们有理由怀疑主线程没有按时提前结束应该是被两个子线程阻塞了。

2.2  先给结论

由于VMThread的某些操作需要STW,主线程在sleep结束前进入了JVM全局安全点,然后主线程要等待其他线程全部进入安全点,所以主线程被长时间没有进入安全点的其他线程给阻塞了。

2.3  验证结论

添加JVM打印安全点日志参数-XX:+PrintSafepointStatistics后再执行上面的实例代码,结果如下截图:

可以从安全点日志中看到,JVM想要执行no vm operation,这个操作需要线程进入安全点,整个期间有12个线程,正在运行的线程有两个,需要等待这两个线程进入安全点,等待耗时2251ms。

加上 -XX:+SafepointTimeout 和-XX:SafepointTimeoutDelay=2000 参数后执行代码可以进一步看等待哪两个线程进入安全点。

果然和猜测的一样,没有到达安全点的两个线程正是示例代码中定义的zz-t1和zz-t2线程。

2.4  为什么

到这里这个示例的执行结果的原因已经有了结论并且得到了验证,基本上已经知其然了。但是如果深入思考一下,初识安全点时学到的知识点还不能解释,所以为了知其所以然,这里提了几个为什么。

(1)为什么会进入安全点

换句话问,是什么触发了进入安全点?

由初识安全点得到的基础知识知道进入安全点需要两个条件:

  • JVM操作设置了主动中断标志

  • 运行的代码中存在安全点

首先想到的是GC触发JVM设置主动中断标志,加上 -XX:-PrintGC再执行示例代码并没有打印 GC 日志,可以排除掉GC。

既然不是GC,还是再回到安全点日志上寻找线索吧,发现有个vmop(虚拟机操作类型):no vm operation关于no vm operation,网上有大神通过解析JVM源码得到了结论,这里不对JVM源码展开做详细解读,直接给结论:

在 JVM 正常运行的时候,如果设置了进入安全点的间隔,就会隔一段时间判断是否有代码缓存要清理,如果有,会进入安全点。这个触发条件不是 VM 操作,所以会将 _vmop_type 设置成-1,输出日志的时候打印对应的 「no vm operation」,也就是我们看到的安全点日志。

在 VM 操作为空的情况下,只要满足以下 3 个条件,也是会进入安全点的:

1、VMThread 处于正常运行状态

2、设置了进入安全点的间隔时间

3、SafepointALot 是否为 true 或者是否需要清理

用 Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint 命令查看 JVM 关于安全点的默认参数:

发现 GuaranteedSafepointInterval 默认设置成了 1 秒,每隔1s就会尝试进入安全点。

那么,修改GuaranteedSafepointInterval参数值,看看是否能阻止进入安全点。

GuaranteedSafepointInterval参数是JVM诊断参数,修改这个参数的值,需要配合-XX:+UnlockDiagnosticVMOptions一起使用。

另外不建议在线上对这个参数的值做修改。

  • 关闭定时进入安全点

通过 -XX:GuaranteedSafepointInterval = 0 关闭定时进入安全点,看看代码运行结果是怎么样的

由运行结果可以看出,关闭定时进入安全点后,主线程睡眠1s后正常结束,不受其他线程阻塞。从安全点日志看,之前等待进入安全点的两个线程也没有了。

  • 调大定时进入安全点间隔时间

由打印的执行结果可以看到子线程运行时间是3s多,如果把进入安全点间隔时间调整为5s,即在子线程结束之后再尝试进入安全点是不是也能避免等待子线程进入安全点呢?

修改参数-XX:GuaranteedSafepointInterval = 5000 调整安全点间隔时间再次执行结果:

从执行结果可以看出,调大安全点间隔时间和关闭定时进入安全点的效果是一样的,也可以避免等待子线程进入安全点的。

(2)主线程是在哪里进入的安全点

从示例代码在默认JVM参数执行结果看,主线程睡眠时间超过了3s,事实上主线程是在Thread.sleep()方法内部进入安全点。这里对JVM 安全点实现的源码简单做一下分析:

Safepoint实现源代码:Safepoint.cpp

读源码太费劲,看注释吧,所幸从注释中也能找到答案。上面截图的注释说在程序进入 Safepoint 的时候,Java 线程可能正处于的五种不同的状态,针对不同的状态的不同处理机制。假设现在有一个操作触发了某个 VM 线程所有线程需要进入 SafePoint,如果其他线程现在:

  • 运行字节码:运行字节码时,解释器会看线程是否被标记为 poll armed,如果是,VM 线程调用 SafepointSynchronize::block(JavaThread *thread)进行 block。

  • 运行 native 代码:当运行 native 代码时,VM 线程略过这个线程,但是给这个线程设置 poll armed,让它在执行完 native 代码之后,它会检查是否 poll armed,如果还需要停在 SafePoint,则直接 block。

  • 运行 JIT 编译好的代码:由于运行的是编译好的机器码,直接查看本地 local polling page 是否为脏,如果为脏则需要 block。这个特性是在 Java 10 引入的 JEP 312: Thread-Local Handshakes 之后,才是只用检查本地 local polling page 是否为脏就可以了。

  • 处于 BLOCK 状态:在需要所有线程需要进入 SafePoint 的操作完成之前,不许离开 BLOCK 状态

  • 处于线程切换状态或者处于 VM 运行状态:会一直轮询线程状态直到线程处于阻塞状态(线程肯定会变成上面说的那四种状态,变成哪个都会 block 住)。

再看一下Thread.sleep方法的声明,就和上面Safepoint.cpp源码注释截图红框对上了,Thread.sleep正是一个native方法。

Thread.sleep(0)在RocketMQ中的妙用

上面这段代码是RocketMQ的一段代码,16年最早版本的实现for循环内每循环1000次会调用一次Thread.sleep(0),这貌似是一段无用的代码,作者真实的目的是为了在这里放置一个安全点,避免for循环运行时间过长导致系统长时间SWT。从代码的变更记录看,22年9月份有人对这段代码换了一种写法:把for循环变量类型定义成long型,同时注释掉了循环内部Thread.sleep(0)代码,为什么可以这样写以及为什么要这样写这里先按下不表。

(3)子线程为什么无法进入安全点

现在已经知道了主线程为什么进入会进入安全点,以及主线程在哪里进入的安全点,按照已知知识点JVM会在循环跳转处和方法调用处放置安全点,为什么子线程没有进入安全点?

可数循环和不可数循环

JVM为了避免安全点过多带来过重的负担,对循环有一项优化措施,认为循环次数较少的话,执行时间应该不会太长,所以使用int类型和范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环,相对应的,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环,将被放置安全点。

在示例代码中,子线程的循环索引值数据类型是int,也就是可数循环,所以JVM没有在循环跳转处放置安全点。

把循环索引值数据类型改成long型,循环成为不可数循环,就能够成功在循环跳转处放置安全点,避免子线程长时间无法进入安全点阻塞主线程。

从上面的执行结果可以看到,把循环索引值数据类型改成long型,主线程在睡眠1s之后立即结束了睡眠,并没有等待子线程的执行。

到这里,也就知道为什么上面贴的RocketMQ大那段代码,把循环索引值数据类型改成long型可以替换循环内部Thread.Sleep(0)达到放置安全点的目的了。

其实,还可以通过-XX:+UseCountedLoopSafepoints参数关闭JVM 对可数循环放置安全点的优化。下面的执行结果可以看出,添加了-XX:+UseCountedLoopSafepoints参数后,也能让运行结果到达预期。

还有一个疑惑

仔细看实例代码,发现子线程循环体内调用了AtomicInteger类的getAndAdd方法,再深入看jdk getAndAdd方法的实现,发现底层是调用了sun.misc.Unsafe#getIntVolatile 这个方法和Thread.sleep方法一样,也是一个native方法,为什么这里没有进入像Thread.sleep方法一样进入安全点?

是的,好可怕,确实被优化了,被 JIT给优化了。为了验证是被JIT优化了,可以用

-Djava.compiler=NONE关闭JIT然后看一下运行结果。

从运行结果看,关闭了JIT优化后,主线程确实在睡眠1s后立即结束了,不过子线程运行的时间比JIT优化开启时多了不少。所以,JIT还是能够带来一定的性能优化的,有时也会带来一些奇怪的现象。

3.更全面的安全点定义

区别于初识安全点的时候局限于GC中的安全点概念,这里给安全点一个比较全面的定义:

Safepoint 可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停。在 SafePoint 保存了其他位置没有的一些当前线程的运行信息,供其他线程读取。这些信息包括:线程上下文的任何信息,例如对象或者非对象的内部指针等等。我们一般这么理解 SafePoint,就是线程只有运行到了 SafePoint 的位置,他的一切状态信息,才是确定的,也只有这个时候,才知道这个线程用了哪些内存,没有用哪些;并且,只有线程处于 SafePoint 位置,这时候对 JVM的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,之后继续运行,每个线程都有一份自己的内存使用快照,这时候其他线程对于内存使用的修改,线程就不知道了,只有再进行到 SafePoint 的时候,才会感知。

4.什么时候会进入Safepoint

当VM Thread需要做vm  操作时会让线程进入安全点,vm操作类型有很多,可以参考VM_OP_ENUM源码 vmOperations.hpp。下面是几种经常发生的进入Safepoint的情形:

(1)GC:由于需要每个线程的对象使用信息,以及回收一些对象,释放某些堆内存或者直接内存,所以需要 进入Safepoint来 Stop the world;

(2)定时进入 SafePoint:每经过-XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻 Stop the world 的任务执行,可以设置-XX:GuaranteedSafepointInterval=0关闭这个定时。

(3)由于 jstack,jmap 和 jstat 等命令,会导致 Stop the world:这种命令都需要采集堆栈信息,所以需要所有线程进入 Safepoint 并暂停。

(4)偏向锁取消:锁大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。但是高并发的情况下,偏向锁会经常失效,导致需要取消偏向锁,取消偏向锁的时候,需要 Stop the world,因为要获取每个线程使用锁的状态以及运行状态。

(5)Java Instrument 导致的 Agent 加载以及类的重定义:由于涉及到类重定义,需要修改栈上和这个类相关的信息,所以需要 Stop the world

(6)Java Code Cache相关:当发生 JIT 编译优化或者去优化,需要 OSR 或者 Bailout 或者清理代码缓存的时候,由于需要读取线程执行的方法以及改变线程执行的方法,所以需要 Stop the world

5.避免Safepoint副作用

Safepoint在一定程度上是可以理解成是为了让所有用户线程停顿(Stop The World)而设计的。STW对应用系统来说是一件很可怕的事情,JVM不论是在GC还是在其他的VM操作上都在努力避免STW和减少STW时间。

安全点最主要的副作用就是可能导致STW时间过长,应该极力避免这点副作用。

对第一个进入安全点的线程来说,STW是从它进入安全点开始的,如果有某个线程一直无法进入安全点就会导致进入安全点的时间一直处于等待状态,进而导致STW的时间过长。所以,应避免线程执行过长无法进入安全点的情况。

可数循环体内执行时间过长以及JIT优化导致无法进入安全点的问题是最常见的无法进入安全点的情况。在写大循环的时候可以把循环索引值数据类型定义成long。

在高并发应用中,偏向锁并不能带来性能提升,反而因为偏向锁取消带来了很多没必要的某些线程进入安全点 。所以建议关闭:-XX:-UseBiasedLocking

jstack,jmap 和 jstat 等命令,也会导致进入安全点。所以,生产环境应该关闭Thead dump的开关,避免dump时间过长导致应用STW时间过长。

参考文献:

[1] 《深入理解java虚拟机》

[2]http://psy-lob-saw.blogspot.com/2015/12/safepoints.html

[3]https://xie.infoq.cn/article/a80542aca7ad53efaaab1a27a

[4]https://zhuanlan.zhihu.com/p/161710652

文:Simon

线下活动推荐:

时间:5月14日(周日)14:00-18:00

主题:得物技术沙龙第17期-稳定生产专场

地点:上海市杨浦区黄兴路221号互联宝地C2栋5楼 培训教室

活动亮点:你知道得物App稳定性是怎么从99.91%提升到99.996%吗?《得物技术沙龙-稳定生产专题》将揭秘,不仅如此,我们还特别邀请了来自AWS(亚马逊中国)、SkyWalking 社区、Greptime(格睿科技)等知名企业的技术专家,将和大家分享他们在保障系统稳定性方面的经验和心得。

点击了解详情:叮~查收你的稳定生产得物技术沙龙邀请函

本文属得物技术原创,来源于:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/514006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

你真的了解Java类加载机制吗?

大家好&#xff0c;我是小米&#xff0c;一个喜欢分享技术的程序员。今天我来给大家简述一下Java类加载模型。 在Java中&#xff0c;类的加载过程是在程序运行时动态进行的。Java的类加载模型可以分为三个步骤&#xff1a;加载、连接和初始化。 类加载过程&#xff1a;加载 首…

Android面试指南:谈谈你对Flutter的理解

一、Flutter简介 Flutter是由Google开发的一种基于Dar编程语言的移动应用开发框架。可以帮助开发在构建高性能、美观、灵活的应用程序&#xff0c;从而实现跨平台开发&#xff0c;适用于与Android、ios、web、windows、macOS和linux等多个平台。 二、学习Flutter有什么优势 …

Java EE企业级应用开发(SSM)第11章

第11章SSM框架 一.预习笔记 1.准备jar包&#xff08;注意版本&#xff09; Spring一套包 Springmvc两个 Mybatis一个 Spring整合mybatis一个 Jstl一个用于jsp显示数据 Mysql一个用于访问数据库 Gson一个用于返回json数据 2.准备配置文件web.xml applicationContext.xml…

MySQL Client

MySQL客户端很多&#xff0c;自身携带的一些客户端工具也需要了解&#xff0c;方便快速测试。 MySQL Shell MySQL Shell Commands。 执行SQL语句时&#xff0c;必须切换到SQL模式。Shell指令较少&#xff0c;同时可以使用Python \py模式。 MySQL Shell所有的命令后面不需要加…

TCP通道和共享链路通道

推送SDK为了适应不同的场景和需求&#xff0c;对于一些对消息及时性、可靠性、自定义性要求高的应用&#xff0c;如即时通讯、社交、游戏等&#xff0c;可能更倾向于使用TCP通道&#xff0c;对于一些对消息节省流量、耗电量、兼容性要求高的应用&#xff0c;如新闻、天气、股票…

【软件工程】自动化测试保证卓越软件工程能力(3)

测试目标定义 对照目标系统&#xff0c;如下&#xff1a; 给出自动化测试平台目标如下&#xff1a; Case levelCase briefReport send toOVERALLUser 1 -> Process -> Customer 1BossLevel 1User 1 -> Process -> Customer 1 User 1 -> Process -> Custome…

AI自动写文章工具-ai文章智能生成器

随着人工智能技术的快速发展&#xff0c;越来越多的应用开始使用AI自动生成文章的功能&#xff0c;实现全自动、高质量和高效率的文章写作。本文将从全自动批量生成、没有错别字和标准语法、自动插入图片以及严格按照标准格式结构生成几个方面&#xff0c;展开对AI自动生成文章…

数据分析04——Pandas简介/Series对象/DataFrame对象

1、Pandas简介&#xff1a; Pandas是基于NumPy开发的数据分析三大剑客之一&#xff0c;Python数据分析的核心库提供快速、灵活、明确的数据结构Series对象&#xff1a;一维数组结构&#xff0c;由index和value构成DataFrame对象&#xff1a;二维数组结构&#xff0c;由index、…

MySQL基础(二十五)InnoDB数据存储结构

1 数据库的存储结构:页 索引结构给我们提供了高效的索引方式&#xff0c;不过索引信息以及数据记录都是保存在文件上的&#xff0c;确切说是存储在页结构中。另一方面&#xff0c;索引是在存储引擎中实现的&#xff0c;MySQL服务器上的存储引擎负责对表中数据的读取和写入工作…

在外Windows公网远程连接MongoDB数据库

文章目录 前言1. 安装数据库2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射2.3 测试随机公网地址远程连接 3. 配置固定TCP端口地址3.1 保留一个固定的公网TCP端口地址3.2 配置固定公网TCP端口地址3.3 测试固定地址公网远程访问 转载自远程内网穿透的文章&#xff1a;公网远…

友元函数,友元类,内部类及其之间的关系,匿名对象等

TIPS 当某一个类当中有自定义类型成员变量的时候&#xff0c;然后对该类的实例化对象调用函数的时候走初始化列表的时候&#xff0c;如果说要对自定义类型成员变量进行初始化列表初始化的时候&#xff0c;尽管那个自定义类型它的构造函数是没有参数的&#xff0c;但是此时括号…

数据剖析更灵活、更快捷,火山引擎 DataLeap 动态探查全面升级

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 近期&#xff0c;火山引擎 DataLeap 上线“动态探查”能力&#xff0c;为用户提供全局数据视角、完善的抽样策略&#xff0c;提高数据探查的灵活度以及响应速率。 …

【STL模版库】string类:模拟实现string类

一、成员变量 private:char *_str;size_t _size;size_t _capacity;public:static size_t npos -1; //编译报错&#xff0c;不能在类中初始化const static size_t npos -1; //[1]const char* c_str() const{ //[2]return _str;}size_t size() const{return _size;} size_t ca…

智慧水务云平台助力“十四五”水安全保障规划!

一、《“十四五”水安全保障规划》 水利部印发《“十四五”水安全保障规划》&#xff0c;规划中指出&#xff0c;“十四五”期间要抓好8个方面重点任务。 一是实施国家节水行动&#xff0c;强化水资源刚性约束。 二是加强重大水资源工程建设&#xff0c;提高水资源优化配置能…

Mongo执行计划explain分析

3.0+的explain有三种模式,分别是:queryPlanner、executionStats、allPlansExecution。现实开发中,常用的是executionStats模式。 1.使用方式 在查询语句后面加上explain("executionStats") db.user.find({"roleCodes":"xsbj","status&…

详细操作Selenium自动化测试之中的断言

Selenium常用的断言包括 页面属性断言&#xff1a;断言标题、url或页面源码中是否包含或不包含特定字符元素存在断言&#xff1a;断言指定元素存在图片及链接断言&#xff1a;断言图片正常显示、链接可以正常打开 页面属性断言 这是最常用的断言方式&#xff0c;可以用来断言…

TTL转HDMI 1.4,性能提升,pin to pin 芯片LT8618SXB

1. 描述 LT8618SX 是 Lontium 的低功耗版本 HDMI 发射器&#xff0c;其基于 ClearEdgeTM 技术。它支持 24 位色深 HDMI 1.4&#xff08;高清多媒体接口&#xff09;规范。它们与 Lontium 的第一代 HDMI 发射器 LT8618EX 完全向后兼容。 LT8618SX 是一款高性能、低功耗器件…

干货分享!9大Python常用技巧!

介绍 Python 炫酷功能&#xff08;例如&#xff0c;变量解包&#xff0c;偏函数&#xff0c;枚举可迭代对象等&#xff09;的文章层出不穷。但是还有很多 Python 的编程小技巧鲜被提及。因此&#xff0c;本文会试着介绍一些其它文章没有提到的小技巧&#xff0c;这些小技巧也是…

csgo搬砖项目,时间自由,项目包下车,包落地

Steam是一款全球较大的综合性数字游戏软件发行平台。steam同时在线飙到3300万&#xff01;超越你说熟悉的王者&#xff0c;吃鸡&#xff01;用户多&#xff0c;竞争者少&#xff0c;连我自己都没想到&#xff0c;有一天我居然可以靠着steam游戏搬砖来赚钱养活自己。 实话实说&a…

计算机基础--->数据结构(1)【图的存储和遍历】

文章目录 图图的存储图的搜索&#xff08;无向无权图&#xff09;代码演示 图 图中包含 顶点、边、度&#xff0c;无向图&#xff0c;有向图&#xff0c;无权图&#xff0c;带权图&#xff0c;其中 度表示一个顶点包含多少条边&#xff0c;有出度和入度。 图的存储 邻接矩阵 代…