JVM、DVM(Dalvik VM)和ART虚拟机的区别

                                   DVM vs JVM
1、共同点:

  • 都是解释执行
  • 都是每个 OS 进程运行一个 VM,并运行一个单独的程序
  • 在较新版本中(Froyo / Sun JDK 1.5)都实现了相当程度的 JIT compiler(即时编译) 用于提速。JIT(Just In Time,即时编译技术)对于热代码(使用频率高的字节码)直接转换成汇编代码;

2、不同点:

(1)dvm执行的是.dex格式文件,jvm执行的是.class文件。class文件和dex之间可以相互转换具体流程如下图,多个class文件转变成一个dex文件会引发一些问题,具体如下:

  • 方法数受限:多个class文件变成一个dex文件所带来的问题就是方法数超过65535时报错,由此引出MultiDex技术,具体资料同学可以google下。
  • class文件去冗余:class文件存在很多的冗余信息,dex工具会去除冗余信息(多个class中的字符串常量合并为一个,比如对于Ljava/lang/Oject字符常量,每个class文件基本都有该字符常量,存在很大的冗余),并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。

(2)许多GC实现都是在对象开头的地方留一小块空间给GC标记用。Dalvik VM则不同,在进行GC的时候会单独申请一块空间,以位图的形式来保存整个堆上的对象的标记,在GC结束后就释放该空间。 (关于这一点后面的Dalvik垃圾回收机制还会更加深入的介绍)
(3)dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。这类的不同是最要命的,因为它将导致一系列的问题,具体如下:

  • dvm速度快!寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。JAVA虚拟机基于栈结构,程序在运行时虚拟机需要频繁的从栈上读取写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费很多CPU时间。
  • 指令数小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作数的地址;jvm基于栈,它的指令是零地址,指令的操作数对象默认是操作数栈中的几个位置。这样带来的结果就是dvm的指令数相对于jvm的指令数会小很多,jvm需要多条指令而dvm可能只需要一条指令。
  • jvm基于栈带来的好处是可以做的足够简单,真正的跨平台,保证在低硬件条件下能够正常运行。而dvm操作平台一般指明是ARM系统,所以采取的策略有所不同。需要注意的是dvm基于寄存器,但是这也是个映射关系,如果硬件没有足够的寄存器,dvm将多出来的寄存器映射到内存中。

Dalvik虚拟机

  谈到垃圾回收自然而然的想到了堆,Dalvik的堆结构相对于JVM的堆结构有所区别,这主要体现在Dalvik将堆分成了Active堆和Zygote堆,这里大家只要知道Zygote堆是Zygote进程在启动的时候预加载的类、资源和对象(具体gygote进程预加载了哪些类,详见文末的附录),除此之外的所有对象都是存储在Active堆中的。对于为何要将堆分成gygote和Active堆,这主要是因为Android通过fork方法创建到一个新的gygote进程,为了尽可能的避免父进程和子进程之间的数据拷贝,fork方法使用写时拷贝技术,写时拷贝技术简单讲就是fork的时候不立即拷贝父进程的数据到子进程中,而是在子进程或者父进程对内存进行写操作时是才对内存内容进行复制,Dalvik的gygote堆存放的预加载的类都是Android核心类和java运行时库,这部分内容很少被修改,大多数情况父进程和子进程共享这块内存区域。通常垃圾回收重点对Active堆进行回收操作,Dalvik为了对堆进行更好的管理创建了一个Card Table、两个Heap Bitmap和一个Mark Stack数据结构。

1、Dalvik创建对象流程

  当Dalvik虚拟机的解释器遇到一个new指令时,它就会调用函数Object* dvmAllocObject(ClassObject* clazz, int flags)。期间完成的动作有( 注意:Java堆分配内存前后,要对Java堆进行加锁和解锁,避免多个线程同时对Java堆进行操作。下面所说的堆指的是Active堆):

  • 调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
  • 执行一次GC, GC执行完毕后,再次调用函数dvmHeapSourceAlloc在Java堆上分配指定大小的内存,成功则返回,否则下一步。
  • 首先将堆的当前大小设置为Dalvik虚拟机启动时指定的Java堆最大值,然后进行内存分配,成功返回失败下一步。这里调用的函数是 dvmHeapSourceAllocAndGrow
  • 调用函数gcForMalloc来执行GC,这里的GC和第二步的GC,区别在于这里回收软引用对象引用的对象,如果还是失败抛出OOM异常。这里调用的函数是dvmHeapSourceAllocAndGrow

