跨平台

Java的一大特性是跨平台,而Java是如何做到跨平台的呢?

主要依赖Java虚拟机,具体来说,是Java虚拟机在各平台上的实现。

Java虚拟机在不同的平台有不同的实现。同一份字节码,通过运行在不同平台上的Java虚拟机,可以完成相同的处理逻辑。

这样,由不同平台的Java虚拟机屏蔽了不同平台的差异,开发者无需针对不同的平台编写不同的代码。

而字节码,即class文件,由Java代码编译而来。

前端编译、解释、后端运行时编译

字节码,即class文件,由Java代码编译而来。这里的编译,是前端编译。即在代码运行前就完成了编译。

前端编译主要包括:

  • 词法分析
  • 语法分析
  • 填充符号表
  • 注解处理
  • 语义分析
  • 糖语法分解
  • 字节码生成

    由于这部分开发人员干涉得不多,不作过多讨论。

解释型语言、编译型语言

将代码翻译成机器码的时机的不同,可分为解释型语言编译型语言

而Java翻译成机器码的过程,既有解释器,又有即时编译器,所以Java属于哪一种语言,有些讨论。

类加载机制

JVM的类加载机制为双亲委派机制,即除了启动类加载器外,其它加载器在加载一个类前都委托父加载器进行加载,当父加载器反馈不在它加载范围内,才尝试自行加载,以保证加载类的唯一性。

public class GetClassLoader {

   public static void main(String[] args) {
/* 启动类加载器,按指定文件加载$JAVA_HOME/lib下的文件 */
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println("bootstrapClassLoader : " + bootstrapClassLoader); /* TODO 扩展类加载器,加载$JAVA_HOME/lib/ext下的文件 */ /* 应用程序类加载器,加载CLASSPATH下的文件 */
ClassLoader applicationClassLoader = GetClassLoader.class.getClassLoader();
System.out.println("applicationClassLoader : " + applicationClassLoader);
} }

结果:

bootstrapClassLoader : null
applicationClassLoader : sun.misc.Launcher$AppClassLoader@73d16e93

双亲委派机制

首先介绍双亲委派机制,除了启动类加载器,其它类加载器都有父类加载器,当一个类加载器接收到一个加载请求时,其首先委托其父类加载器去尝试加载,只有其父类加载器反映无法加载时,才由其自身加载。(当然,父类加载器也有其父类加载器,会级联向上委托)。

而类加载器向上委托,并未通过继承机制实现,而是通过组合方式实现。(为什么呢?)

细节可查看ClassLoader.loadClass(String name, boolean resolve)

                try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

ClassLoader.loadClass(String name, boolean resolve)通过父ClassLoad加载不成功后,调用自己的findClass(name)去加载类,此方法无具体实现,用的是模板方法模式。

自定义ClasdLoad

上面说到ClassLoad的findClass(name),我们自定义的ClassLoad就可重写此方法去加载类,比如:

package com.nicchagil.jvmexercise.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException; public class MyClassLoader extends ClassLoader { @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = null; /* 从指定路径读取Class */
try (FileInputStream fis = new FileInputStream(
"D:/my_classload_classpath/" + name + ".class")) { /* 读取字节 */
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = fis.read())) {
baos.write(ch);
}
bytes = baos.toByteArray();
} catch (FileNotFoundException e) {
throw new ClassNotFoundException(e.getMessage());
} catch (Exception e) {
throw new RuntimeException(e);
} /* 读取不到Class */
if (bytes == null) {
throw new ClassNotFoundException();
} return this.defineClass(name, bytes, 0, bytes.length);
} /**
* 打印ClassLoad层级
* @param classLoader 类加载器
*/
public static void printClassLoadHierarchy(ClassLoader classLoader) {
int depth = 0; System.out.println("depth " + ++depth + " : " + classLoader);
ClassLoader parentClassLoader = null; do {
parentClassLoader = classLoader.getParent();
System.out.println("depth " + ++depth + " : " + parentClassLoader);
classLoader = parentClassLoader;
} while (classLoader != null);
} public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("User");
System.out.println("clazz.newInstance() : " + clazz.newInstance());
System.out.println("clazz.getClassLoader() : " + clazz.getClassLoader());
System.out.println("打印ClassLoad层级:");
printClassLoadHierarchy(myClassLoader);
} }

上次用自定义的ClassLoad加载的类,记得不要放在类路径下,不然会被AppClassLoader加载,双亲委派机制嘛!

结果:

