`

JVM(Java虚拟机)启动{转}

 
阅读更多
当JVM(Java虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:

bootstrap classloader

extension classloader

system classloader

bootstrap classloader - 引导(也称为原始)类加载器,它负责加载Java的核心类。在Sun的JVM中,在执行java的命令中使用-

Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是

java.lang.ClassLoader的子类,而是由JVM自身实现的。大家可以通过执行以下代码来获得bootstrap classloader加载了那些核心类库:
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls.toExternalForm());
}
在我的计算机上的结果为:
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
file:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
file:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
file:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
file:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
file:/C:/j2sdk1.4.1_01/jre/classes
这时大家知道了为什么我们不需要在系统属性CLASSPATH中指定这些类库了吧,因为JVM在启动的时候就自动加载它们了。

extension classloader - 扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR

的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放

入这个目录的JAR类包对所有的JVM和system classloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为引导加载器

bootstrap classloader不是一个真正的ClassLoader实例
。所以当大家执行以下代码时:
System.out.println(System.getProperty("java.ext.dirs"));
ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
结果为:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader是system classloader的parent,而bootstrap classloader是extension classloader的parent,但它不是一个实际的

classloader,所以为null


system classloader - 系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系

统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如

果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:
System.out.println(System.getProperty("java.class.path"));
输出结果则为用户在系统属性里面设置的CLASSPATH。
classloader加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所

有Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器(而不是super

,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,

也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但

是必须重新启动JVM才能生效的原因。


每个ClassLoader加载Class的过程是:
1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
3.请求parent classloader载入,如果成功到8,不成功到5
4.请求jvm从bootstrap classloader中载入,如果成功到8
5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
6.从文件中载入Class,到8.
7.抛出ClassNotFoundException.
8.返回Class.

其中5.6步我们可以通过覆盖ClassLoader的findClass方法来实现自己的载入策略。甚至覆盖loadClass方法来实现自己的载入过程。

类加载器的顺序是:
先是bootstrap classloader,然后是extension classloader,最后才是system classloader。大家会发现加载的Class越是重要的越在靠前面

。这样做的原因是出于安全性的考虑,试想如果system classloader“亲自”加载了一个具有破坏性的“java.lang.System”类的后果吧。这

种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由bootstrap classloader

来加载的。大家可以执行一下以下的代码:
System.out.println(System.class.getClassLoader());
将会看到结果是null,这就表明java.lang.System是由bootstrap classloader加载的,因为bootstrap classloader不是一个真正的

ClassLoader实例,而是由JVM实现的,正如前面已经说过的。

下面就让我们来看看JVM是如何来为我们来建立类加载器的结构的:
sun.misc.Launcher,顾名思义,当你执行java命令的时候,JVM会先使用bootstrap classloader载入并初始化一个Launcher
,执行下列代码:
System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
结果为:
the Launcher's classloader is null (因为是用bootstrap classloader加载,所以class loader为null)
Launcher会根据系统和命令设定初始化好class loader结构,JVM就用它来获得extension classloader和system classloader,并载入所有的需

要载入的Class,最后执行java命令指定的带有静态的main方法的Class。extension classloader实际上是sun.misc.Launcher$ExtClassLoader

类的一个实例,system classloader实际上是sun.misc.Launcher$AppClassLoader类的一个实例。并且都是java.net.URLClassLoader的子类。

让我们来看看Launcher初试化的过程的部分代码。

Launcher的部分代码:
public class Launcher {
public Launcher() {
ExtClassLoader extclassloader;
try {
//初始化extension classloader
extclassloader = ExtClassLoader.getExtClassLoader();
} catch(IOException ioexception) {
throw new InternalError("Could not create extension class loader");
}
try {
//初始化system classloader,parent是extension classloader
loader = AppClassLoader.getAppClassLoader(extclassloader);
} catch(IOException ioexception1) {
throw new InternalError("Could not create application class loader");
}
//将system classloader设置成当前线程的context classloader(将在后面加以介绍)
Thread.currentThread().setContextClassLoader(loader);
......
}
public ClassLoader getClassLoader() {
//返回system classloader
return loader;
}
}

extension classloader的部分代码:
static class Launcher$ExtClassLoader extends URLClassLoader {

public static Launcher$ExtClassLoader getExtClassLoader()
throws IOException
{
File afile[] = getExtDirs();
return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
}
private static File[] getExtDirs() {
//获得系统属性“java.ext.dirs”
String s = System.getProperty("java.ext.dirs");
File afile[];
if(s != null) {
StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
int i = stringtokenizer.countTokens();
afile = new File;
for(int j = 0; j < i; j++)
afile[j] = new File(stringtokenizer.nextToken());

} else {
afile = new File[0];
}
return afile;
}
}

system classloader的部分代码:
static class Launcher$AppClassLoader extends URLClassLoader
{

public static ClassLoader getAppClassLoader(ClassLoader classloader)
throws IOException
{
//获得系统属性“java.class.path”
String s = System.getProperty("java.class.path");
File afile[] = s != null  Launcher.access$200(s) : new File[0];
return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
}
}

看了源代码大家就清楚了吧,extension classloader是使用系统属性“java.ext.dirs”设置类搜索路径的,并且没有parent。system

classloader是使用系统属性“java.class.path”设置类搜索路径的,并且有一个parent classloader。Launcher初始化extension

classloader,system classloader,并将system classloader设置成为context classloader,但是仅仅返回system classloader给JVM


  这里怎么又出来一个context classloader呢?它有什么用呢?我们在建立一个线程Thread的时候,可以为这个线程通过

setContextClassLoader方法来指定一个合适的classloader作为这个线程的context classloader,当此线程运行的时候,我们可以通过

getContextClassLoader方法来获得此context classloader,就可以用它来载入我们所需要的Class。默认的是system classloader
。利用这个

特性,我们可以“打破”classloader委托机制了,父classloader可以获得当前线程的context classloader,而这个context classloader可

以是它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的Class,这就打破了只能向父classloader请求的限

制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的classloader加载的时候,由system classloader(即在jvm

classpath中)加载的class可以通过context classloader获得定制的classloader并加载入特定的class(通常是抽象类和接口,定制的

classloader中是其实现),例如web应用中的servlet就是用这种机制加载的.


好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和

清除classloader的cache中已经载入的Class就行了,有两个方案,一是我们继承一个classloader,覆盖loadclass方法,动态的寻找Class文

件并使用defineClass方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来new一个classloader就行了,这样即更新了类搜索

路径以便来载入新的Class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。噢,太好了,我们几乎不用做什么工作,

java.netURLClassLoader正是一个符合我们要求的classloader!我们可以直接使用或者继承它就可以了!

这是j2se1.4 API的doc中URLClassLoader的两个构造器的描述:
URLClassLoader(URL[] urls)
Constructs a new URLClassLoader for the specified URLs using the default delegation parent ClassLoader.
URLClassLoader(URL[] urls, ClassLoader parent)
Constructs a new URLClassLoader for the given URLs.
其中URL[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。


好,现在我们能够动态的载入Class了,这样我们就可以利用newInstance方法来获得一个Object。但我们如何将此Object造型呢?可以将此

Object造型成它本身的Class吗?

首先让我们来分析一下java源文件的编译,运行吧!javac命令是调用“JAVA_HOME/lib/tools.jar”中的“com.sun.tools.javac.Main”的

compile方法来编译:

public static int compile(String as[]);

public static int compile(String as[], PrintWriter printwriter);

返回0表示编译成功,字符串数组as则是我们用javac命令编译时的参数,以空格划分。例如:
javac -classpath c:\foo\bar.jar;. -d c:\ c:\Some.java
则字符串数组as为{"-classpath","c:\\foo\\bar.jar;.","-d","c:\\","c:\\Some.java"},如果带有PrintWriter参数,则会把编译信息出到

这个指定的printWriter中。默认的输出是System.err。

其中Main是由JVM使用Launcher初始化的system classloader载入的,根据全盘负责原则,编译器在解析这个java源文件时所发现的它所依赖和

引用的所有Class也将由system classloader载入,如果system classloader不能载入某个Class时,编译器将抛出一个“cannot resolve

symbol”错误。

所以首先编译就通不过,也就是编译器无法编译一个引用了不在CLASSPATH中的未知Class的java源文件,而由于拼写错误或者没有把所需类库

放到CLASSPATH中,大家一定经常看到这个“cannot resolve symbol”这个编译错误吧!

其次,就是我们把这个Class放到编译路径中,成功的进行了编译,然后在运行的时候不把它放入到CLASSPATH中而利用我们自己的classloader

来动态载入这个Class,这时候也会出现“java.lang.NoClassDefFoundError”的违例,为什么呢?

我们再来分析一下,首先调用这个造型语句的可执行的Class一定是由JVM使用Launcher初始化的system classloader载入的,根据全盘负责原

则,当我们进行造型的时候,JVM也会使用system classloader来尝试载入这个Class来对实例进行造型,自然在system classloader寻找不到

这个Class时就会抛出“java.lang.NoClassDefFoundError”的违例。

OK,现在让我们来总结一下,java文件的编译和Class的载入执行,都是使用Launcher初始化的system classloader作为类载入器的,我们无法

动态的改变system classloader,更无法让JVM使用我们自己的classloader来替换system classloader,根据全盘负责原则,就限制了编译和

运行时,我们无法直接显式的使用一个system classloader寻找不到的Class,即我们只能使用Java核心类库,扩展类库和CLASSPATH中的类库

中的Class。

还不死心!再尝试一下这种情况,我们把这个Class也放入到CLASSPATH中,让system classloader能够识别和载入。然后我们通过自己的

classloader来从指定的class文件中载入这个Class(不能够委托parent载入,因为这样会被system classloader从CLASSPATH中将其载入),

然后实例化一个Object,并造型成这个Class,这样JVM也识别这个Class(因为system classloader能够定位和载入这个Class从CLASSPATH中)

,载入的也不是CLASSPATH中的这个Class,而是从CLASSPATH外动态载入的,这样总行了吧!十分不幸的是,这时会出现

“java.lang.ClassCastException”违例。

为什么呢?我们也来分析一下,不错,我们虽然从CLASSPATH外使用我们自己的classloader动态载入了这个Class,但将它的实例造型的时候是

JVM会使用system classloader来再次载入这个Class,并尝试将使用我们的自己的classloader载入的Class的一个实例造型为system

classloader载入的这个Class(另外的一个)。大家发现什么问题了吗?也就是我们尝试将从一个classloader载入的Class的一个实例造型为

另外一个classloader载入的Class,虽然这两个Class的名字一样,甚至是从同一个class文件中载入。但不幸的是JVM却认为这个两个Class是

不同的,即JVM认为不同的classloader载入的相同的名字的Class(即使是从同一个class文件中载入的)是不同的!这样做的原因我想大概也

是主要出于安全性考虑,这样就保证所有的核心Java类都是system classloader载入的,我们无法用自己的classloader载入的相同名字的

Class的实例来替换它们的实例。

看到这里,聪明的读者一定想到了该如何动态载入我们的Class,实例化,造型并调用了吧!

那就是利用面向对象的基本特性之一的多形性。我们把我们动态载入的Class的实例造型成它的一个system classloader所能识别的父类就行了

!这是为什么呢?我们还是要再来分析一次。当我们用我们自己的classloader来动态载入这我们只要把这个Class的时候,发现它有一个父类

Class,在载入它之前JVM先会载入这个父类Class,这个父类Class是system classloader所能识别的,根据委托机制,它将由system

classloader载入,然后我们的classloader再载入这个Class,创建一个实例,造型为这个父类Class,注意了,造型成这个父类Class的时候(

也就是上溯)是面向对象的java语言所允许的并且JVM也支持的,JVM就使用system classloader再次载入这个父类Class,然后将此实例造型为

这个父类Class。大家可以从这个过程发现这个父类Class都是由system classloader载入的,也就是同一个class loader载入的同一个Class,

所以造型的时候不会出现任何异常。而根据多形性,调用这个父类的方法时,真正执行的是这个Class(非父类Class)的覆盖了父类方法的方

法。这些方法中也可以引用system classloader不能识别的Class,因为根据全盘负责原则,只要载入这个Class的classloader即我们自己定义

的classloader能够定位和载入这些Class就行了。

这样我们就可以事先定义好一组接口或者基类并放入CLASSPATH中,然后在执行的时候动态的载入实现或者继承了这些接口或基类的子类。还不

明白吗?让我们来想一想Servlet吧,web application server能够载入任何继承了Servlet的Class并正确的执行它们,不管它实际的Class是

什么,就是都把它们实例化成为一个Servlet Class,然后执行Servlet的init,doPost,doGet和destroy等方法的,而不管这个Servlet是从

web-inf/lib和web-inf/classes下由system classloader的子classloader(即定制的classloader)动态载入。说了这么多希望大家都明白了。

在applet,ejb等容器中,都是采用了这种机制.

对于以上各种情况,希望大家实际编写一些example来实验一下。

最后我再说点别的,classloader虽然称为类加载器,但并不意味着只能用来加载Class,我们还可以利用它也获得图片,音频文件等资源的URL

,当然,这些资源必须在CLASSPATH中的jar类库中或目录下。我们来看API的doc中关于ClassLoader的两个寻找资源和Class的方法描述吧:
        public URL getResource(String name)
        用指定的名字来查找资源,一个资源是一些能够被class代码访问的在某种程度上依赖于代码位置的数据(图片,音频,文本

等等)。
一个资源的名字是以'/'号分隔确定资源的路径名的。
这个方法将先请求parent classloader搜索资源,如果没有parent,则会在内置在虚拟机中的classloader(即bootstrap classloader)的路

径中搜索。如果失败,这个方法将调用findResource(String)来寻找资源。
        public static URL getSystemResource(String name)
从用来载入类的搜索路径中查找一个指定名字的资源。这个方法使用system class loader来定位资源。即相当于

ClassLoader.getSystemClassLoader().getResource(name)。

例如:
System.out.println(ClassLoader.getSystemResource("java/lang/String.class"));
的结果为:
jar:file:/C:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/String.class
表明String.class文件在rt.jar的java/lang目录中。
因此我们可以将图片等资源随同Class一同打包到jar类库中(当然,也可单独打包这些资源)并添加它们到class loader的搜索路径中,我们

就可以无需关心这些资源的具体位置,让class loader来帮我们寻找了!
分享到:
评论
发表评论

文章已被作者锁定,不允许评论。

相关推荐

    java虚拟机规范高清中文版本(java SE 8版本)

    第1章 :简单地介绍了Java虚拟机的历史并... 第5章:定义了Java虚拟机启动以及类和接口的加载、链接和初始化的过程; 第6章:定义了Java虚拟机指令集; 第7章:提供了一张以操作码值为索引的Java虚拟机操作码助记表。

    JAVA虚拟机(JVM)规范(中文版).rar

    **JAVA虚拟机(JVM)规范** JAVA虚拟机(JVM)是Java语言的核心组成部分,它为Java程序提供了运行环境,使得Java代码能在任何支持JVM的平台上运行,实现了“一次编写,到处运行”的目标。JVM规范定义了Java程序如何...

    Java虚拟机规范中文版.pdf

    Java虚拟机规范还定义了类加载机制,即在JVM启动时或运行过程中动态加载类到内存中。类加载器在运行Java程序时按需加载类,这允许Java程序进行延迟加载,即仅在需要时才加载类,这有助于优化程序的启动时间和运行时...

    Java虚拟机规范中文版(JavaSE7).pdf

    类加载是Java虚拟机启动时或运行时动态加载类的过程。它由类加载器系统执行,通常分为引导类加载器、扩展类加载器和应用程序类加载器。加载过程包括加载、验证、准备、解析和初始化五个阶段,确保加载的类符合规范且...

    使用C++创建java虚拟机JVM,使用JNI调用java函数.zip

    本文将深入探讨如何使用C++创建Java虚拟机(JVM),并通过JNI来调用Java函数。这是一项技术性很强的任务,需要对C++编程、Java虚拟机的工作原理以及JNI接口有深入的理解。 首先,Java虚拟机(JVM)是Java平台的核心...

    深入理解Java虚拟机(jvm性能调优+内存模型+虚拟机原理).zip

    《深入理解Java虚拟机》是一本深度探讨Java虚拟机(JVM)的著作,涵盖了JVM性能调优、内存模型以及虚拟机原理等多个关键领域。本文将基于这些主题,详细阐述其中的重要知识点。 首先,我们要了解Java虚拟机(JVM)...

    java虚拟机JVM详解ppt

    ### Java虚拟机(JVM)详解 #### 一、引言 Java虚拟机(JVM)作为Java编程语言的核心组件之一,其重要性不言而喻。本文将深入剖析JVM的基本概念、架构及其内存管理机制,重点解读堆和栈内存溢出的情况及案例分析。 ##...

    Java虚拟机规范.Java SE 8版

    《Java核心技术系列:Java虚拟机规范(Java SE 8版)...第5章定义Java虚拟机启动以及类与接口的加载、链接和初始化过程;第6章阐释并列举Java虚拟机指令集;第7章提供一张以操作码值为索引的Java虚拟机操作码助记符表。

    Java虚拟机(第二版)PDF

    《Java虚拟机(第二版)》是一本深入探讨Java虚拟机(JVM)技术的权威著作。这本书在第一版的基础上进行了大量的扩充和完善,为读者提供了更全面、更深入的JVM理解与应用知识。 首先,Java虚拟机是Java平台的核心...

    JVM.rar_java 工作流_java 虚拟机_jvm_jvm hook_虚拟机 Java

    Java虚拟机(JVM)是Java程序运行的基础,它是一个抽象的计算机系统,负责执行Java字节码。在深入理解JVM的工作流程之前,我们首先需要知道Java程序是如何被编译和运行的。Java源代码(.java文件)通过Java编译器...

    Java虚拟机规范(中文版) Java SE 7

    这些区域在Java虚拟机启动时会被初始化,并且有着各自特定的用途和生命周期。 - **类文件格式**:Java虚拟机执行的是字节码,而这些字节码存储在.class文件中。规范详细定义了.class文件的格式,包括常量池、字段表...

    java 虚拟机参数配置说明及Myeclipse内存不足

    在IT行业中,Java虚拟机(JVM)是Java程序运行的核心,它负责解析并执行Java字节码。本文将深入探讨Java虚拟机的参数配置,特别是针对MyEclipse开发环境中遇到的内存不足问题。 首先,Java虚拟机参数配置是优化JVM...

    JVM(Java虚拟机)详解.pdf

    JVM(Java虚拟机)详解 一、JVM 概念 JVM(Java虚拟机)是一个抽象的计算模型,提供了一个运行环境,能够运行 Java 字节码。JVM 可以解读指令代码并与底层进行交互,包括操作系统平台和执行指令并管理资源的硬件...

    JVM虚拟机,经典java虚拟机

    Java虚拟机(JVM)是一种抽象的计算机器,它为运行Java程序提供了一个平台无关的环境。JVM是Java平台的核心组成部分,负责将Java字节码转换为机器码执行。JVM的设计目标是使Java程序能在任何硬件和操作系统上运行,...

    jvm调优,java 虚拟机优化

    Java虚拟机(JVM)调优是提升Java应用程序性能的关键环节,主要涉及到内存管理、垃圾收集(GC)、线程调度等多个方面。JVM调优的目标是优化程序运行效率,减少不必要的系统资源消耗,特别是减少全GC(Full GC)的...

    javajvm虚拟机原理PPT课件.pptx

    Java虚拟机(Java Virtual Machine,JVM)是Java平台的核心组件之一,对Java程序的执行和管理起着至关重要的作用。下面是Java虚拟机的主要知识点: Java虚拟机生命周期 Java虚拟机的生命周期可分为三个主要阶段:...

    深入Java虚拟机——本地方法栈.pdf

    Java虚拟机(JVM)是Java程序运行的基础,它提供了执行环境和各种内存区域,以支持Java代码的高效运行。本地方法栈是JVM的一部分,它主要负责处理与本地方法(通常是由C或C++编写)相关的调用。本地方法栈在Java线程...

    Java虚拟机规范PDF

    由于无法查看实际的图片和OCR扫描文字内容,我将基于标题和描述提供关于Java虚拟机(JVM)规范的详细知识点。 ### Java虚拟机规范知识点 #### 1. JVM概述 Java虚拟机是运行所有Java程序的抽象计算机,它遵循一定的...

    JVM(Java 虚拟机)的详细讲解

    ### JVM(Java虚拟机)的详细讲解 #### Java运行原理概览 Java作为一种跨平台的编程语言,其核心优势在于“一次编写,到处运行”的特性。实现这一目标的关键技术是Java虚拟机(JVM)。简单来说,Java源代码在编译后...

    java虚拟机wince版

    Java虚拟机(JVM)是Java编程语言的核心组成部分,它允许开发者编写一次,到处运行的代码。在Windows CE(简称Wince)操作系统上运行Java应用,就需要一个专门为这个平台优化的JVM。"Java虚拟机wince版"指的是专门为...

Global site tag (gtag.js) - Google Analytics