一、运行时数据区与线程概述

1 运行时数据区的结构


  其中方法区和堆是随着虚拟机的创建而创建摧毁而摧毁,为各个线程所共用。而程序计数器(PC)、本地方法栈(NMS)、虚拟机栈(VMS)则是随着某个线程的创建而创建摧毁而摧毁。

  垃圾回收大部分是发生在堆区,但也有一些发生在 方法区/元空间/永久代 中。另外对于JIT编译缓冲,有的书中说是在方法区中,有的书上说是独立于方法区存在,众说纷纭。

2 一些比较重要的守护线程

  要知道执行Java程序的时候,并不是只有main方法这一个线程。还有很多很多虚拟机自动开启的守护线程。比较重要的有以下几个:

  ① 虚拟机线程:负责线程栈收集,线程栈挂起,以及偏向锁的撤销。

  ② 周期任务线程:是时间周期事件的体现(如中断),用于周期性操作的调度执行。

  ③ GC线程:顾名思义,负责垃圾回收。

  ④ 编译线程:负责JIT编译,将常用字节码编译为机器指令并缓存。

  ⑤ 信号调度线程:负责接受信号并发送给JVM,内部通过调用适当方法进行处理。

二、虚拟机栈

1 栈的运行原理

  一个时间点上,只会有一个正在活动得到栈帧。该栈帧叫做当前栈帧,与之对应的方法叫做当前方法,再与之对应的类,叫做当前类。就有点像栈顶,懂吧。

  栈帧的内部主要是局部变量表、操作数栈、其次还有动态链接、方法返回地址、和一些附加信息(有时会忽略附加信息)。有时还会将动态链接、方法返回地址、附加信息称为帧数据区。

  不同的线程之间的方法是无法共享的,也就是他们的栈帧是无法共享的。

  当前栈帧无论是使用return或是抛出异常,都会弹栈。

2 栈帧的内部结构 — 局部变量表

  局部变量表是一个数字数组。局部变量表中最基本的存储单元是slot(变量槽)。32位以内的数据,每个数据指针一个变量槽,如byte short char boolean。而long和double则要转换成两个变量槽。

  局部变量表的大小在编译期间就定下来了。方法运行期间不会修改。我们随便写一个方法:

public static void main(String[] args) {Test test = new Test();int n = 16;
}


  这里面起始pc就是字节码指令的相对地址。长度则是该变量可以起作用的范围,声明完可不就能起作用了。所以同一个方法内,起始PC和长度之和大部分情况下是一个常量。

  另外,成员方法的局部变量表中必然是有一个this的,就在局部变量表的第一个位置上。这也算是解释了static方法为什么不能使用this关键字。声明一个没有任何内容的成员方法func()并查看局部变量表:

  顺带插一嘴其他的。

  方法名、描述符与访问标志。

  异常表:

  杂项:操作数最大深度、局部变量最大槽数、字节码长度等。

  行号表,即class中字节码指令相对地址和java源代码中的行号的对应关系。

3 共用变量槽

  这是我认为一个很有趣的地方,先看如下代码:

public void func() {int a = 0;{int b = 0;b = a + 1;}int c = a + 1;
}

  查看他的方法属性和局部变量表:


  我们发现虽然有四个局部变量,但局部变量表长度却是3,这是为什么的,我们发现变量b从 pc = 4 时进入局部变量表,在 pc = 8 时退出。而原本b的位置后面被c重复利用了(他俩index都是2),所以才会出现这种状况。

  为了防止出现占内存溢出,我们在开发中也可以通过妥善利用大括号来缩小栈帧大小。

4 java中的变量

Java变量分类有两种分法:

分法一:按照数据类型分

  • 基本数据类型
  • 引用数据类型

分法二:按照在类中的位置分:

  • 成员变量:在使用前,都经过默认初始化复制

    • 类变量(静态变量,被static修饰的成员变量):linking的prepare阶段给变量默认赋值—>initial阶段:给变量显示赋值即静态代码块赋值
    • 实例变量(没有被static修饰的成员变量):随着对象的创建,会在堆空间分配实例变量空间,进行默认赋值
  • 局部变量:使用前必须显示赋值,否则编译不通过。