clazz.newInstance() : User [name=Nick Huang]
clazz.getClassLoader() : com.nicchagil.jvmexercise.classloader.MyClassLoader@15db9742
打印ClassLoad层级:
depth 1 : com.nicchagil.jvmexercise.classloader.MyClassLoader@15db9742
depth 2 : sun.misc.Launcher$AppClassLoader@73d16e93
depth 3 : sun.misc.Launcher$ExtClassLoader@7852e922
depth 4 : null

引申:ClassNotFoundException与NoClassDefFoundError的区别

最重要的区别,ClassNotFoundException是异常,并且为检查异常,即预料有可能发生的异常。

而NoClassDefFoundError是错误,是一件严重的事情。而它什么时候发生,最好去看下NoClassDefFoundError的类描述,那儿说得很清楚。这里简述:在代码中方法调用,或通过new关键字去实例化一个类,JVM或类加载器去加载类定义时无法找到类定义,就会抛出。此类定义在编译时存在,但运行时找不到。

下面,写一个ClassNotFoundException的例子:

public class ClassNotFoundExceptionExercise {

   public static void main(String[] args) {
try {
Class.forName("java.lang.StringX");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} }

结果:

java.lang.ClassNotFoundException: java.lang.StringX
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.nicchagil.jvmexercise.classnotfound.ClassNotFoundExceptionExercise.main(ClassNotFoundExceptionExercise.java:7)

为什么要双亲委派机制

如果没有双亲委派机制,试想一下,java.lang.String这个类由启动类加载器加载了,然而你在ClassPath上自定义一个java.lang.String,并由应用程序类加载器去加载,这样就存在两个java.lang.String。这样,通过包路径+类名定义的类唯一性原则不就被破坏了吗?

对于,我们对类加载有其它个性化需求,比如从网络中加载某些类,可以自定义类加载器。

比如Tomcat的实现,有自定义类加载器(绿色部分):



而为什么Tomcat需要自定义类加载器,其中一个原因是各个Web应用之间的隔离问题。

关于Tomcat与ClassLoader,有篇文章写得不错:浅议tomcat与classloader

运行时内存

运行时内存,可粗分为两大块看:线程私有的内存区和线程共享的内存区:

而线程共享区域为堆,堆是JVM内存分配垃圾回收机制的作用区域,是非常受关注的一部分:可分为新生代区、老年代区、永久代区。这里后续细作讨论。

如何确定对象是否有引用

通常确认对象能否有引用,主要有两种做法:引用计数法可达性分析法

引用计数法,就是用计数器计算一个对象被引用的次数,此方法简单,但不好解决依赖的问题。

可达性分析法,是JVM的运用方案,从指定的根节点往下寻找,能被根节点直接或间接连接的对象则有引用,否则无引用。

如何确定对象能否被GC

当对象没有引用,并不是立即被GC,而需要通过某些逻辑,也就是仍有挽救机会。

回收空间的基础算法

确定哪些对象需要GC,根据不同区域的特点,可运用不同的空间回收算法。比如新生代区,因其特点为存活的对象总数不多,复制代码不高,常用复制算法,而老年代区,常用标记-清除算法或其升级版标记-整理算法

即时编译哪些代码

HotSpot虚拟机HotSpot,为热点的意思。之前介绍将翻译成机器码有解释即时编译两种手段。

解释,是直译。

即时编译,则经过优化的编译。当然,翻译时间比较长,但运行效率比较高。即时编译按照程度不同,有C1C2两个级别,C2级别优化程度更高,常用于服务器模式运行JVM。

既然即时编译相对耗时,当然优先对较频繁执行的代码进行翻译,以下流程图简单描述识别热点代码:

垃圾收集器

JVM有很多种垃圾收集器,它们各具特点,用户根据场景选择合适的收集器。对于响应时间有一定要求的应用,多用CMS收集器G1收集器,这里主要讨论它们。

CMS收集器

CMSConcurrent Mark Sweep,即并发标记清除,从名字可知,使用的是标记清除算法,并且是并发的。

那么是如何并发的呢?是全程并发执行,还是部分步骤并发的呢?下面是CMS收集的几个步骤:

  1. 初始标记。仅标记GC Roots直接引用的对象。(串行,耗时短)
  2. 并发标记。对于GC Roots引用的对象进行深层次的可达性分析。(并发,耗时长)
  3. 重新标记。因并发标记是并发执行的,在标记过程中难免对象的引用有所变动,重新标记用于修正并发标记期间的变动。(串行,耗时短)
  4. 并发清除。清除标记的对象。(并发,耗时长)

了解了CMS收集的运作,可以发现它的缺点:

  1. 它有标记清除算法的缺点,就是容易形成不连续的可用内存块
  2. 有浮动垃圾。在并发清除阶段,由于是并发执行的,执行过程中其它代码的运行有可能产生新的可回收对象。

G1收集器

G1Garbage First,即垃圾优先,意为回收价值高的垃圾优先回收。适用于响应时间高、大内存、多CPU的环境。

它有几个特点:

  1. 利用多CPU缩短Stop The World的时间
  2. 运用标记整理算法和复制算法
  3. 可预测停顿时间

一些改变:

  1. 之前的收集器专用于新生代区老年代区的收集,而这两个区域G1都能收集。内存布局也有所改变,虽还有新生代区老年代区的概念,但不再从物理区域上划分,而是由可能非连续的内存区域组成新生代区老年代区
  2. 在每个Region维护Remember Set,在写入引用类型时,如果引用的对象处于其它Region,即将引用对象信息写入Remember Set,这样,在做可达性分析时,对Remember Set也作可达性分析,则可避免全堆扫描。

收集步骤与CMS收集器差不多,比较大的差别在于最后一个步骤筛选回收,根据用户期待的回收时间收集对象,全部步骤如下:

  1. 初始标记。仅标记GC Roots直接引用的对象。(串行,耗时短)
  2. 并发标记。对于GC Roots引用的对象进行深层次的可达性分析。(并发,耗时长)
  3. 重新标记。因并发标记是并发执行的,在标记过程中难免对象的引用有所变动,重新标记用于修正并发标记期间的变动。(串行,耗时短)
  4. 筛选回收。(并发,耗时长)

JVM参数

打印gc日志

当前文件路径下生成名为gc.log的gc日志:

-Xloggc:gc.log # 输出指定路径、名称的日志
-XX:+PrintGCDetails # 打印详细日志
-XX:+PrintGCDateStamps # 打印日志时打印系统时间

运行模式(Server模式、Client模式)

JVM运行模式分Server模式Client模式,可通过修改的jvm.cfg文件中-server KNOWN-client KNOWN的先后顺序切换模式,此文件在我的电脑上位于D:\green\jdk1.8.0_25\jre\lib\amd64\jvm.cfg

注:我的JDK版本是1.8,默认是使用Server模式

-server KNOWN
-client IGNORE

而查询当前JDK使用哪个模式:

C:\>java -version
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b18)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

使用指定收集器

-XX:+UseG1GC # 使用G1收集器
-XX:+UseConcMarkSweepGC # 使用CMS收集器

内存大小设置

-Xms512m # 最小堆512M
-Xmx512m # 最大堆512M
-Xss512k # 每个线程的栈(Stack Space)大小

生成堆转储文件

当内存溢出时,需导出堆转储快照分析异常,记得提前建好目录d:/dump/

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/dump/

会导出java_pid进程号.hprof的文件,我一般用Memory Analyzer(MAT)进行分析。如果快照太大,可能会不够内存打开,可在MemoryAnalyzer.ini调整最大堆大小。

运行时,也可以通过jmap工具导出堆转储快照:

# 查询Java的进程列表
jps # 导出堆转储快照
jmap -dump:format=b,file=D:\dump_workspace\mydump_20170715-5 进程ID

日志格式

G1日志

用JVM参数-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps,得到详细的gc日志。

日志头部,包含JVM环境、版本、内存、参数等信息:

Java HotSpot(TM) 64-Bit Server VM (25.131-b11) for windows-amd64 JRE (1.8.0_131-b11), built on Mar 15 2017 01:23:53 by "java_re" with MS VC++ 10.0 (VS2010) # JVM的环境,如JVM版本
Memory: 4k page, physical 8268588k(4642384k free), swap 16535316k(12403048k free) # 内存信息,如物理内存的总量、可用量,交换区内存的总量、可用量
CommandLine flags: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=1073741824 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation -XX:+UseStringDeduplication # 运行的JVM参数

新生代GC:

2017-07-04T23:31:13.837+0800: 0.388: [GC pause (G1 Evacuation Pause) (young), 0.0114754 secs] # 在显示时间(JVM启动0.388秒后),发生新生代GC
[Parallel Time: 2.8 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 387.7, Avg: 387.7, Max: 387.8, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 1.0, Max: 2.7, Diff: 2.5, Sum: 3.9]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
[Object Copy (ms): Min: 0.0, Avg: 1.7, Max: 2.4, Diff: 2.4, Sum: 6.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.2]
[Termination Attempts: Min: 1, Avg: 3.5, Max: 7, Diff: 6, Sum: 14]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 2.7, Avg: 2.8, Max: 2.8, Diff: 0.1, Sum: 11.0]
[GC Worker End (ms): Min: 390.4, Avg: 390.4, Max: 390.5, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[String Dedup Fixup: 0.2 ms, GC Workers: 4]
[Queue Fixup (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Table Fixup (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]
[Clear CT: 0.0 ms]
[Other: 8.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.5 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 12.0M(12.0M)->0.0B(15.0M) Survivors: 0.0B->2048.0K Heap: 12.0M(256.0M)->2826.0K(256.0M)] # 伊甸区的使用量和总量在GC前后的变化、存活区的内存变化、堆的使用量和总量在GC前后的变化
[Times: user=0.01 sys=0.00, real=0.02 secs] # 实际花费了0.02秒

老年代GC:

2017-07-05T14:13:49.219+0800: 26.825: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0170832 secs] # 初始标记
...etc...
[Eden: 30.0M(92.0M)->0.0B(86.0M) Survivors: 12.0M->5120.0K Heap: 153.4M(256.0M)->127.1M(256.0M)]
[Times: user=0.06 sys=0.00, real=0.02 secs]
2017-07-05T14:13:49.237+0800: 26.842: [GC concurrent-root-region-scan-start] # 并发扫描开始
2017-07-05T14:13:49.244+0800: 26.850: [GC concurrent-string-deduplication, 1633.0K->271.8K(1361.2K), avg 62.3%, 0.0077193 secs]
2017-07-05T14:13:49.247+0800: 26.852: [GC concurrent-root-region-scan-end, 0.0101110 secs] # 并发扫描结束
2017-07-05T14:13:49.247+0800: 26.852: [GC concurrent-mark-start] # 并发标记开始
2017-07-05T14:13:49.425+0800: 27.030: [GC concurrent-mark-end, 0.1781354 secs] # 并发标记结束
2017-07-05T14:13:49.432+0800: 27.038: [GC remark 27.038: [GC ref-proc, 0.0026288 secs], 0.0112982 secs] # 二次标记
[Times: user=0.06 sys=0.00, real=0.01 secs]
2017-07-05T14:13:49.444+0800: 27.050: [GC cleanup 157M->151M(256M), 0.0004895 secs] # 清除
[Times: user=0.00 sys=0.00, real=0.00 secs]
2017-07-05T14:13:49.444+0800: 27.050: [GC concurrent-cleanup-start] # 并发清除开始
2017-07-05T14:13:49.444+0800: 27.050: [GC concurrent-cleanup-end, 0.0000132 secs] # 并发清除结束

JVM工具

jconsole

运行$JAVA_HOME/bin/jconsole.exe即可:

概览页签:

内存页签:

可选择:堆内存使用量非堆内存使用量内存池“PS Old Gen”内存池“PS Eden Space”内存池“PS Surviver Space”内存池“Metaspace”内存池“Code Cache”内存池“Compressed Class Space”

线程页签:

类页签:

VM概要页签:

MBean页签:

jvisualvm

运行$JAVA_HOME/bin/jvisualvm.exe即可:

概述:

监视:

线程:

抽样器:

Profiler:

MAT

手动导出快照:

jmap -dump:format=b,file=D:\dump_workspace\mydump_20170715-5 进程ID

或者设置OOM并发生了然后导出了快照,可以用MAT查看、分析快照:

内存泄漏

如下图,每个谷底(即Full GC)的值没有递增,说明没有内存泄漏,如果谷底有递增,即说明有可能内存泄漏:

如果怀疑有内存泄漏,用MAT分析快照,对比快照,找出并分析可疑代码。

【Java虚拟机】浅谈Java虚拟机的更多相关文章

  1. 【Java】浅谈Java IO

    注意 本文的代码,为了学习方便,简化代码复杂度,未考虑拆包.粘包等情况的处理.所以仅供学习使用,不能用于实际环境. 阻塞IO,BIO Java1.1发布的IO是BIO.阻塞地连接之后,通过流进行同步阻 ...

  2. 【JAVA】浅谈java内部类

    一.什么是内部类? 到底什么是内部类呢?通俗的讲,就是在类内部定义的类,包括定义在一个类的方法外面.方法里面或者代码块中. 二.为什么要使用内部类? 为什么我们要不走寻常路,把一个类定义在另一个类的内 ...

  3. 【JAVA】浅谈java枚举类

    一.什么情况下使用枚举类? 有的时候一个类的对象是有限且固定的,这种情况下我们使用枚举类就比较方便? 二.为什么不用静态常量来替代枚举类呢? public static final int SEASO ...

  4. 【Java】浅谈Java内部类(转载)

    说得挺细的,不是泛泛而谈.https://blog.csdn.net/weixin_42762133/article/details/82890555

  5. 从虚拟机视角谈 Java 应用性能优化

    从虚拟机视角谈 Java 应用性能优化 周 祥, 软件工程师, IBM 简介:Java 的普及和广泛应用,以及其基于虚拟机运行的机制,使得性能问题越来越重要.本文从 Java 虚拟机的角度,特别是垃圾 ...

  6. 浅谈Java线程安全

    浅谈Java线程安全 - - 2019-04-25    17:37:28 线程安全 Java中的线程安全 按照线程安全的安全程序由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类 ...

  7. 浅谈Java中的栈和堆

    人们常说堆栈堆栈,堆和栈是内存中两处不一样的地方,什么样的数据存在栈,又是什么样的数据存在堆中? 这里浅谈Java中的栈和堆 首先,将结论写在前面,后面再用例子加以验证. Java的栈中存储以下类型数 ...

  8. 浅谈Java代理一:JDK动态代理-Proxy.newProxyInstance

    浅谈Java代理一:JDK动态代理-Proxy.newProxyInstance java.lang.reflect.Proxy:该类用于动态生成代理类,只需传入目标接口.目标接口的类加载器以及Inv ...

  9. 浅谈Java回收对象的标记和对象的二次标记过程_java - JAVA

    文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 一.对象的标记 1.什么是标记?怎么标记? 第一个问题相信大家都知道,标记就是对一些已死的对象打上记号,方便垃圾收集器的 ...

  10. 浅谈Java语言环境搭建-JDK8

    title: 浅谈Java语言环境搭建-JDK8 blog: CSDN data: Java学习路线及视频 1.What's the JDK,JRE JDK(Java Development Kit ...

随机推荐

  1. JavaScript开发区块链只需200行代码

    用JavaScript开发实现一个简单区块链.通过这一开发过程,你将理解区块链技术是什么:区块链就是一个分布式数据库,存储结构是一个不断增长的链表,链表中包含着许多有序的记录. 然而,在通常情况下,当 ...

  2. 006.Docker网络管理

    一 docker网络模式 Docker使用Linux的Namespaces技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Name ...

  3. MIT-6.824 Raft协议

    摘要 raft是一种比paxos容易理解的一致性算法,实现起来比paxos简单许多.本文前部分描述算法的细节,后部分尝试探讨下该算法的原理. 算法描述 raft算法之所以简单的原因之一是它将问题分解成 ...

  4. canvas学习-----画直线

    画布 1.添加canvas标签  可以通过CSS或者JS来设置canvs标签的width,height;Ps: <canvas id="cvs"></canvas ...

  5. loj#2665. 「NOI2013」树的计数

    目录 题目链接 题解 代码 题目链接 loj#2665. 「NOI2013」树的计数 题解 求树高的期望 对bfs序分层 考虑同时符合dfs和bfs序的树满足什么条件 第一个点要强制分层 对于bfs序 ...

  6. 10.25 正睿停课训练 Day9

    目录 2018.10.25 正睿停课训练 Day9 A 数独(思路 DP) B 红绿灯(最短路Dijkstra) C 轰炸(计算几何 圆并) 考试代码 B C 2018.10.25 正睿停课训练 Da ...

  7. Django查询SQL语句

    Django查询SQL语句 # 1 res=models.Book.objects.all() # print(res)#<QuerySet [<Book: Book object> ...

  8. javac编译出现需要标识符问题解决

    因为没有写public static void mian(String[] args) 在类里面只有属性和方法,内部类.不能直接写System.out.println():

  9. Windows平台交叉编译Arm Linux平台的QT5.7库

    1.准备交叉编译环境 环境说明:Windows 7 64位 此过程需要: (1)Qt库开源代码,我使用的是5.7.0版本: (2)Perl语言环境5.12版本以上: (3)Python语言环境 2.7 ...

  10. Github和Git上fork指南

    现在有这样一种情形:有一个叫做Joe的程序猿写了一个游戏程序,而你可能要去改进它.并且Joe将他的代码放在了GitHub仓库上.下面是你要做的事情: fork并且更新GitHub仓库的图表演示 For ...