2、Dalvik回收对象流程

  Dalvik的垃圾回收策略默认是标记擦除回收算法,即Mark和Sweep两个阶段。标记与清理的回收算法一个明显的区别就是会产生大量的垃圾碎片,因此程序中应该避免有大量不连续小碎片的时候分配大对象,同时为了解决碎片问题,Dalvik虚拟机通过使用dlmalloc技术解决,关于后者读者另行google。下面我们对Mark阶段进行简单介绍。
Mark阶段使用了两个Bitmap来描述堆的对象,一个称为Live Bitmap,另一个称为Mark Bitmap。Live Bitmap用来标记上一次GC时被引用的对象,也就是没有被回收的对象,而Mark Bitmap用来标记当前GC有被引用的对象。当Live Bitmap被标记为1,但是在Mark Bitmap中标记为0的对象表明该对象需要被回收。此外在Mark阶段往往要求其它线程处于停止状态,因此Mark又分为并行和串行两种方式,并行的Mark分为两个阶段:

1)、只标记gc_root对象,即在GC开始的瞬间被全局变量、栈变量、寄存器等所引用的对象,该阶段不允许垃圾回收线程之外的线程处于运行状态。

2)、有条件的并行运行其它线程,使用Card Table记录在垃圾收集过程中对象的引用情况。整个Mark 阶段都是通过Mark Stack来实现递归检查被引用的对象,即在当前GC中存活的对象。标记过程类似用一个栈把第一阶段得到的gc_root放入栈底,然后依次遍历它们所引用的对象(通过出栈入栈),即用栈数据结构实现了对每个gc_root的递归。
Dalvik的GC类型共有四种:

  • GC_CONCURRENT: 表示是在已分配内存达到一定量之后触发的GC。
  • GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。
  • GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。
  • GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。

其中GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三种类型的GC都是在分配对象的过程触发的。垃圾回收具体都是通过调用函数void dvmCollectGarbageInternal(const GcSpec* spec) 来执行垃圾回收,该函数的参数GcSpec结构体定义见本文的附录。对于函数dvmCollectGarbageInternal的内部逻辑,即垃圾回收流程,根据垃圾回收线程和工作线程的关系分为并行GC和非并行GC。前者在回收阶段有选择性的停止当前工作线程,后者在垃圾回收阶段停止所有工作线程。但是并行GC需要多执行一次标记根集对象以及递归标记那些在GC过程被访问了的对象的操作,意味着并行GC需要花费更多的CPU资源。dvmCollectGarbageInternal函数的内部逻辑如下:(本文末尾的附录中给出了一个对应的流程图)
(1)调用函数dvmSuspendAllThreads挂起所有的线程,以免它们干扰GC。
    这里如何挂起其它线程呢?其实就是每个线程在运行过程中会周期性的检测自身的一个标志位,通过这个标志位我们可以告知线程停止运行。
(2)调用函数dvmHeapBeginMarkStep初始化Mark Stack,并且设定好GC范围。
    Mark Stack其实是一个object指针数组
(3)调用函数dvmHeapMarkRootSet标记根集对象。
Mark的第一阶段,主要分为两大类:

  • Dalvik虚拟机内部使用的全局对象(维护在一个hash表中);
  • 应用程序正在使用的对象(维护在一个调用栈中)

(4)调用函数dvmClearCardTable清理Card Table。(只在并行gc发生)
    Card Table记录记录在Zygote堆上分配的对象在垃圾收集执行过程中对在Active堆上分配的对象的引用。
(5)调用函数dvmUnlock解锁堆。这个是针对调用函数dvmCollectGarbageInternal执行GC前的堆锁定操作。(只在并行gc发生)
(6)调用函数dvmResumeAllThreads唤醒第1步挂起的线程。(只在并行gc发生)
    此时非gc线程可以开始工作,这部分线程对堆的操作记录在CardTable上面,gc则进行Mark的第二阶段