5 栈帧的内部结构 — 操作数栈

  就和数据结构课设的表达式求值里面的操作数栈是一个审儿的。

  比如下面这个例子:

public void func(String[] args) {int a = 20;int b = 30;int c = a + b;
}

  对应字节码:

 0 bipush 20 // 将20压入操作数栈2 istore_1      // 将操作数栈栈顶int型元素弹栈弹入局部变量一(局部变量表中索引为一)3 bipush 30  // 将30压入操作数栈5 istore_2      // 将操作数栈栈顶int型元素弹栈弹入局部变量二6 iload_1      // 将int型的本地变量一压入操作数栈栈顶7 iload_2     // 将int型的本地变量二压入操作数栈栈顶8 iadd            // 将栈顶两个int型数弹出并相加,然后将结果压入栈顶9 istore_3       // 将操作数栈栈顶int型元素存入局部变量三
10 return       // 返回

  上面没有出现0号局部变量,因为0号是this。

  我们发现操作数栈分别在pc = 0、pc = 3、 pc = 8时入栈。又分别在pc = 2、pc = 5弹栈,在pc = 8时弹两次栈。用伪代码表示即:

0:  operandStack.push(20);
2:  localVariableTable[1] = operandStack.pop();
3:  operandStack.push(30);
5:  localVariableTable[2] = operandStack.pop();
6:  operandStack.push(localVariableTable[1]);
7:  operandStack.push(localVariableTable[2]);
8:  operandStack.push(operandStack.pop() + operandStack.pop());
9:  localVariableTable[3] = operandStack.pop();
10: return;

  那么我们可以推断出来操作数栈的最大深度就是2了。

  如果方法有返回值。如下:

public int func() {int a = 20;int b = 30;int c = a + b;return c;
}

  那就得把返回值存入栈顶,如下:

 0 bipush 202 istore_13 bipush 305 istore_26 iload_17 iload_28 iadd9 istore_3
10 iload_3
11 ireturn

  用伪代码表示为:

0:  operandStack.push(20);
2:  localVariableTable[1] = operandStack.pop();
3:  operandStack.push(30);
5:  localVariableTable[2] = operandStack.pop();
6:  operandStack.push(localVariableTable[1]);
7:  operandStack.push(localVariableTable[2]);
8:  operandStack.push(operandStack.pop() + operandStack.pop());
9:  localVariableTable[3] = operandStack.pop();
10: operandStack.push(localVariableTable[3]);
11: return operandStack.pop();

  常说java虚拟机的执行引擎是基于栈的执行引擎,其中的栈就是操作数栈。

  和局部变量表一样,操作数栈的深度在编译时就确定好了。

  另外频繁使用栈,访问内存,这无疑是很低效率的。因而诞生了栈顶缓存技术,把栈顶元素缓存到通用寄存器中,大幅度加快速度。该技术并未大量应用。

6 栈帧的内部结构 — 动态链接

  许多字节码指令执行时都需要访问到常量池。因而每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。而这些引用就叫动态链接。

  在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

  举个例子:

package com.spd.jvm;public class Test {private int num;public void func1() {func2();}private void func2() {num++;}}

  对应的字节码指令为:

func1():0 aload_0            // 将引用类型的本地变量0(即this)压入操作数栈栈顶1 invokespecial #2   // 调用超类构造方法、实例初始化方法、私有方法。此处为私有方法,引用类型#2指向常量池中的func2()4 return            // 返回空func2():0 aload_0         // 将引用类型的本地变量0(即this)压入操作数栈栈顶1 dup                // 复制操作数栈栈顶数值并将复制值压入栈顶2 getfield #3     // 获得指定的静态域,并压入栈顶。此处#3为运行时常量池中的num5 iconst_16 iadd7 putfield #3      // 将栈顶元素弹出到指定静态域。10 return

  这段字节码中出现了例如#2 #3 这样的东西,他们就是指向常量池的引用。或者叫动态链接。

