内容简介:我们知道java要运行需要编译和运行,javac将java源代码编译为class文件。而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这就是JVM加载class文件到内存有两种方式:之前的我只知道在对象创建之前会先初始化静态的东西,也知道从父类开始初始化,但一直不懂为什么会是这样的顺序,直到我了解了虚拟机是如何实现类加载的。在开始真正了解类加载之前,我们先来看三个例子。
我们知道 java 要运行需要编译和运行,javac将java源代码编译为class文件。而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这就是 类加载机制 ,他在运行期间完成。
JVM加载class文件到内存有两种方式:
- 隐式加载:虚拟机自动加载需要的类
- 显式加载:代码中通过调用ClassLoader类来加载,例如Class.forName()、this.getClass.getClassLoader().loadClass()或者自己实现ClassLoader的findClass()
接下来先来看三个例子
之前的我只知道在对象创建之前会先初始化静态的东西,也知道从父类开始初始化,但一直不懂为什么会是这样的顺序,直到我了解了虚拟机是如何实现类加载的。在开始真正了解类加载之前,我们先来看三个例子。
第一个
class SuperClass { static{ System.out.println("SuperClass Init"); } public static int value = 123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass Init"); } } public class NotInitialization{ public static void main(String agrs[]){ System.out.println(SubClass.value); } } 复制代码
输出:
SuperClass Init 123 复制代码
这道例子似乎很简单,他告诉我们对于 静态字段,只有直接定义这个字段的类才会被初始化 ,所以,即使这里是通过子类来引用父类的静态属性,他也不会使子类发生初始化,而至于加载和验证,虚拟机并没有明确规范,各步骤的作用下文会谈
第二个
class SuperClass { static{ System.out.println("SuperClass Init"); } public static int value = 123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass Init"); } } public class NotInitialization{ public static void main(String agrs[]){ SuperClass[] sca = new SuperClass[10]; } } 复制代码
输出:
//无输出 复制代码
是的,运行之后并没有输出,但他触发了一个叫“[Lorg.fenixsoft.classloading.SuperClass”的类初始化,而 创建动作由字节码指令newarray触发 ,从这里,我们也就直到创建一个对象数组的真实情况了
第三个
class ConstClass{ static{ System.out.println("ConstClass init"); } public static final String WORD = "Hello"; } public class NotInitialization{ public static void main(String agrs[]){ System.out.println(ConstClass.WORD); } } 复制代码
输出:
Hello 复制代码
这里WORD作为一个常量,他在编译阶段就已经生成,意思是说编译阶段经过常量传播优化,已经将他存储到了NotInitialization类的常量池中,以后所有对它的引用都是NotInitialization对常量池的引用,这就是为什么不初始化类。
类初始化
下面来总结一下五种必须对类初始化的情况:
- 遇到new,getstatic,putstatic,invokestatic这四条字节码指令(后三者可以简单理解为对静态属性或方法的调用)
- 使用java.lang.reflect包的方法对类进行反射调用时
- 初始化类时的父类没有初始化时初始化父类
- 虚拟机启动时,用户需要执行的主类(main方法的那个类)
- JDK1.7动态语言支持时(类型检查在运行时而不是编译时,java.lang.invoke包,这里多说一句,动态语言和反射又有所不同)
以上,都是类第一次发生初始化的情况,而对于接口的初始化,他和类的不同就是只有在真正使用到父接口的时候才会初始化父接口。
类加载过程
下面来具体看一下类加载的全过程分别要做哪些事情
加载
这个时期需要完成三件事:
- 通过一个全限定名来获取一个此类的二进制流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口
这里,非数组类的加载阶段和数组类有些不同:
- 对于非数组类
- 加载阶段可以通过系统提供的引导类加载器完成,也可以由用户自定义的类加载器去完成,也可自己控制字节流的获取方式(重写一个类加载去的loadClass方法)
- 对于数组类
- 若数组组件类型是引用类型,数组在加载该组件类型的类加载器的类名称空间上被标识
- 若组件类型不是引用类型,将把数组标记为与引导类加载器关联
- 数组类的可见性和它的组件类型可见性一致,则默认为public
说直白加载的作用就是找到.class文件并把这个文件包含的字节码读取到内存中
验证
这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全,大概分为四部验证
- 文件格式验证:是否符合Class文件格式的规范,且能被当前版本虚拟机处理,保证输入的字节流能正确地解析并存储与方法区之内,格式上符合一个Java类型信息的要求
- 元数据验证:对类的元数据信息进行语义校验,保证在不符合Java语言规范的元数据信息
- 字节码验证:通过数据流和控制流分析,确定程序语义合法,符合逻辑
- 符号引用验证:确保解析动作正常执行,若无法通过符号引用验证,抛出java.lang.IncompatibleClassChangeError异常的子类
准备
为类变量分配内存并设置类变量初始化值,在方法区进行分配,如int为0,boolean为false,reference为null
解析
将常量池内的符号引用替换为直接引用的过程
问,什么是符号引用,什么是直接引用?
我的理解:
符号引用就是一个字符串,这个字符串有足够的信息可以找到相应的位置。直接引用就是偏移量,通过偏移量可以直接在内存区域找到方法字节码的起始位置。
解析主要包括对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这些符号引用进行
初始化
在类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化为默认值,初始化遵守下面几条原则(其中是类初始化的字节码指令)
- 静态初始化块中只能访问到定义在静态语句块之前的变量;定义在他之后的变量,在前面的静态语句块可以赋值,不能访问
- 虚拟机保证在父类的在子类的之前执行
- 虚拟机保证一个类的方法在多线程环境被正确的加锁、同步
下面来看几个例子
public class Test { static { i = 0; //System.out.println(i); } static int i; } 复制代码
上面注释的那一行会报错,因为在静态初始化块中只能访问到定义在静态语句块之前的变量;定义在他之后的变量,在前面的静态语句块可以赋值,不能访问,说明了第一条
public class Test { static class DeadLoopClass{ static{ if (true){ System.out.println(Thread.currentThread() + "init DeadLoopClass"); while(true){ } } } } public static void main(String agrs[]){ Runnable script = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + "run over"); } }; Thread t1 = new Thread(script); Thread t2 = new Thread(script); t1.start(); t2.start(); } } 复制代码
输出
Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-0,5,main]init DeadLoopClass 复制代码
他会打印上面的语句并会发生阻塞,这个例子说明了初始化的时候会保证类会被正确加锁
类加载器
接下来我们具体看一下类加载器有哪些特点,它的作用就是动态加载类到Java虚拟机的内存空间中,就是上文说的“通过一个类的全限定名来获取描述此类的二进制字节流”, 并且这个动作是放到Java虚拟机外部实现的,就是说应用程序自己决定如何去获取需要的类
类与类加载器
在JVM中标识两个class对象是否为同一个类对象存在两个必要条件
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
什么是类加载的动态性?
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销
双亲委派模型
类加载器可以大致分为三类:
- 启动类加载器(Bootstrap ClassLoader):这个加载器是C++写的,他在Java虚拟机启动后初始化,负责 加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类
- 扩展类加载器(Extension ClassLoader):由sum.misc.Launcher$ExtClassLoader实现,负责 加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库
- 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径上所指定的类库,父类为ExtensionClassLoader
- 自定义类加载器
那么什么是双亲委派模型呢?我们先来看一下他的工作过程。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
注意,这里叫双亲不是因为继承关系而是组合关系
双亲委派模型的好处
很容易想到,双亲委派模型的层级可以避免重复加载,尤其是java的核心类库不会被替换,例如自己定义了一个java.lang.Integer,双亲委派模型不会去初始化他,而是直接返回加载过的Integer.class。当然,如果强行用defineClass()方法(这个方法将byte字节流解析成JVM能够识别的Class对象)去加载java.lang开头的类也不会成功,会抛出安全异常
双亲委派模型代码实现
ClassLoader的loadClass(),只列出了关键的
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //首先,检查请求的类是否已经被加载过了 Class c = findLoadedClass(name); if (c == null){ try{ if (parent != null){ c = parent.loadClass(name,false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e){ //如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求 } if (c == null){ //在父类加载器无法加载的时候 //再调用本身的findClass方法来进行类加载 c = findClass(name); } } if (resolve){ //使用类的Class对象创建完成也同时被解析 resolveClass(c); } return c; } 复制代码
ClassLoader的findClass(),
//直接抛出异常 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 复制代码
ClassLoader的defineClass
protected Class<?> findClass(String name) throws ClassNotFoundException { //获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name,classData,0,classData.length); } } 复制代码
ClassLoader的resolveClass()
protected final void resolveClass(Class<?> c) { if (c == null) { throw new NullPointerException(); } } 复制代码
下面再来看一下关键方法的具体作用:
- loadClass():该方法加载指定名称(包括包名)的二进制类型,resolve参数代表加载同时是否需要被解析
- findClass():自定义的类加载逻辑写在findClass()方法中
- defineClass():用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑)
- resolveClass():该方法可以使用类的Class对象创建完成也同时被解析
先看以下loadClass()方法,通过以上代码可以看到逻辑并不复杂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass(),若父加载器为空让启动类加载器为父加载器,若父类加载失败,抛出异常,再调用自己的findClass()方法
在JDK1.2之后,如果我们自定义类加载器的话我们将不再重写loadClass(),因为ClassLoader已经实现loadClass(),并且用它来达到双亲委派的效果。我们自定义类加载器需要重写的是findClass(),知道findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
破坏双亲委派模型
双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的情况我们就要破坏这种机制,双亲委派模型主要出现过三次被破坏的情况
第一次:
因为双亲委派模型是在JDK1.2的时候出现的,所以,在JDK1.2之前,是没有双亲委派的,为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的protected的findClass()方法,这个方法的唯一逻辑就是调用自己的loadClass(),前文分析代码实现的时候我们知道双亲委派模型就是根据loadClass()来实现的,所以为了使用双亲委派模型,我们应当把自己的类加载逻辑写道findClass()中。
第二次:
我们有一些功能是java提供接口,而其他的公司提供实现类,例如我们的JDBC、JNDI(由多个公司提供自己的实现)所以像JDBC、JNDI这样的SPI(服务提供者接口),就需要第三方实现,这些SPI的接口属于核心库,由Bootstrap类加载器加载,那么如何去加载那些公司提供的实现类呢?这就是我们的 线程上下文类加载器 ,下图是整体大概的工作流程
这里,线程上下文加载器默认是父类加载器是ApplicationClassLoader
第三次:
第三次破坏委派双亲模型就是由于用户追求 动态性 导致的,“动态性”就是指代码 热替换、模块热部署 等,就是希望程序不需要重启就可以更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每一个程序模块(OSGi中称为Bundle)都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉实现热部署
所以,在OSGi环境下,类加载器不再是层次模型,而是网状模型,如图
当OSGi收到一个类加载的时候会按照以下的顺序进行搜索:
- 将以java.*开头的类委派给父类加载器加载
- 否则,将委派列表名单内的类委派给父类加载器加载
- 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
- 检查Fragment Bundle中是否可以加载
- 查找Dynamic Import列表的Bundle
- 若以上都没有进行类加载,则加载失败
以上前两点仍符合双亲委派规则,其余都是平级类加载器查找
Tomcat的类加载器模式
前文我们了解了Java中类加载器的运行方式;但主流的Web服务器都会有自己的一套类加载器,为什么呢?因为对于服务器来说他要自己解决一些问题:
- 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离 。两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
- 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本可以互相共享 。例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费—— 这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险 。
- Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响 。Web容器也有用Java实现的,那么肯定不能把Web容器的类库和程序的类库弄混
- 支持jsp的web容器,要支持热部署 。我们知道运行jsp时实际上会先将jsp翻译成servlet,再编译为.class再在虚拟机运行起来再返回给客户端。而我们在编写jsp时,当tomcat服务器正在运行的时候,我们直接在jsp中修改代码时并不需要重启服务器,这就是达到了动态加载类的效果。
显然,如果Tomcat使用默认的类加载机制是无法满足上述要求的
- 无法加载两个相同类库的不同版本的,因为默认类加载只在乎权限定类名,第一条不行
- 可以实现
- 默认类加载只在乎权限定类明,所以第三条不行
- 前文我们说过,JVM确定是否为同一个类对象会要求类和类加载器都相同,默认的肯定不行,但我们可以想到当改变jsp代码的时候就改一次类加载器
接下来来看Tomcat的类加载器:
++一个WebAppClassLoader下可能还对应多个JspClassLoader++
再来说说Tomcat的目录结构:
- /common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
- /server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
- /shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
- /WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
再来看一下具体每个类加载器的加载流程:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
Tomcat 6.x把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用
现在我们再来看Tomcat时如何解决之前的四个问题的:
- 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离 :各个WebAppClassLoader实例之间相互隔离
- 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本可以互相共享 :可以放在Common或Shared目录下让这些程序共享
- Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响 :CatalinaClassLoader加载web服务器需要的类库,WebAppClassLoader只能得到SharedClassLoader的类库
- 支持jsp的web容器,要支持热部署 :每当改变jsp时,更新JasperClassLoader
问:前文说到如果我们服务器上有十个Spring组织的程序,我们可以把Spring放到Common或者Shared目录下共享,但Spring要进行类管理肯定要访问到用户程序,即访问到不在他加载范围的用户程序,这要怎么实现呢?
前文我们说过破坏委托模型,这里就是一个例子,可以采用线程上下文加载器,让父类加载器请求子类加载器完成加载类作用
常见加载类异常错误分析
ClassNotFoundException
这个错误是说当JVM加载指定文件的字节码到内存时,找不到相应的字节码。解决办法为在当前classpath目录下找有没有指定文件(this.getClass().getClassLoader().getResource("").toString()可以查看当前classpath)
NoClassDefFoundError
这种错误出现的情况就是使用了new关键字、属性引用某个类、继承某个接口或实现某个类或某个方法参数引用了某个类,这时虚拟机隐式加载这些类发现这些类不存在的异常。解决这个错误的办法就是确保每个类引用的类都在当前的classpath下面
UnsatisfiedLinkError
可能是在JVM启动的时候不小心在JVM中的某个lib删了
ClassCastException
无法转型,这个可能对于初学者来说会很常见(比如说我,哈哈),解决办法时转型前先用instanceof检查是不是目标类型再转换
ExceptionInInitializerError
这个异常是由于类加载过程中静态块初始化过程失败所导致的。由于它出现在负责启动程序的主线程中,因此你最好从主类中开始分析,这里说的主类是指你在命令行参数中指定的那个,或者说是你声明了public static void main(String args[])方法的那个类。这个异常很大可能会伴随NoClassDefFoundError,所以出现NoClassDefFoundError时我们先看ExceptionInInitializerError出现没。
自定义类加载器
接下来我们要自己写一个类加载器,在开始写之前,我们要知道为什么需要我们自己写类加载器呢?
- 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
- 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
- 可以定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。
下面我们开始自定义类加载器吧
自定义File类加载
package SelfClassLoader; import java.io.*; public class FileClassLoader extends ClassLoader { private String rootDir; public FileClassLoader(String rootDir){ this.rootDir = rootDir; } /** * 编写findClass方法的逻辑 * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null){ throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name,classData,0,classData.length); } } /** * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return */ private byte[] getClassData(String className){ //读取类文件的字节 String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int bytesNumRead = 0; // 读取类文件的字节码 while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 类文件的完整路径 * @param className * @return */ private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } /** * 读取文件 */ public static void main(String[] args) throws ClassNotFoundException { String rootDir="C:\\java\\JVM\\JVMInstruction\\src"; //创建自定义文件类加载器 FileClassLoader loader = new FileClassLoader(rootDir); try { //加载指定的class文件,加上包名 Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } } 复制代码
我们通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。
还有一种方式是继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类,这种方式更常见
package SelfClassLoader; import java.io.File; import java.net.*; public class PathClassLoader extends URLClassLoader { private String packageName = "net.lijunfeng.classloader"; public PathClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } public PathClassLoader(URL[] urls) { super(urls); } public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(urls, parent, factory); } protected Class<?> findClass(String name) throws ClassNotFoundException{ Class<?> aClass = findLoadedClass(name); if (aClass != null){ return aClass; } if (!packageName.startsWith(name)){ return super.loadClass(name); } else { return findClass(name); } } public static void main(String[] args) throws ClassNotFoundException, MalformedURLException { String rootDir="C:\\java\\JVM\\JVMInstruction\\src"; //创建自定义文件类加载器 File file = new File(rootDir); //File to URI URI uri=file.toURI(); URL[] urls={uri.toURL()}; PathClassLoader loader = new PathClassLoader(urls); try { //加载指定的class文件 Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } } 复制代码
。
以上所述就是小编给大家介绍的《类加载机制总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。