(7) 调用函数dvmHeapScanMarkedObjects从第3步获得的根集对象开始,归递标记所有被根集对象引用的对象。
(8)调用函数dvmLockHeap重新锁定堆。这个是针对前面第5步的操作。(只在并行gc发生)
(9)调用函数dvmSuspendAllThreads重新挂起所有的线程。这个是针对前面第6步的操作。(只在并行gc发生)
这里需要再次停止工作线程,用来解决前面线程对堆的少部分的操作,这个过程很快。
(10)调用函数dvmHeapReMarkRootSet更新根集对象。因为有可能在第4步到第6步的执行过程中,有线程创建了新的根集对象。(只在并行gc发生)
(11)调用函数dvmHeapReScanMarkedObjects归递标记那些在第4步到第6步的执行过程中被修改的对象。这些对象记录在Card Table中。(只在并行gc发生)
(12)调用函数dvmHeapProcessReferences处理那些被软引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的对象,以及重写了finalize方法的对象。这些对象都是需要特殊处理的。
(13)调用函数dvmHeapSweepSystemWeaks回收系统内部使用的那些被弱引用引用的对象。
(14)调用函数dvmHeapSourceSwapBitmaps交换Live Bitmap和Mark Bitmap。

  • 执行了前面的13步之后,所有还被引用的对象在Mark Bitmap中的bit都被设置为1。
  • Live Bitmap记录的是当前GC前还被引用着的对象。
  • 通过交换这两个Bitmap,就可以使得当前GC完成之后,使得Live Bitmap记录的是下次GC前还被引用着的对象。

(15)调用函数dvmUnlock解锁堆。这个是针对前面第8步的操作。(只在并行gc发生)
(16)调用函数dvmResumeAllThreads唤醒第9步挂起的线程。(只在并行gc发生)
(17)调用函数dvmHeapSweepUnmarkedObjects回收那些没有被引用的对象。没有被引用的对象就是那些在执行第14步之前,在Live Bitmap中的bit设置为1,但是在Mark Bitmap中的bit设置为0的对象。
(18)调用函数dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。这个是针对前面第2步的操作。
(19)调用函数dvmLockHeap重新锁定堆。这个是针对前面第15步的操作。(只在并行gc发生)
(20)调用函数dvmHeapSourceGrowForUtilization根据设置的堆目标利用率调整堆的大小。
(21)调用函数dvmBroadcastCond唤醒那些等待GC执行完成再在堆上分配对象的线程。(只在并行gc发生)
(22)调用函数dvmResumeAllThreads唤醒第1步挂起的线程。(只在非并行gc发生)
(23)调用函数dvmEnqueueClearedReferences将那些目标对象已经被回收了的引用对象增加到相应的Java队列中去,以便应用程序可以知道哪些引用引用的对象已经被回收了。
总结:
通过上面的流程分析,我们知道了并行和串行gc的区别在于:

  • 并行gc会在mark第二阶段将非gc线程唤醒;当mark的第二阶段完成之后,再次停止非gc线程;利用cardtable的信息再次进行一个mark操作,此时的mark操作比第一个mark操作要快得多。
  • 并行gc会在sweep阶段将非gc线程唤醒。
  • 串行gc会在垃圾回收开始就暂停所有非gc线程,知道垃圾回收结束。
  • 并行gc涉及到两次的mark操作,消耗cpu时间。

