学习JVM
所谓虚拟机,就是一台虚拟的机器。它是一款软件,用来执行一系列虚拟计算机指令,大体上虚拟机可以分为系统虚拟机和程序虚拟机,大名鼎鼎的Visual Box、VMware就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在java虚拟机中执行的指令我们成为Java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。Java发展至今,出现过很多虚拟机,最初Sun使用的一款叫做Classic的Java虚拟机,到现在引用最广泛的是HotSpot虚拟机,除了Sun以外,还有BEA的JRockit,目前JRockit和HotSpot都被Oracle收入旗下,大有整合的趋势。
下面我们来看下Java虚拟机的基本结构,如下图所示。
下面我们便来学习Java虚拟机的基本结构:
1、类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。
2、方法区:就是存放类信息、常量信息、常量池信息、包括字符串字面量和数字常量等。
3、Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到java堆中,堆空间是所有线程共享的。
4、直接内存:Java的NIO库允许Java程序使用直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
5、Java栈:每个虚拟机线程都有一个私有的栈,一个线程的Java栈在线程创建的时候被创建,Java栈中保存着局部变量、方法参数、同时Java的方法调用、返回值等。
6、本地方法栈:和Java栈非常类似,最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通常使用C编写)。
7、垃圾收集系统是Java的核心,也是必不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理。
8、PC(Program Counter)寄存器也是每个线程私有的空间,Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前被执行的指令,如果是本地方法,则PC寄存器值为undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量值指针等信息。
9、虚拟机最核心的组件就是执行引擎了,它负责执行虚拟机的字节码。一般会先进行编译成机器码后执行。
下面我们来看下堆、栈、方法区概念和联系
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
方法区则是辅助堆栈的永久区(Perm),解决堆栈信息的产生,是先决条件。
我们创建一个新的对象,User:那么User类的一些信息(类信息、静态信息、都存在于方法区中)
而User类被实例化出来之后,被存储到Java堆中,一块内存空间。当我们去使用的时候,都是使用User对象的引用,形如User user = new User();这里user就是存放在Java栈中的,即User真实对象的一个引用。如下图所示。
下面我们一起来详细学习下堆
java堆是和Java应用程序关系最密切的内存空间,几乎所有的对象都存放在其中,并且Java堆完全是自动化管理化的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示地释放。
根据垃圾回收机制不同,Java堆有可能拥有不同的结构。最为常见的就是将整个Java堆分为新生代和老年代。其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。
新生代分为eden区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互换角色的空间。
绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,当对象达到一定的年龄后,则进入老年代。如下图所示。新生代被垃圾回收的频率明显高于老年代,因为新生代存放的都是些刚创建的对象或时间比较短的对象,这些对象很不稳定,有可能有的对象一实例化出来就没有被使用过,所以GC会对新生代频繁的进行回收,而老年代存放的是那些经过很多次垃圾回收依然没有被回收的对象,这些对象相对来说已经很稳定了,GC便没有必要再频繁的去尝试回收了,低频率便可以满足要求。
我们单独拎出来新生代来详细说明,如下图所示,当一个对象刚被创建时会存放到eden区,eden这个单词是伊甸园的意思,伊甸园是亚当、夏娃出生的地方,所以用这个单词表示对象刚出生(刚被创建)。我们知道Java虚拟机有一套垃圾回收机制GC,GC会自动帮我们去回收已经不用的对象,当被创建的对象经过GC回收一次(没有被回收,是因为它在被别人引用),这个对象便会被放到s0区或者s1区,s0和s1在同一时刻只有一个正在被使用,另一个没有被使用。假如现在s0区正在被使用,GC来回收垃圾,根据复制算法,在s0区的依然被引用的对象都会被复制到当前没有被使用的s1区,等复制完之后,会把s0区中剩余的对象全部删除,然后切换到s1区,让这个区处于被使用状态。当GC下一次进行垃圾回收的时候,会自动去s1区去回收未被引用的对象,如果s1区现在有些对象依然处于被引用状态,那么根据算法,这些被引用的对象会被复制然后放到s0区,等把所有的没有被释放的对象都复制完并放到s0区后,s1区整个空间会被GC回收。s0区又开始被使用了。等到下次GC回收的时候就会去s0区去回收,如此循环往复,这样s0和s1便会不断互换角色。复制算法的好处是可以干净彻底的清楚垃圾,避免出现空间不连续的问题。
下面我们来学习下Java栈
Java栈是一块线程私有的内存空间,一个栈,一般由三部分组成:局部变量表、操作数栈和帧数据区。
局部变量表:用于报错函数的参数及局部变量。
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支撑常量池的解析,这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外,当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
下面我们来学习java方法区
java方法区和堆一样,方法区是一块所有线程共享的内存区域,它保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出错误。方法区可以理解为永久区(Perm)。因此我们有可能要配置方法区的空间大小的。
下面进入最重要的部分-----虚拟机参数(JVM调优)
在虚拟机运行的过程中,如果可以跟踪系统的运行状态,那么对于问题的故障排查会有一定的帮助,为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其实主要就是围绕着堆、栈、方法区进行配置。
首先来看堆分配参数
-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志。
-XX:+UseSerialGC 配置串行回收器
-XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况
-Xms: 设置java程序启动时初始堆大小
-Xmx: 设置java程序能获得的最大堆大小
-Xmx20m -Xms5m -XX:+PrintCommandLineFlags:可以将隐式或者显示传给虚拟机的参数输出。
下面可以来看个实例Test01,如下所示。
- package com.jvm.base;
- public class Test01 {
- public static void main(String[] args) {
- //-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails
- //查看GC信息
- System.out.println("max memory:"+Runtime.getRuntime().maxMemory());
- System.out.println("free memory:"+Runtime.getRuntime().freeMemory());
- System.out.println("total memory:"+Runtime.getRuntime().totalMemory());
- byte[] b1 = new byte[1*1024*1024];
- System.out.println("分配了1M");
- System.out.println("max memory:"+Runtime.getRuntime().maxMemory());
- System.out.println("free memory:"+Runtime.getRuntime().freeMemory());
- System.out.println("total memory:"+Runtime.getRuntime().totalMemory());
- byte[] b2 = new byte[4*1024*1024];
- System.out.println("分配了4M");
- System.out.println("max memory:"+Runtime.getRuntime().maxMemory());
- System.out.println("free memory:"+Runtime.getRuntime().freeMemory());
- System.out.println("total memory:"+Runtime.getRuntime().totalMemory());
- }
- }
首先来解释下上面的max memory、free memory、total memory的意思。
1、maxMemory()这个方法返回的是Java虚拟机(这个进程)能构从操纵系统那里挖到的最大的内存
2、totalMemory:程序运行的过程中,内存总是慢慢的从操纵系统那里挖的,基本上是用多少挖多少,直 挖到maxMemory()为止,所以totalMemory()是慢慢增大的
3、freeMemory:挖过来而又没有用上的内存,实际上就是 freeMemory(),所以freeMemory()的值一般情况下都是很小的(totalMemory一般比需要用得多一点,剩下的一点就是freeMemory)
运行上面的代码,结果如下图所示,可以看到最开始的时候free memory(剩余内存)是254741016byte,当分配了1M后free memory的值是253692424byte,前者减去后者得到的值%1024%1024得到的值便是1M。与我们分配了1M刚好相符,再分配4M,这时free memory的值是249498104byte,用第一个free memory减去第三个free memory的差%1024%1024值就等于5M,刚好与我们分配了5M内存相符。
上面是在没有配置jvm参数的情况下的运行结果,下面我们设置下jvm参数,Run As的子菜单中我们点击"Run Configurations...",如下图所示。
在下图中的VM arguments:一栏中 "-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintCommandLineFlags",这行配置的意思是碰到GC就会打印日志,分配初始堆大小为5m,最大堆大小是20m,配置串行回收器,查看详细信息包括各个区的信息,将隐式或者显示传给虚拟机的参数输出
运行结果如下图所示。第一行打印的是我们配置的jvm参数。第二行max memory的值20316160byte相当于19.375M,与我们在上图配置的参数"-Xmx20m"基本上是一致的(注意:我们给jvm配置的空间大小并不一定就完全一致的那么大,程序真实运行情况往往会有所偏差,但差不了多少,这里我们配置了最大20m,现在是19.375m,已经是比较接近了)。第三行free memory的值5312280byte相当于5.066m,第四行total memory的值6094848byte相当于5.81m,total memory的值一般会比free memory稍大,但比max memory要小。这与我们设置的"-Xms5m"(初始堆大小)比较接近,可以认为它俩是一致的。
当我们分配了1M的内存后,触发了第一次垃圾回收, [GC (Allocation Failure) [DefNew: 764K->191K(1856K), 0.0010702 secs] 764K->529K(5952K), 0.0011022 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]这是打印的第一条垃圾回收的信息,[DefNew: 764K->191K(1856K), 0.0010702 secs]的意思是在GC之前新生代已使用764K,GC之后新生代使用容量为191K,说明回收了764K-191K=573K的垃圾。1856K是新生代的总容量,764K->529K(5952K)的意思是GC前java堆已使用空间764K,GC后java堆已使用空间是529K,也就是说回收了235K的堆垃圾。堆包括新生代和老年代。
分配了1M内存后,打印的free memory的值变成了4470408byte,这个值相当于4M,初始是5M,分配了1M,现在剩4M,符合运行情况。
当我们再分配4M的容量后,又触发了一次垃圾回收,[GC (Allocation Failure) [DefNew: 1249K->0K(1856K), 0.0009063 secs][Tenured: 1553K->1553K(4096K), 0.0017102 secs] 1586K->1553K(5952K), [Metaspace: 2636K->2636K(1056768K)], 0.0026923 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 这是打印的第二条垃圾回收信息,[DefNew: 1249K->0K(1856K), 0.0009063 secs]是指新生代原来使用的空间是1249K,垃圾回收后,新生代所用空间变为0K,说明新生代已经没有垃圾了。新生代占的空间是1856K,是1M多,[Tenured: 1553K->1553K(4096K), 0.0017102 secs]这句信息的意思是老年代没有回收任何垃圾,说明老年代中的对象十分稳定,老年代所占用的空间是4096K,也就是4M,4096%1856=2.2,这与新生代与老年代默认所占空间比例(1:2)基本上是一致的。[Metaspace: 2636K->2636K(1056768K)], 0.0026923 secs]这句信息的意思是元数据区经过垃圾回收后,没有回收任何垃圾,元数据区所占空间大小是1056768%1024%1024=1G。这里需要提醒的是,JDK1.8将永久区换成了元数据区。
分配4M内存后,free memory的值变成了4539056byte,这个值也相当于4M,而此时total memory发生了变化,不再是我们设置的初始值5M了,这是为什么呢?其实这是由于我们第一次分配1M后,free memory的值是4470408byte,约等于4M,但是我们知道,在环境实际运行中我们配置的参数与真实jvm环境值还是有一些出入的,我们申请分配4M内存时可能jvm真实环境中total memory已经不够申请了,于是乎向max memory申请要点空间,由于max memory的值是20M,足够申请这4M空间,因此当前total memory的空间大小便由原来的5M加上4M变成现在的10358784byte(约等于9M)。
再往下便是打印的Heap的详细信息,def new generation total 1920K, used 69K [0x00000000fec00000, 0x00000000fee10000, 0x00000000ff2a0000)明显指的是新生代的信息,总空间是1920K,用了69K,我们看到的[0x00000000fec00000, 0x00000000fee10000, 0x00000000ff2a0000)这句信息我们只关心前两个数就行了。我们将0x00000000fec00000和0x00000000fee10000都转成int,int a = 0x00000000fec00000;和int b = 0x00000000fee10000;然后用(b-a)/1024得到的值是2112K,这与total 1920K已经很接近了,两者一般不会完全相等,但我们要知道他俩的值应该是一样的。新生代下面的信息便是具体的eden、from、to三个模块具体的信息。再往下便是老年代的信息,再往下便是元数据区的信息(方法区)。
小结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提高性能。
下面我们来学习新生代的配置
-Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右。
-XX:SurvivorRatio:用来设置新生代中eden空间和from/to空间的比例。含义:-XX:SurvivorRatio=eden/from=eden/to
我们还是看个例子
- package com.jvm.base;
- public class Test02 {
- public static void main(String[] args) {
- //第一次配置
- //-XX:SurvivorRatio=2的意思是新生代的eden与s0或s1所占空间的比例,其中s0与s1是大小相等互相切换的两个区域
- //需要特别注意的是,下面的-Xms与-Xmn值不能相等,Xms一定要大于Xmn。因为Xms指定的是堆的初始大小,而Xmn只是新生代的大小
- //堆是包括新生代和老生代的,因此Xms要大于Xmn。
- //-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
- //第二次配置
- //-Xms20m -Xmx20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
- //第三次配置
- //-XX:NewRatio=老年代/新生代
- //-Xms20m -Xmx20m -Xmn7m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
- byte[] b = null;
- //连续向系统申请10MB空间
- for(int i=0;i<10;i++){
- b = new byte[1*1024*1024];//一次申请1M,申请10次,就是10M
- }
- }
- }
我们先进行第一次配置,如下图所示。
运行结果如下图所示。在Heap详细信息中,def new generation total 768K, used 490K [0x00000000fec00000, 0x00000000fed00000, 0x00000000fed00000)的意思是新生代当前起作用的空间总大小是768K,used 490K是指已经使用了490K。而紧跟这句信息的是如下三句信息。这三句的意思是eden区空间大小是512K,from区(也叫s0区)空间大小是256K,to区(也叫s1区)空间大小是256K,eden/ftom=2,并且eden/to=2。eden与from或eden与to两者之和是768K。从这儿也印证了,同一时刻from和to区只有一个区起作用。eden、from、to三个区加起来大小是1024K刚好是1M。
- eden space 512K, 45% used [0x00000000fec00000, 0x00000000fec3ab78, 0x00000000fec80000)
- from space 256K, 99% used [0x00000000fecc0000, 0x00000000fecffff8, 0x00000000fed00000)
- to space 256K, 0% used [0x00000000fec80000, 0x00000000fec80000, 0x00000000fecc0000)
tenured generation total 19456K, used 10413K [0x00000000fed00000, 0x0000000100000000, 0x0000000100000000)这句信息的意思是老年代所占空间大小是19456K,刚好等于19M,加上新生代的大小1M,刚好等于20M。
下面我们来修改下参数,修改成第二次配置所指定的参数,其实只修改了新生代的空间大小,原来是1M,现在要分配7M。如下图所示。
运行结果如下图所示,新生代分配的空间变大了,GC反而回收的次数增加了,但是也不全是,当我们把新生代的空间配置成10m时,GC的回收次数就变成了2次,当我们把新生代的空间配置成15m时,GC回收的次数就变成了1次。可见GC回收的触发机制并不全是新生代空间的大小,肯定还有其它因素的作用,只不过我不太清楚。下图的eden区与s0或s1区的比例是2:1,三者所占空间大小总和是7M,刚好就是我们分配的新生代的总大小。def new generation total 11520K, used 5875K 这句信息中的total 11520K 很明显不是eden、from、to三者之和,而是eden与from或者to两者之和。
下面我们来配置老年代与新生代的比例(对应代码中第三次配置),如下图所示。
运行结果如下图所示:可以看到,当前新生代总大小是5760K+704K+704K=7M。tenured generation total 13312K, used 2575K [0x00000000ff300000, 0x0000000100000000, 0x0000000100000000)这句信息中指明了老年代的大小是13312K=13M,老年代/新生代约等于2。
小结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略是尽可能将对象预留在新生代,减少老年代的GC次数。除了可以设置新生代的绝对大小(-Xmn),还可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/新生代。
下面我们来学习一下如何用内存分析工具来分析内存溢出。
首先我们先来造一个内存溢出的例子
- package com.jvm.base;
- import java.util.Vector;
- public class Test03 {
- public static void main(String[] args) {
- //-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/Test03.dump
- //堆内存溢出
- Vector v = new Vector<>();
- for(int i=0;i<5;i++){
- v.add(new Byte[1*1024*1024]);
- }
- }
- }
我们按照代码中的JVM参数进行配置,可以看到我们配置的最大内存才2M,而程序却想要获取5M内存,肯定会发生内存溢出。如下图进行配置(这里有个地方需要说一下,就是我们在Test03类的main方法处右键------>Run As----->Run Configuration,弹出框中可能没有Test03,这时我们可以点击"Java Application"---->New,就会出现Test03了)。
运行结果如下图所示。
学习JVM的更多相关文章
- java虚拟机学习-JVM内存管理:深入垃圾收集器与内存分配策略(4)
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项 ...
- java虚拟机学习-JVM内存管理:深入Java内存区域与OOM(3)
概述 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又 ...
- 学习JVM是如何从入门到放弃的?
前言 只有光头才能变强 JVM在准备面试的时候就有看了,一直没时间写笔记.现在到了一家公司实习,闲的时候就写写,刷刷JVM博客,刷刷电子书. 学习JVM的目的也很简单: 能够知道JVM是什么,为我们干 ...
- 学习JVM虚拟机原理总结
0x00:JAVA虚拟机的前世今生 1991年,在Sun公司工作期间,詹姆斯·高斯林和一群技术人员创建了一个名为Oak的项目,旨在开发运行于虚拟机的编程语言,允许程序多平台上运行.后来,这项工作就演变 ...
- 学习JVM参数前必须了解的
JVM参数是什么 大家照相通常使用手机就够用了,但是针对发烧友来说会使用更专业的设备,比如单反相机,在单反里有好几个模式,P/A/S/M,其中P是傻瓜模式,程序会自动根据环境设置快门速度和光圈大小,以 ...
- 学习JVM GarbageCollection
前言 Java和C++之间显著的一个区别就是对内存的管理.Java和C++把内存管理的权利赋予给开发人员的方式不同,Java拥有一套自动的内存回收系统(Garbage Collection,GC)简称 ...
- java虚拟机学习-JVM调优总结-调优方法(12)
JVM调优工具 Jconsole,jProfile,VisualVM Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用.对垃圾回收算法有很详细的跟踪.详细说明参考这里 ...
- 学习jvm(一)--java内存区域
前言 通过学习深入理解java虚拟机的教程,以及自己在网上的查询的资料,做一个对jvm学习过程中的小总结. 本文章内容首先讲解java的内存分布区域,之后讲内存的分配原则以及内存的监控工具.再下来会着 ...
- JVM基础:深入学习JVM堆与JVM栈
转自:http://developer.51cto.com/art/201009/227812.htm JVM栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;JVM堆解决的是数据存储的问题, ...
随机推荐
- sdut3140 A*B(math)
题目:传送门 题目描述 Your task is to find the minimal positive integer number Q so that the product of digits ...
- phpstudy2016 redis扩展 windows
第一步,查看环境的信息. 第二步,根据线程是否安全.架构32位或64位下载redis扩展. http://pecl.php.net/package-stats.php 第三步,php_redis.dl ...
- Hadoop 2.0 上深度学习的解决方案
原文连接:https://www.paypal-engineering.com/tag/data-science/ 摘要:伴随着数据的爆炸性增长和成千上万的机器集群,我们需要使算法可以适应在如此分布的 ...
- input 虚拟键盘
if (!Element.prototype.scrollIntoViewIfNeeded) { Element.prototype.scrollIntoViewIfNeeded = function ...
- PHP中构造函数和析构函数解析
构造函数 void __construct ([ mixed $args [, $... ]] ) PHP 5 允行开发者在一个类中定义一个方法作为构造函数.具有构造函数的类会在每次创建新对象时先调用 ...
- Javascript中的函数数学运算
1.Math函数与属性使用语法 Math.方法名(参数1,参数2,...); Math.属性; 说明 Math函数可以没有参数,比如Math.random()函数,或有多个参数,比如Math.max( ...
- Asp.net Core Windows部署
一. IIS 部署模式 1. 安装IIS服务 2. 下载安装Core SDK https://www.microsoft.com/net/download/Windows/build3 ...
- [pixhawk笔记]5-uORB消息传递
本文主要内容翻译自官方文档:https://dev.px4.io/en/middleware/uorb.html 在前一篇笔记中使用uORB完成消息传递,实现了一个简单示例程序,本文将对uORB进行系 ...
- GET 对比 POST
HTTP 方法:GET 对比 POST HTTP 消息 标签列表(字母排序) 两种最常用的 HTTP 方法是:GET 和 POST. 什么是 HTTP? 超文本传输协议(HTTP)的设计目的是保证客户 ...
- go基础语法
定义变量: 可放在函数内,或直接放在包内使用var集中定义变量使用:=定义变量写的短一些 package main import ( "fmt" "math" ...