文章目录
虚拟机类加载机制
JVM系列文章是基于:《深入理解Java虚拟机:JVM高级特性与最佳实践》-周志明第二版
- 类加载机制:将描述类的数据加载到内存并转换为Java中的Class对象的过程。
- Java中类的加载、连接、初始化是在程序运行期间完成的,这让Java更加灵活并可以动态扩展。
一、类加载过程和时机
1.1 加载时机
- JVM规范规定了一些立即对类进行初始化的场景,部分如下:
1.new、getstatic、putstatic、invokestatic:对应的场景是new实例、读写静态变量字段、
2.反射调用一个类时
3.初始化时,需要先初始化其父类
1.2 加载过程
- 下图是类的加载过程,需要注意的是解析阶段并不总是按照图中的顺序,有可能在初始化之后才解析
1.3 加载细节
- 加载:
1.通过类的全限定名称获取此类的二进制字节流
2.将字节流所代表的静态存储结构转化为方法区运行时数据结构
3.在内存中生成一个代表该类的Class对象
- 验证:文件格式(版本号),元数据(父类,抽象类等验证),字节码验证(验证程序语义合法,比如int形参不能传入long类型实参)
- 准备:类变量赋值,并设置初始值,比如将静态的int赋值为0(而不是用户写的一个值,比如1)
- 解析:符号引用替换为直接引用,类方法或者接口方法解析
- 初始化:初始化类变量
二、类加载器
2.1 类和类加载器
- 类加载器用于完成类加载的动作。一个类需要加载它的类加载器和它自己一同来唯一确认,因此一个类加载器相当于一个命名空间,不同的类加载器加载的一个Class文件,也是不一样的类,这里一样的类的判断标准是:instanceof,或者Class的equals方法,inInstance方法等。
2.2 双亲委派模型
2.2.1 类加载器分类
- 启动类加载器(Bootstrap ClassLoader):C++实现,加载lib下面的类库,比如rt.jar。
- 扩展类加载器(Extension ClassLoader):加载lib/ext下面的类库,由Sun的ExtClassLoader实现
- 应用程序类加载器(Application ClassLoader):也称系统类加载器,因为它是ClassLoader.getSystemClassLoader()的返回值,加载classpath指定的类库,由Sun的AppClassLoader实现的
- 自定义类加载器:自定义的类加载器继承自ClassLoader,并覆盖findClass方法,它的作用是将特殊用途的类加载到内存中。
2.2.2 为什么需要双亲委派?
- 它的好处可以用一句话总结,即防止内存中出现多份同样的字节码,保证核心类库的唯一性。
- 另外也可以避免重复加载,提高性能
比如Object类,在双亲委派模式下无论哪一个类加载器去加载,最终都是通过启动类加载器加载的Object,保证了唯一性。如果没有双亲委
派模型的保证,用户自定义一个java.lang.Object,使用用户自定义的加载器去加载,就会导致系统中存在多个java.lang.Object,
出现混乱,如果出现一个类加载多次的情况,那么可能会出现而已篡改一些类库,比如篡改了java.lang.Object然后加载,引发核心API被
篡改的问题。
但是经过双亲委派模型,即保证了唯一性,同时对于java.lang这样的包名,比如用户自定义了一个java.lang.XX类,交给启动类加载器肯
定加载失败(无法加载),最后交给用户的类加载器加载,但是会抛出异常,这样也可以限制用户用一些和系统冲突的包名。具体可以参考本文
第七小节。
java.lang.SecurityException: Prohibited package name: java.lang
2.2.3 为什么需要多个类加载器,而不是一个?
- 加载类的方法有无数种,从文件,网络的方式等,这样设计更加灵活,可扩展性好。
- 另一个原因是扩展性:通过这种方式支持自定义类加载器,可以实现一些加密,动态修改字节码等需求,包括后面自定义类加载器所带来的很多技术,比如字节码加密,热部署等都是基于这种灵活的设计方式,如果设计成只有一个类加载器,那么这些扩展性、灵活性都不能很好地提供了。
2.3 类加载器的关系
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器肯定为AppClassLoader。
三、ClassLoader代码分析
- 下图是继承关系,由此可以看到扩展类加载器和应用程序类加载器都是继承自ClassLoader(抽象类),准确的说是继承自其子类URLClassLoader(非抽象类)。
- 由此我们可以看到,除了启动类加载器之外(C++编写),其他的类加载其都是继承自抽象类ClassLoader。
3.1 loadClass方法
- loadClass()方法在ClassLoader中已经实现好了,方法中实现了双亲委派的逻辑,在实现自定义的类加载器的时候,如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象,这里的this.getClass().getClassLoder得到的是Launcher$AppClassLoader。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//1.检查是否已经被加载,如果加载过就不需要再加载了,这里面是一个native方法
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2.如果有父类加载器,就使用父类加载器加载(注意扩展类加载器就没有父类加载器)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//3.如果没有父类加载器,就使用启动类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//4.到这里说明双亲委派过程,父类加载器不能加载,因此需要自己来加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//5.自定义的加载逻辑在该方法中实现,ClassLoader中该方法是没有实现的
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//6.根据参数,决定是否解析该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
3.2 findClass()方法
- 在实现自定义的类加载器时,把自定义的类加载逻辑写在findClass()方法中,在ClassLoader中并没有实现该方法。
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
3.3 defineClass()方法
- 作用:defineClass()将byte字节流解析成JVM能够识别的Class对象,在ClassLoader中已实现该方法逻辑
- defineClass()不仅能通过class文件实例化class对象,也可通过其他方式实例化class对象,比如通过网络接收一个类的字节码然后转换为byte字节流创建对应的Class对象
- 注意:defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
- 另外需要注意的是defineClass()得到的Class文件是还没有解析过的,需要调用resolveClass解析,这个方法在前面的loadClass中看到过
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError {
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
3.4 resolveClass()方法
- resolveClass方法用于解析Class对象,方法在ClassLoader中都有实现,底层调用的是本地方法。
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
3.5 小结
- 前面的四个方法是类加载过程和自定义类加载器很关键的四个方法,作用如下:
方法名称 | 作用 |
---|---|
loadClass方法 | 加载类的主流程,实现双亲委派的逻辑,一般不需要重写,ClassLoader中已经实现好 |
findClass方法 | 完成类加载逻辑,交给子类实现,ClassLoader中没有实现 |
defineClass方法 | 将字节码转换为Class对象,ClassLoader中已经实现,这个方法通常提供给findClass使用 |
resolveClass方法 | 解析Class对象,ClassLoader中已经实现 |
- 从这里我们看到,在实现自定义类加载器的时候,主要是实现findClass方法,完成类的加载逻辑,在findClass中完成Class字节码的加载规则,加载字节码转换为流之后通过defineClass将流转换为Class文件,又必要通过resolveClass解析Class文件。
四、几个重要的类
4.1 ClassLoader
- ClassLoader是扩展类加载器、应用程序类加载器的父类,也是自定义类加载器的父类,它实现了大部分方法,子类只需要实现findClass完成加载规则即可,双亲委派逻辑、将流转换为Class对象以及解析Class对象都在ClassLoader中实现了,
4.2 SercureClassLoader
- SercureClassLoader扩展了ClassLoader,新增了和权限相关的功能,一般不会直接继承它。
4.3 URLClassLoader
-
ClassLoader是抽象类,很多方法没有实现,比如findClass()、findResource()等。而URLClassLoader是URLClassLoader子类并实现了很多方法。在编写自定义类加载器时如果没有太过于复杂的需求可以直接继承URLClassLoader类,可以避免实现findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
-
URLClassLoader中存在一个URLClassPath类,通过这个类就可以找到要加载的字节码流。URLClassPath类负责找到要加载的字节码读取成字节流,通过defineClass()创建Class对象。URLClassLoader构造方法需要传递一个参数URL[]代表字节码文件的路径,该参数指定类加载器的到哪个目录下找class文件。同时URL[]也是URLClassPath类的必传参数,URLClassPath根据传递过来的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认Loader类去加载相应路径下的class文件,而当JVM调用findClass()方法时,就由这3个加载器中的一个将class文件的字节码流加载到内存中,最后利用字节码流创建类的class对象。
-
如果继承ClassLoader实现自定义类加载器需要实现findclass()方法的加载逻辑以及获取字节码流的逻辑。
-
拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都是继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的,他们之间的关系在前面的类结构图已有展示。
4.4 ExtClassLoader和AppClassLoader
- 拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都是继承自URLClassLoader,ExtClassLoader并没有重写loadClass()方法,因此是继承自ClassLoader,这说明其遵循双亲委派模式,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。
五、关于自定义类加载器
5.1 为什么需要自定义类加载器?
- 自定义路径;加载不在ClassPath路径下的类文件,比如自定义网络类加载器。
- 加解密;加解密很可能需要使用到自定义的加解密逻辑,需要自定义类加载器。
- 热部署;重新加载修改后的字节码来实现动态替换类的功能。(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要自定义ClassLoader的逻辑。
默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生
成class对象。对于启动/扩展/应用程序 这三种类加载器,我们知道他们都是加载特点路径下面的类的。如果我们有一个类路径不在
前面几类加载器对应的路径那么就会无法加载,此时就需要自定义类加载器来寻找这样的字节码文件,比如网络类加载器。
这几个理由,也是为什么Java虚拟机不直接弄一个类加载器的原因,如果使用一个类加载器也可以做到类加载,但是肯定没有这么灵活。
5.2 如何自定义类加载器,其父加载器是什么?
- 可以继承ClassLoader,也可以继承URLClassLoader
- 自定义类加载器的父类加载器是:AppClassLoader。
5.3 自定义类加载器 – 热部署
- 因为在JVM中,是一个类加载器+一个ClASS字节码才能唯一确定一个类对象,如果修改类加载器,那么即使加载的是一个字节码对象,对于JVM而言也是不同的类。因此就可以在程序中,每一次都创建一个类加载器实例来加载一个字节码文件,这样就可以动态的在JVM中创建Class对象,而不需要重启程序,能够做到动态修改程序的功能。
- 这里创建一个新的类加载器实例来实现热部署,还有一个原因就是:一个确定的类加载器不会重复的去加载一个字节码文件,他会先检查缓存,重复加载会报错,因此我们每次加载需要一个新的类加载器实例,即使用不同的类加载器重复加载某一个Class字节码文件来实现热部署。这里从前面的方法分析中我们也可以绕过这个缓存的检查逻辑,直接调用findClass,因为缓存的查询逻辑实在loadClass中实现的
- 从下面代码可以看出,即使是一个类加载器加载的字节码文件,但是是不通的加载器实例,得到的Class对象也是不同的,热部署就是通过这种方式当字节码修改之后,不需要重启应用,新建一个类加载实例来加载对应的字节码。
private static void hotDeployTest() throws MalformedURLException {
String rootDir = "E:/";
//1.创建自定义文件类加载器
File file = new File(rootDir);
//2.File转换为URI
URI uri = file.toURI();
URL[] urls = {uri.toURL()};
//3.创建类加载器
FileUrlClassLoader loader1 = new FileUrlClassLoader(urls);
FileUrlClassLoader loader2 = new FileUrlClassLoader(urls);
try {
//4.加载指定的class文件
Class<?> aClass1 = loader1.loadClass("TestObj");
Class<?> aClass2 = loader2.loadClass("TestObj");
System.out.println(aClass1.hashCode() + " -- " + aClass2.hashCode());
System.out.println(aClass1.equals(aClass2));
} catch (Exception e) {
e.printStackTrace();
}
}
325040804 -- 1173230247
false
六、打破双亲委派模型
6.1 线程上下文类加载器
-
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的SPI有JDBC、JNDI等,这些 SPI的接口属于Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而SPI的第三方实现代码则是作为Java应用所依赖的jar包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
-
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例
-
DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,在DriverManager初始化时会执行如下代码,在加载DriverManager的时候是使用启动类加载器,但是启动类加载器是无法加载这些驱动实现类的,因为他们不在启动类所加载的路径下,因此在DriverManager的代码中,也就是启动类加载器加载的过程中,需要使用其他的类加载器类加载(这里使用ContextClassLoader,实际上得到的也是AppClassLoader),通过这种方式实现了Java核心代码内部去调用外部实现类。。
-
线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题。
private static void loadInitialDrivers() {
// ... 省略 ...
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
//通过classloader获得全部的驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//使用指定的类加载器去加载特定的驱动实现类,这里是使用线程上下文类加载器去加载
//DriverManager是在rt.jar中,启动类加载器是无法加载这些驱动实现类的,因为他们不在rt.jar中,而在第三方的库中,比如jdbc包
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
七、自定义java.lang.String?
- 有这样一个面试题,如果自定义一个java.lang.String,会怎么样?如果用自己写的一个类加载器去加载这样一个自定义的java.lang.String类又会怎么样?
7.1 加载自定义的java.lang.String
- 如下代码所示,我们使用自定义的加载器加载自己编译的java.lang.String类,然后我们打印出加载的类的构造方法,我们发现得到的是JDK里面的String,而并非我们自己定义的java.lang.String,这是为什么?
public class TestLoadString {
public static void main(String[] args) throws ClassNotFoundException {
//1.创建自定义文件类加载器
String path = "E:/";
FileClassLoader loader = new FileClassLoader(path);
try {
//2.加载指定的class文件
Class<?> aClass = loader.loadClass("java.lang.String");
//Class<?> aClass = loader.findClass("java.lang.String");
System.out.println(aClass.getName());
System.out.println(Arrays.toString(aClass.getDeclaredConstructors()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
java.lang.String
[public java.lang.String(byte[],int,int), public java.lang.String(byte[],java.nio.charset.Charset), public java.lang.String(byte[],java.lang.String) throws java.io.UnsupportedEncodingException, public java.lang.String(byte[],int,int,java.nio.charset.Charset), public java.lang.String(byte[],int,int,java.lang.String) throws java.io.UnsupportedEncodingException, java.lang.String(char[],boolean), public java.lang.String(java.lang.StringBuilder), public java.lang.String(java.lang.StringBuffer), public java.lang.String(byte[]), public java.lang.String(int[],int,int), public java.lang.String(), public java.lang.String(char[]), public java.lang.String(java.lang.String), public java.lang.String(char[],int,int), public java.lang.String(byte[],int), public java.lang.String(byte[],int,int,int)]
//下面是自己写的一个java.lang.String
package java.lang;
/**
* @author by mozping
* @Classname String
* @Description TODO
* @Date 2019/9/16 19:54
*/
public class String {
public int hashCode() {
return 1;
}
}
-
原因其实在loadClass这个方法里面,因为这个方法是在ClassLoader这个类里面定义的,前面我们说过这个方法里面是实现了双亲委派的逻辑的,我们自定义的加载器是ClassLoader的子类,因此我们使用的loadClass方法也是走的父类的逻辑,而父类的loadClass里面先回去缓存查询,查到了就直接返回了,而父类加载器里面查到的就是系统类加载器加载的JDK里面的String,因此这样验证了前面所提到的通过双亲委派模型来保证类的唯一性。
-
那么我们有没有办法强行的去加载,而不走双亲委派模型?当然有,因为在loadClass的双亲委派模型里面,会先去缓存查找,查不到缓存再使用父类加载器/启动类加载器去加载,如果加载失败,再调用子类的findClass去按照子类的加载逻辑去加载,因此我们可以直接使用findClass,就是前面代码中注释的那一行,执行后结果如下:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.intellif.mozping.ch06.clazzloader.FileClassLoader.findClass(FileClassLoader.java:38)
at com.intellif.mozping.ch06.clazzloader.test.TestLoadString.main(TestLoadString.java:23)
- 很容易看到,他提示我们禁止使用java.lang的包名,然后我依次测试了使用包名java.aa这种也是不行的,同样会报这个错。通过异常栈我们可以看到这个错在底层是defineClass1这个方法抛出来的,按照前面的几个方法分析,我们知道即使我们按照自己意愿在findClass中实现自己的加载逻辑,但是我们只不过得到一个流而已,而将流转换为Class对象,我们还是需要依靠defineClass这个方法,因此不管我们的文件流从何而来(网络或者文件等),我们最终然不过defineClass,而在这个方法里面会对包名做校验,因此我们自定义的java或者java.lang这样的包是会加载失败的。
//这是自定义的类加载器的findClass方法,里面只是按照自己的逻辑去加载文件流,还是需要通过父类的defindClass来将流转换为Class对象
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
//1.获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//2.通过defineClass生成class对象
return defineClass(null, classData, 0, classData.length);
}
}
//defindClass最终会走到这里,这是在ClassLoader里面的
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)throws ClassFormatError {
//1.包名检查
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
//2.这是核心的转换过程,不过是本地方法,和平台有关看不到源码
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd){
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException("Prohibited package name: " + name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
7.2 结论
- 我们自定义一个java.lang.String会怎么样?
1.如果我们直接在项目的包里面定义一个java.lang.String,不使用自定义的类加载器,那么是不会加载成功的,因为系统的类加载器通过双亲委派模型
会在缓存中检查,如果已经加载过java.lang.String,就不会再次加载了,因此我们自己写的是不会被加载的,在idea里面会提示方法不可用
2.如果我们自己强行写一个自己的类加载器去加载,那么按照约定通过loadClass方法加载,也是不会加载的,因为loadClass里面会走到父类ClassLoader的
loadClass方法,会检查到该类已经被加载,因此也不会被加载
3.如果我们自己强行写一个自己的类加载器去加载,并且尝试直接去finaClass,绕过双亲委派模型,那么很遗憾,我们会失败并抛出包名异常,因为我们
还是需要调用系统的defindClass来将流转换为Class对象,defindClass里面会校验包名,因此我们定义的包名以java开头的都会加载失败。
因此无论如何也是无法实现加载自己定义的java.lang.String的