ART虚拟机

  在Android5.0中,ART取代了Dalvik虚拟机(安卓在4.4中发布了ART)。ART虚拟机直接执行本地机器码;而Dalvik虚拟机运行的是DEX字节码需要通过解释器执行。安卓运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者重新将自己的应用直接编译成目标机器码,应用程序仍然是一个包含dex字节码的apk文件,这主要得益于AOT技术,AOT(Ahead Of Time)是相对JIT(Just In Time)而言的;也就是在APK运行之前,就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了。ART应用安装的时候把dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。去除了运行时的解释执行,效率更高,启动更快。
  ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map(Live 和 Mark Object Map),以及三个Object Stack (Live、Mark、Allocation Stack)。具体结构图参考附录。
  Image Space和Zygote Space之间,隔着一段用来映射system@framework@boot.art@classes.oat文件的内存。system@framework@boot.art@classes.oat是一个OAT文件,它是由在系统启动类路径中的所有DEX文件翻译得到的,Image Space映射的是一个system@framework@boot.art@classes.dex文件,这个文件保存的是在生成system@framework@boot.art@classes.oat这个OAT文件的时候需要预加载的类对象,这些需要预加载的类由/system/framework/framework.jar文件里面的preloaded-classes文件指定。以后只要系统启动类路径中的DEX文件不发生变化(即不发生更新升级),那么以后每次系统启动只需要将文件system@framework@boot.art@classes.dex直接映射到内存即可。
  由于system@framework@boot.art@classes.dex文件保存的是一些预先创建的对象,并且这些对象之间可能会互相引用,因此我们必须保证system@framework@boot.art@classes.dex文件每次加载到内存的地址都是固定的。这个固定的地址保存在system@framework@boot.art@classes.dex文件开头的一个Image Header中。此外,system@framework@boot.art@classes.dex文件也依赖于system@framework@boot.art@classes.oat文件,因此也会将后者固定加载到Image Space的末尾。
  Image Space是不能分配新对象的。Image Space和Zygote Space在Zygote进程和应用程序进程之间进行共享,而Allocation Space是每个进程都独立地拥有一份。

ART的运行原理:

1、在Android系统启动过程中创建的Zygote进程利用ART运行时导出的Java虚拟机接口创建ART虚拟机。
2、APK在安装的时候,打包在里面的classes.dex文件会被工具dex2oat翻译成本地机器指令,最终得到一个ELF格式的oat文件。
3、APK运行时,上述生成的oat文件会被加载到内存中,并且ART虚拟机可以通过里面的oatdata和oatexec段找到任意一个类的方法对应的本地机器指令来执行。

  • oat文件中的oatdata包含用来生成本地机器指令的dex文件内容
  • oat文件中的oatexec包含有生成的本地机器指令。

注意:
  这里将DEX文件中的类和方法称之为DEX类和DEX方法,将OTA中的类和方法称之为OTA类和OTA方法,ART运行时将类和方法称之为Class和ArtMethod。
  ART中一个已经加载的Class对象包含了一系列的ArtField对象和ArtMethod对象,其中,ArtField对象用来描述成员变量信息,而ArtMethod用来描述成员函数信息。对于每一个ArtMethod对象,它都有一个解释器入口点和一个本地机器指令入口点。

ART找到一个类和方法的流程:

  在DEX文件中找到目标DEX类的编号,并且以这个编号为索引,在OAT文件中找到对应的OAT类。
  在DEX文件中找到目标DEX方法的编号,并且以这个编号为索引,在上一步找到的OAT类中找到对应的OAT方法。
  使用上一步找到的OAT方法的成员变量begin_和code_offset_,计算出该方法对应的本地机器指令。
  上面的流程对应给出了流程图,具体内容参考附录。

ART运行时对象的创建过程:

  可以分配内存的Space有三个:Zygote Space、Allocation Space和Large Object Space。不过,Zygote Space在还没有划分出Allocation Space之前,就在Zygote Space上分配,而当Zygote Space划分出Allocation Space之后,就只能在Allocation Space上分配。因此实际上应用运行的时候能够分配内存也就Allocation 和 Large Object Space两个。
而分配的对象究竟是存入上面的哪个Space呢?满足如下三个条件的内存,存入Large Object Space:

1)Zygote Space已经划分除了Allocation Space

2)分配对象是原子类型数组,如int[] byte[] boolean[],

3)分配的内存大小大于一定的门限值。
  对于分配对象时内存不足的问题,是通过垃圾回收和在允许范围内增长堆大小解决的。由于垃圾回收会影响程序,因此ART运行时采用力度从小到大的进垃圾回收策略。一旦力度小的垃圾回收执行过后能满足分配要求,那就不需要进行力度大的垃圾回收了。这跟dalvik虚拟机的对象分配策略也是类似的。

ART垃圾回收流程:

1、并行GC流程图如下:

  • 调用子类实现的成员函数InitializePhase执行GC初始化阶段。
  • 获取用于访问Java堆的锁。
  • 调用子类实现的成员函数MarkingPhase执行GC并行标记阶段。
  • 释放用于访问Java堆的锁。
  • 挂起所有的ART运行时线程。
  • 调用子类实现的成员函数HandleDirtyObjectsPhase处理在GC并行标记阶段被修改的对象。
  • 恢复第4步挂起的ART运行时线程。
  • 重复第5到第7步,直到所有在GC并行阶段被修改的对象都处理完成。
  • 获取用于访问Java堆的锁。
  • 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
  • 释放用于访问Java堆的锁。
  • 调用子类实现的成员函数FinishPhase执行GC结束阶段

2、非并行GC流程图如下:

调用子类实现的成员函数InitializePhase执行GC初始化阶段。挂起所有的ART运行时线程。

  • 调用子类实现的成员函数MarkingPhase执行GC标记阶段。
  • 调用子类实现的成员函数ReclaimPhase执行GC回收阶段。
  • 恢复第2步挂起的ART运行时线程。
  • 调用子类实现的成员函数FinishPhase执行GC结束阶段

通过两者的对比可以得出如下结论(与Dalvik大同小异):

  • 非并行GC在垃圾回收的整个过程中暂停了所有非gc线程
  • 并行GC在一开始只是对堆进行加锁,对于那些暂时并不会在堆中分配的内存的线程不起作用,它们依然可以运行,但是会造成对象的引用发生变化,但是这段时间的引用发生的变化被记录了下来。之后系统会停止所有线程,对上面记录的数据进行处理,然后唤起所有线程,系统进入垃圾回收阶段。

附录:

Gygote堆预加载的类有:

Dalvik对应的GC类型结构体定义如下:

1

2

3

4

5

6

7

8

9

struct GcSpec {

/* If true, only the application heap is threatened. */

bool isPartial;

/* If true, the trace is run concurrently with the mutator. */

bool isConcurrent;

/* Toggles for the soft reference clearing policy. */

bool doPreserve;

/* A name for this garbage collection mode. */

const char *reason;

下图就是根据Dalvik回收阶段调用的dvmCollectGarbageInternal()函数所得到的流程图

图.1、dvmCollectGarbageInternal函数针对并行和串行两种gc的流程图

  

下图是ART的堆结构图

图.2、ART的堆结构

Mod Union Table对象

(1)一个用来记录在GC并行阶段在Image Space上分配的对象对在Zygote Space和Allocation Space上分配的对象的引用。

(2)另一个用来记录在GC并行阶段在Zygote Space上分配的对象对在Allocation Space上分配的对象的引用。

  • Allocation Stack:用来记录上一次GC后分配的对象,用来实现类型为Sticky的Mark Sweep Collector。
  • Live Stack:配合allocation_stack_一起使用,用来实现类型为Sticky的Mark Sweep Collector。
  • Mark Stack:用来在GC过程中实现递归对象标记

ART找到一个类和方法的流程:

图.3、在OAT文件中查找类方法的本地机器指令的过程

  我们从左往右来看图.3。首先是根据类签名信息从包含在OAT文件里面的DEX文件中查找目标Class的编号,然后再根据这个编号找到在OAT文件中找到对应的OatClass。接下来再根据方法签名从包含在OAT文件里面的DEX文件中查找目标方法的编号,然后再根据这个编号在前面找到的OatClass中找到对应的OatMethod。有了这个OatMethod之后,我们就根据它的成员变量begin_和code_offset_找到目标类方法的本地机器指令了。其中,从DEX文件中根据签名找到类和方法的编号要求对DEX文件进行解析,这就需要利用Dalvik虚拟机的知识了。

————————————————
版权声明:本文为CSDN博主「evan_man」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/evan_man/article/details/52414390

JVM、DVM(Dalvik VM)和ART虚拟机的区别相关推荐

  1. dalvik模式和art模式的区别

    dalvik模式和art模式的区别: dalvik是执行的时候编译+运行,安装比较快,开启应用比较慢,应用占用空间小 ART是安装的时候就编译好了,执行的时候直接就可以运行的,安装慢,开启应用快,占用 ...