7 方法的调用

  如果某方法调用方法时,如果编译阶段就能获知调用什么,那就叫做静态链接。如果只有运行时才能知道调用什么,那就叫动态链接。

  原方法与被调用方法之间存在绑定关系。如果是静态链接,那对应的就是早期绑定,如果是动态绑定,那就是使用晚期绑定了。

  另外java中也有虚方法和非虚方法的概念。早期绑定就是虚方法。万起绑定就是非虚方法。虚属性不能自己定义,是编译器决定的。

  非虚方法有:静态方法、私有方法、final方法、实例构造器方法、父类方法。其他都是虚方法。也就是能体现多态的都是虚方法。

  虚拟机中提供的方法调用指令由以下五种:

  • 普通调用方法:

    • ① invokestatic:调用静态方法。
    • ② invokespecial:调用<init>()方法、私有及父类方法、解析阶段唯一确定方法版本。
    • ③ invokevirtual:调用所有虚方法
    • ④ invokeinterface:调用接口方法
  • 动态调用方法:
    • ⑤ invokedynamic:动态解析出需要调用的方法、然后执行

  其中① ②都是非虚方法,其他都是虚方法。但也有个例外:③也可能调用final方法,这种情况是非虚方法。

8 JAVA语言的动态特性

  invokedynamic是java为了实现“动态类型语言”支持而做出的改进。

  但他只是为JAVA语言提供了一些动态语言的特性。java在本质上还是属于静态语言。

  我们看下面这个例子:

package com.spd.jvm;public class Test {public static void lambda(Func func) {System.out.println(func.func());}public static void main(String[] args) {Func func = () -> true;lambda(func);lambda(() -> 64);}}@FunctionalInterface
interface Func {Object func();
}

  对应字节码:

lambda(Func func):0 getstatic #2         // 获得指定静态域,#2即是3 aload_0             // 将引用类型从局部变量表中存入操作数栈4 invokeinterface #3   // 调用接口方法,#3即是func()9 invokevirtual #4     // 调用实例方法,#4即是println12 returnmain(String args[]):0 invokedynamic #5     // 调用动态方法,#5即是lambda$main$0():5 astore_1             // 将栈顶引用变量弹入局部变量表6 aload_1              // 将引用类型从局部变量表中存入操作数栈7 invokestatic #6      // 执行静态方法,#6就是lambda10 invokedynamic #7      // 调用动态方法,#5即是lambda$main$1():15 invokestatic #6     // 执行静态方法,#6就是lambda18 returnlambda$main$0():0 iconst_11 invokestatic #9 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>4 areturnlambda$main$1():0 bipush 642 invokestatic #8 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>5 areturn

  这些特性的出现,有利于Java平台的动态语言解释器。