  2. 【JVM笔记】JVM发展历程—VM(Java虚拟机)介绍

    目录 Sun Classic VM Exact VM HotSpot VM JRockit VM J9 VM KVM.CDC.CLDC Azul VM Liquid VM Apache Harmony ...

  3. Android运行环境Dalvik模式和ART模式的区别对比

    Android系统是以Linux系统为底层构建的,开源的Android系统需要配置到不同硬件配置的设备上,为了降低应用的开发难度,在Linux底层之上构筑了一个名为"Dalvik" ...

  4. Java虚拟机和Dalvik(android)虚拟机的区别

    首先你必须对java虚拟机中常用的一款有深入的了解,比如Hotspot. 然后你再了解下android系统和Dalvik. Dalvik 与 传统jvm的差别还是很大的,总结来说,我个人把其归纳为三大 ...

  5. Android运行环境Dalvik模式和ART模式的区别

    博主很懒,只留下一个网址就走了:https://blog.csdn.net/lihappyangel/article/details/71212082

  6. JVM(JAVA虚拟机)、DVM(Dalvik虚拟机)和ART虚拟机

    一.什么是DVM,和JVM有什么不同? JVM是Java Virtual Machine,而DVM就是Dalvik Virtual Machine,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进 ...

  7. 面试关于jvm、dvm和art虚拟机区别

    一.Dalvik与JVM的区别 1.首先最主要的区别,Dalvik 基于寄存器,而 JVM 基于栈.那么什么是基于寄存器什么是基于栈呢? 有关寄存器可以参照百度百科的内容,下面是一段摘抄: 寄存器是中 ...

  8. Android虚拟机-Dalvik VM架构

    Dalvik, VM会监视所有的程序APK,并且创建依赖关系熟,为每个程序优化代码并存储在Dalvik缓存中.Dalvik VM第一次加载后会生成Cache文件,加速加载,Dalvik Vm的第一次加 ...

  9. Android安全与逆向之Java虚拟机和Dalvik虚拟机的区别

    Google于2007年底正式发布了Android SDK, 作为 Android系统的重要特性,Dalvik虚拟机也第一次进入了人们的视野.它对内存的高效使用,和在低速CPU上表现出的高性能,确实令 ...

最新文章

  1. VC++动态链接库(DLL)编程(四)――MFC扩展 DLL
  2. 用createinstallmedia创建可恢复的OSX安装DMG
  3. JMeter学习笔记--JMeter监听器
  4. MATLAB 图像的傅里叶变换
  5. 基于zookeeper实现系统配置中心
  6. Centos下安装mysql(二进制版)
  7. python神经网络教程16_Python深度学习之神经网络视频
  8. 内核并发控制---自旋锁(来自网易)
  9. verilog之状态机的结构
  10. mongodb mysql 事务_让你的mongodb支持事务---升级到mongodb 4.0
  11. Ubuntu系统安装JDK教程
  12. SQL Server 之 修改时不允许保存更改
  13. 计算机 无法自检,电脑开机无法完成自检的原因分析
  14. 580集photoshop顶尖视频教程送给你,设计总监手把手带你学ps!
  15. JavaSrcipt学习(学习打卡Day8)
  16. 【Python】迭代法求解非线性方程及方程组
  17. NDT(正态分布变换)算法学习
  18. 安卓强制横屏的坑!正确设置横屏的姿势!
  19. Metal 框架之使用 Metal Debugger 查看 GPU 工作负载
  20. 【Excel】巧用数组——多项目跨越多个调整期的利息计算器

热门文章

  1. JZOJ.5236【NOIP2017模拟8.7】利普希茨
  2. 明日之后最新服务器开服时间,明日之后什么时候开服 新服开区详解
  3. 【2020】win10java(jdk安装)环境变量配置和相关问题解决
  4. 一步步教你使用云端服务器yeelink远程监控
  5. 字节的按位逆序 Reverse Bits
  6. STM32中的IDR和ODR寄存器
  7. Python_oldboy_自动化运维之路_面向对象2(十)
  8. 微信二次开发sdk非ipad/android协议(很好用)
  9. 红黑树解决了什么问题
  10. Apex英雄手游国服何时上线 Apex英雄手游和端游互通吗