9 方法重写的本质

  1.找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

  2.如果在过程结束;如果不通类型c中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java.lang.IllegalAccessError异常。即是无访问或修改权限。

  3.否则,按照继承关系从下往上依次对(的各个父类进行第⒉步的搜索和验证过程。

  4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

10 虚方法表

  为了提高性能,jvm会在类的方法区中创建一个虚方法表来实现。使用索引表来代替查找。表中存着各个方法的实际入口。

  虚方法表会在类加载的链接阶段被创建并初始化。

  举个例子:

class Father {@Overridepublic String toString() {return "I am son/father.";}public void method() {System.out.println("Father.method");}}class Son extends Father {@Overridepublic void method() {System.out.println("Son.method");}
}

  父类继承Object并重写toString,而子类继承父类,重写了method,未重写toString。那么在虚方法表中,Son的hashcode、clone等方法,由于父类未重写,所以直接指向Object的hashcode、clone等。而toString父类重写了,直接继承自Father,故这个方法指向Father

11 栈帧的内部结构 — 方法返回地址

  方法返回地址就是用来存储pc寄存器的值。

  方法执行完而退出时。就得回到调用他的那个方法那里继续往下执行。而调用它的方法的pc地址就存储在方法返回地址中。

12 本地方法接口、本地方法库与本地方法栈

  native修饰的java方法即是用其他语言编写的方法,主要是c。

  实现本地方法和本地方法库调用的部件就是本地方法接口。

  而本地方法的栈帧就是存放在本地方法栈中。

  另外,在hotspot中,本地方法栈和虚拟机栈合二为一了。

三、堆区

1 堆空间的概述

  堆空间是jvm中最大的一片内存空间。它是所有线程共享的。

  几乎所有的对象实例都会在堆空间中分配内存。

  堆空间在物理上是不连续的,但在逻辑上是连续的。

  虽然说各个线程共享堆。但不是完全共享,完全共享必然存在线程安全问题。故每个线程在对空间中有各自独有的缓冲区(TLAB)

  方法运行结束后,堆中的内容并不会马上被移除,而是等待cg时被移除。只要java栈弹栈就移除堆中内容的话,移除发生太频繁,影响效率。

2 堆空间的分代


  堆空间分为新生代、养老区、原空间。但其中原空间在逻辑上属于堆空间,在物理上属于方法区。

3 堆空间的大小设置和查看

  -Xms: 用来设置堆空间(新生代 + 老年代)的初始内存大小。(ms即memory start)

  -Xmx: 用来设置对空间(新生代 + 老年代)的最大内存大小。(mx即memory max)

  默认堆空间初始大小是物理电脑内存大小64\frac{物理电脑内存大小}{64}64物理电脑内存大小​,默认堆空间最大大小是物理电脑内存大小4\frac{物理电脑内存大小}{4}4物理电脑内存大小​

  如下是查看方式:

package com.spd.jvm;public class Test {public static void main(String[] args) {long initMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms: " + initMemory + "MB");System.out.println("-Xmx: " + maxMemory + "MB");}}

  在开发中,通常将初始大小和最大大小设置成同一个值。因为堆空间不断地扩容和释放内存,会给机器带来不必要的压力。(想想ArrayList的扩容,这个不仅要扩容,由于gc存在,利用率太低了还得缩容)

  我们把初始大小和最大大小都设置成600MB,却会发现运行结果是575MB。这是由于幸存者一区和幸存者二区同一时间内只能使用一个,这涉及到垃圾回收的知识。就是说设置的时候给的内存,人家是按两个幸存者都有的情况分配这么多。但是统计的时候却是按只有一个算。

4 新生代与老年代

  存储在JVM中的Java对象可以被划分为两类:一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速。另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。

  一个对象刚刚创建就会进入伊甸园(圣经中人类刚刚创建也是生活在伊甸园),若一段时间内没有被gc而幸存下来,那他就会进入幸存者一区或二区。若幸存者经过一段时间还未被gc,那就会进入老年代。其中绝大部分的对象都是在新生代中消失的。

  修改老年代与新生代的方法:-XX: NewRatio-2,表示新生代占一份,老年代占两份。新生代占三分之一。

  一般来说,这个比例很少会去进行修改,除非我们明确地知道代码中会有大量生命周期比较长或比较短的对象。

  另外,默认情况下,伊甸园和幸存者0区和幸存者1区的比例是8 : 1 : 1。开发人员可以通过一XX:SurvivorRatio=?来调整这个比例。

5 对象的分配

  对象刚创建时会进入伊甸园,伊甸园装满后,开始启用gc算法,进行Young GC / Minor GC。把垃圾清除掉,把非垃圾存入幸存者零区(from区)。并给他们的年龄计数器赋值为“1”。这时伊甸园就空了。

  待伊甸园再次被填满,进行一轮Minor GC,找到垃圾后释放掉。然后将非垃圾移入幸存者一区(to区)。判断幸存者零区内容是否需要被GC,若需要GC则释放掉,否则将幸存者零区(from区)的内容的年龄计数器自增,并移入幸存者一区(to区)

  此时s1中有数据,而s0中没有数据,那么s1就成了from区,s0成了to区。

  在两个幸存者区不停传递,就会增加年龄计数器的值,若某些数据的年龄计数器数值达到15了,那就把它的年龄计数器自增并移动到老年代。移入老年代的过程叫做晋升promotion,而15被称为阈值(不想画图了)

  gc的频繁发生地是新生代,很少在老年区gc,几乎不在幸存者区进行gc。

6 对象分配的特殊情况


  超大对象直接进入老年代,幸存者区装不下则直接晋升老年代。

7 Minor GC、Major GC及Full GC

  Minor GC即Young GC,针对于新生代。

  Major GC即Old GC针对于老年代(会报OOM),只有CMS GC会有单独收集老年代的行为。

  Full GC针对于整个堆空间。

  GC分为部分GC(Partial GC)和整堆GC(Full GC)

8 JVM的内存分配策略

9 对空间为每个线程分配的TLAB

10 堆空间常用参数设置

11 空间分配担保

12 逃逸分析



四、方法区

1 栈、堆、方法区的交互关系

2 方法区的基本理解

  《Java虚拟机规范》中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap (非堆),目的就是要和堆分开。

  方法区的大小决定了系统可以定义多少类,如果系统定义了太多类(例如加载过多第三方jar包、或tomcat部署的工程太多),就会导致方法区溢出报OOM。

  启动JVM创建方法区,关闭JVM释放方法区。

  所谓的永久代、元空间都是方法区的实现方式。从jdk8开始,就用元空间代替了永久代。

  永久代与元空间最大的区别在于元空间使用的是本地内存,而永久代使用的是虚拟机设置的内存中。

3 设置方法区大小的相关参数

3 如何解决OOM

4 方法区的内部结构

  《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。



  另外,如图:类加载器会记录它都加载了哪些类。而方法区中的类信息也会记录这些类是哪些类加载器加载的。

  补充说明:每个全局常量(static final)在编译的时候就会被分配。而非准备或初始化阶段。

5 常量池

  方法区中包含的叫做运行时常量池,而字节码文件中包含的叫常量池。将字节码文件中的常量池加载到方法区以后,就叫做运行时常量池了。

  也就是说字节码的常量池中的类都是符号引用,在运行的时候才会转换成直接引用。

  我们用jclasslib来查看字节码:

  其中的#2 #3 #4 便是常量池中的符号引用。

6 运行时常量池


  比起字节码文件中的常量池,方法区中的运行时常量池具有动态性。有可能运行时常量池的信息会比常量池的多。

7 方法区的演进细节


  要注意一点,很多帖子和书上都说静态变量是存储在方法区中,实际上这是错的,如上图,静态变量应该存储在堆里。

  jdk6:

  jdk7:

  jdk8:

  ① 在jdk8中为什么要把永久代替换成元空间?

  其中对于第二点,永久代调优困难,因为永久代是虚拟机内存,需要进行GC判断某个类还需不需要使用(永久代需要Full GC来回收垃圾)。所以永久代的GC效率较低,这个过程很浪费时间。

  ② 在jdk7中为什么要把字符串常量池(StringTable)和静态变量调整位置?

  正如上面所说,永久代GC效率低,在Full GC中才会触发。而Full GC是老年代空间不足、永久代空间不足时才会触发。这就导致StringTale回收率不高,进一步导致永久代内存不足。而将StringTable放进堆里便能及时回收内存了。

8 方法区的垃圾回收

  方法区垃圾回收主要回收的是常量池中废弃的常量和不再使用的类型。

  有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java虚拟机规范》对万法区的约界定非吊见松的,北业刊知批的l收值哭左左法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZCG收集器就不支持类卸载)。

  一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的Hotspot虚拟机对此区域未完全回收而导致内存泄漏。

  常量池中主要存放两大常量:字面量和符号引用。字面量更贴近于Java语言层面的常量概念,而符号引用更贴近于编译原理层面的概念。HotSpot对常量池的回收策略即是只要常量池中的常量没有被任何地方引用到,就可以被回收。

  但是一个类型是否还要使用则比较难判断,他需要满足以下三个条件:① 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。② 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSCG、JSP的重加载等,否则通常是很难达成的。③ 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

【JVM学习笔记】运行时数据区相关推荐

  1. Java 虚拟机学习笔记 | 运行时数据区总结

    前言 要想学习好 Java,Java虚拟(JVM)的学习是绕不开的.学习 Java虚拟(JVM)首先就要先了解的就是Java虚拟(JVM)运行时数据区. 在Java语言和虚拟机规范中对运行时数据区进行 ...

  2. @JVM内存模型(运行时数据区)

    前言 说到Java内存区域,可能很多人第一反应是"堆栈".首先堆栈不是一个概念,而是两个概念,堆和栈是两块不同的内存区域,简单理解的话,堆是用来存放对象而栈是用来执行程序的.对于J ...

  3. [JVM]了断局: “运行时数据区“理论梳理

    Table of Contents 一.前言 二.运行时数据区 2.1.程序计数器 2.2.Java堆 2.3.方法区 2.4.运行时常量池 2.5.直接内存 2.6.Java虚拟机栈 2.7.本地方 ...

  4. 深入理解JVM底层原理——运行时数据区

    运行时数据区概述和线程 1.运行时数据区概述 !     内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了Java在运行过程中内存申请 ...

  5. JVM系列之运行时数据区

    1.运行时数据区 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是 ...

  6. JVM内存模型——运行时数据区的特点和作用

    文章目录 前言 1程序计数器 2本地方法栈 3虚拟机栈 3.1局部变量表 3.2操作数栈 3.3动态连接 3.4返回地址 4方法区 5堆 5.1查看方法区跟堆大小 5.2新生代跟老年代 5.3什么时候 ...

  7. 【JVM】3. 运行时数据区及程序计数器

    文章目录 3.1.

  8. 浅谈JVM之运行时数据区

    Java内存结构 提到Java执行流程,我们就要关注Java的内存结构.我们还要了解到的一个概念就是Java内存结构≠Java内存模型.今天我们先不深入展开. 如上图所示,首先Java源代码文件(.j ...

  9. JVM从入门到精通(五): Java运行时数据区和常用指令

    JVM Runtime Data Area and JVM Instructions Java运行时数据区以及JVM指令 i=i++结果为8 i=++i结果为9 一个class的生命周期 以下面的规范 ...

  10. JVM学习笔记之-运行时数据区概述及线程概述,程序计数器(PC寄存器),虚拟机栈(栈,局部变量表,操作数栈,动态连接,方法调用,方法返回地址等),本地方法接口,本地方法栈

    运行时数据区概述及线程概述 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行.JVM内存布局规定了Java在运行过程中内存申请.分配.管理的策略,保证了JV ...

最新文章

  1. 10、同步机制遵循的原则_我要遵循的10条原则
  2. 一人之力,刷爆三路榜单!信息抽取竞赛夺冠经验分享
  3. 《JS权威指南学习总结--1.1语言核心》
  4. Python 技术篇-利用pyqt5库读取剪切板已复制数据的格式类型实例演示,python判断复制文件的文件类型
  5. torchvision 笔记:transforms.Normalize()
  6. Android自动化测试 - 自动化测试工具比较
  7. 360浏览器打不开网页_苹果移动端、PC端safari浏览器打不开网页的解决方案!
  8. keepalived与lvs结合使用配置实例
  9. 自然语言处理 —— 2.4 嵌入矩阵
  10. java 核心技术2_你必须掌握的 21 个 Java 核心技术
  11. PHP 练习(新闻发布)
  12. 性能测试用例、策略和方法
  13. 我的Java开发学习之旅------心得总结:Java性能优化技巧集锦
  14. 进阶篇:3.4.1)机械加工件-不同制造工艺详解和对应设备
  15. hp 计算机如何显示在桌面上,HPDL1414 精致桌面小伴侣 显示时间日历和电脑状态(CPU温度负载...
  16. Unable to detect adb version, exit value: 0xc000007b
  17. c1-02西班牙的语言,【图片】考试的同学看过来~DELE-C1两个月准备(实用经验+超详细流程)转【西班牙语吧】_百度贴吧...
  18. 如何打开sql server配置管理器
  19. markdown图片旋转
  20. Spring3 MVC详解二

热门文章

  1. Android 开发中的SSL pinning
  2. 洛谷P2056 采花
  3. wxpython查询功能_wxpython程序基本功能源码整理,包括基本文字,输入框,字体设置,按钮绑定事件触发...
  4. 6个习惯让你立刻远离拖延
  5. Unity 批量修改命名(重命名)
  6. JAVA语言考试系统的设计与实现(论文+源代码+文献综述+外文翻译+开题报告)
  7. Linux 系统黑洞 /dev/null
  8. 如何设计出更好的 API ?
  9. 鼠标光标变成了一个点
  10. python3GUI--实用!B站视频下载工具(附源码)