0%

在Java虚拟机规范中规定了字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。下面主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。

1. 运行时栈帧结构

帧栈(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。帧栈存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

在概念模型上,典型的栈帧结构如下图所示:

jvm-class-loader-stack-frame

Read more »

1. 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initalization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。

jvm-class-loader-class-lifecycler

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

什么情况下需要开始类加载过程的第一个阶段:[加载]吗?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5中场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

Read more »

想一想为什么ruby,groovy等语言能够运行在jvm上?因为Java虚拟机不和包括Java在内的任何语言绑定,它只与 “Class文件” 这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介,虚拟机并不关心Class的来源是何种语言。

Class文件对于Java虚拟机如此重要,所以我们有必要详细的了解Class文件的结构。下面这张图形象的展示了其基本组织结构:

jvm-class-loader-basic-organization-structure-of-class-file

Class文件是一组以8位字节为基础单元的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表示有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “_info” 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count
Read more »

Java技术体系中所倡导的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先的TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

1. 对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

虚拟机提供了-XX:PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。在实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package gc;

public class TestGC {

private static final int _1MB = 1024 * 1024;

public static void main(String[] args) {
testAllocation();
}

/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*
* -Xms20M -Xmx20M -Xmn10M 限制了Java堆大小为20M,不可扩展,其中10M分配给新生代,剩下的10M分配给老年代。
* -XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1。
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
}
Read more »

这里谈论的GC分类主要是理清楚在各分代中会发生什么GC以及其作用是什么。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

我们可以认为Major GC == Full GC,它们是一个概念,就是针对 [老年代/永久代] 进行GC。因为取名叫Full就会让人疑惑,到底会不会先Minor GC。事实上Full GC本身不会先进行Minor GC,我们可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Minor GC可以提高老年代GC的速度。比如老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC。

弄清楚了Full GC本意单纯就是针对老年代了之后,我们再进一步深入理解Full GC的含义。上篇文章介绍过,因为CMS主要可以分为initial mark(stop the world), concurrent mark, remark(stop the world), concurrent sweep几个阶段,其中initial mark和remark会stop the world。Full GC的次数和时间等同于老年代GC时 stop the world的次数和时间。


参考:

  1. 深入理解Java虚拟机(第2版)
  2. 聊聊JVM(四)深入理解Major GC, Full GC, CMS

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用。虚拟机所在的区域,则表示它是属于新生代收集器还是老年代收集器。

jvm-gc-garbage-collector

虽然我们是在对各个收集器进行比较,但并非为了挑选一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要更多解释就能证明:如果有一个放之四海皆准、任何场景下都适用的完美收集器存在,那么Hotspot虚拟机就没有必要实现那么多不同的收集器了。

1. Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器。这个收集器是一个单线程的收集器, 但它的单线程意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。 “Stop the world” 这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常的线程全部停掉,这对很多应用来说都是难以接受的。

从JDK1.3开始一直到JDK1.7,Hotspot虚拟机开发团队为消除或者减少工作线程因内存回收而导致停顿的努力一直在进行着,从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成果Garbage First(G1)收集器,我们看到一个越来越优秀、也越来越复杂的收集器出现,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除。寻找更优秀的垃圾收集器的工作仍在继续!

尽管如此,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。它也有着由于其他收集器的地方:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

Read more »

下面介绍4种收集算法,分别是标记-清除算法、复制算法、标记-整理算法、分代收集算法。这里更多的是偏思想和算法的,理论性稍强。

1. 标记-清除算法

该算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

jvm-gc-mark-clear

2. 复制算法

为了解决效率问题,复制算法应运而生,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

不过这种算法的代价是将内存缩小为原来的一般,未免太高了一点。

jvm-gc-copy

Read more »

“泛型”这个术语的意思是:“适用于许多许多的类型”,泛型在编程语言中出现时,其最初的目的是希望类或方法能够具备最广泛的表达能力。如何做到这一点呢,正是通过解耦类或方法与所使用的类型之间的约束。而落实到Java中的泛型并没有这么高的最求,因为Java中的泛型在其他编程语言看来(例如C++)是一种“伪泛型”,在Java中,泛型是这门语言首次发布大约10年之后才引入的,因此向泛型迁移的问题特别多,并且对泛型的设计产生了明显的影响。而这一切都是由于Java设计者在设计1.0版本时所表现出来的短视造成的。

了解了这些背景之后,再看看Java中没有泛型之前,从集合中读取到的每一个对象都必须经过转换,如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型之后,可以告诉编译器每个集合中接受哪些对象类型。编译器自动地为你的插入进行转换,并在编译时告知是否插入了类型错误的对象。JDK1.5发行版本增加了泛型,主要用于解决集合框架的安全问题。泛型是一个类型安全机制,将运行时期出现的ClassCastException问题转移到编译时期,避免了强制转换的麻烦。

1. 原始类型和泛型类型

你需要知道怎么区分原始类型和泛型变量的类型:

  • 在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
  • 在不指定泛型的情况下,泛型变量的类型为该方法中几种类型的同一个父类的最小级,直到Object。
  • 在指定泛型的情况下,该方法中的几种类型必须是该泛型实例类型或者其子类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.leeeyou.test.basic;

/**
* Created by leeeyou on 2017/4/14.
*/
public class TestGeneric1 {
public static void main(String[] args) {
/**不指定泛型的时候*/
int i = TestGeneric1.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = TestGeneric1.add(1, 1.2);//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
Object o = TestGeneric1.add(1, "asd");//这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object

/**指定泛型的时候*/
int a = TestGeneric1.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
int b = TestGeneric1.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float
Number c = TestGeneric1.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}

//这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
}

2. Java中泛型的特点

2.1. 擦除机制

Java中的泛型 基本上都是在编译器这个层次来实现的, 在生成的Java字节码中是不包含泛型的类型信息,使用泛型时加上的类型参数会在编译期间去掉,这个过程就称为类型擦除。

擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会突然间破坏所有现有的代码。

而擦除的代价是显著的,泛型不能用于显示地引用运行时类型的操作之中,例如转型、instanceOf操作和new表达式。因为所有关于参数的类型信息都丢失了,作为开发人员我们需要提醒自己,你只是看起来好像拥有有关参数的类型信息而已。

擦除的整个过程也比较容易理解:首先找到用来替换类型参数的原始类型,这个具体类不指明则默认Object,如果指定了参数类型的上界,那么就使用这个上界来替换类型参数。同时去掉类型声明,即去掉<>的内容,比如T get()方法声明就变成Object get(),List< String >就变成了List。

2.2. 不可具体化的类型

运行时包含的信息比它的编译时包含的信息更少。

2.3. 通配符

类型通配符一般是使用 ? 代替具体的类型实参,注意此处是类型实参,而不是类型形参

2.3.1. 有界通配符

1
2
3
4
5
6
7
private static void testLowerBoundsWildcards() {
List<? super Apple> appleList;
}

private static void testUpperBoundsWildcards() {
List<? extends Apple> appleList;
}

有界通配符描述的是引用了明确的类型,并不是意味着这个appleList将持有任何类型的Apple,它意味着“appleList引用某种还没有指定具体类型的Apple”。

2.3.1.1. 上界通配符

<? extends T> 表示包括T在内的任何T的子类。如下关系图所示:

java-generic-extend

List<? extends Apple> appleList; 这句中的appleList能接收红色区域描述的任意类型中的(某一种)水果列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    List<RedApple> redAppleList = new ArrayList<>();
redAppleList.add(new RedApple("红苹果1"));
redAppleList.add(new RedApple("红苹果2"));
redAppleList.add(new RedApple("红苹果3"));

// List<Apple> appleList = redAppleList; //error,这里明确规定要求是Apple类型,没有灵活性可言。?没有继承关系可言
List<? extends Apple> appleList = redAppleList;//利用通配符的灵活性,可以成功将 红苹果列表 赋值给 苹果列表
for (Fruit fruit : appleList) {
System.out.println(fruit.getName());
}
System.out.println();

List<GreenApple> greenApples = new ArrayList<>();
greenApples.add(new GreenApple("绿苹果1"));
greenApples.add(new GreenApple("绿苹果2"));
greenApples.add(new GreenApple("绿苹果3"));
greenApples.add(new GreenApple("绿苹果4"));
appleList = greenApples;//利用通配符,appleList 灵活的接收了 红苹果列表 和 绿苹果列表
for (Fruit fruit : appleList) {
System.out.println(fruit.getName());
}

2.3.1.2. 下界通配符

<? super T> 表示包括T在内的任何T的父类。如下关系图所示:

java-generic-super

List<? extends Apple> appleList; 这句中的appleList就能接收红色区域描述的任意类型中的(某一种)水果列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    List<Apple> apples = new ArrayList<>();
apples.add(new Apple("苹果1"));
apples.add(new Apple("苹果2"));
apples.add(new Apple("苹果3"));

List<? super Apple> appleList = apples;
for (Object object : appleList) {
System.out.println(((Apple) object).getName());
}
System.out.println();

List<Fruit> fruits = new ArrayList<>();
fruits.add(new Fruit("水果1"));
fruits.add(new Fruit("水果2"));
fruits.add(new Fruit("水果3"));
appleList = fruits;
for (Object object : appleList) {
System.out.println(((Fruit) object).getName());
}
System.out.println();

List<RedApple> redApples = new ArrayList<>();
redApples.add(new RedApple("红苹果1"));
redApples.add(new RedApple("红苹果2"));
redApples.add(new RedApple("红苹果3"));
// appleList = redApples;//error

有界通配符让Java不同泛型之间的转换更容易了,但这样的转换带来的副作用就是容器的部分功能失效。其直观的影响就是:

  • 上界<? extends Apple>:不能往里存,只能往外取。
    • 原因是编译器只知道容器内是Apple或者它的派生类,但具体是什么类型不知道,可能是红苹果、绿苹果、蛇果、小苹果。反过来想如果编译器让通过编译,那么取出来的类型到底是哪个具体种类的苹果呢?不得而知。所以这里不允许往里存。
  • 下界<? super Apple>:不影响往里存,但往外取只能放在Object对象里。
    • 原因是下界规定了元素最小粒度的下限(这里最少是一个苹果),实际上是放松了容器元素的类型控制。既然元素是Apple的基类,那往里存粒度比Apple小的都可以。由于擦除的特性,元素的类型信息全部丢失,只能用Object对象来接收它。

2.3.1.3. PECS原则

如果参数化类型表示一个生产者T(Producer),即你想从列表中 读取 T类型的元素,就使用<? extends T>,比如List<? extends Apple>,因此你不能往该列表中添加任何元素。

如果参数化类型表示一个消费者(Consumer),即你想把T类型的元素 添加 到列表中,则使用<? super T>,比如List<? super Apple>,因此你不能保证从中读取到的元素的类型。

这一点对于设计出优秀强大的泛型代码是一个不错的原则! 参考java.util.Collections里的copy方法,我们可以从Java开发团队的代码中获得到一些启发,copy方法中使用到了PECS原则,实现了对参数的保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Copies all of the elements from one list into another. After the
* operation, the index of each copied element in the destination list
* will be identical to its index in the source list. The destination
* list must be at least as long as the source list. If it is longer, the
* remaining elements in the destination list are unaffected. <p>
*
* This method runs in linear time.
*
* @param <T> the class of the objects in the lists
* @param dest The destination list.
* @param src The source list.
* @throws IndexOutOfBoundsException if the destination list is too small
* to contain the entire source List.
* @throws UnsupportedOperationException if the destination list's
* list-iterator does not support the <tt>set</tt> operation.
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}

2.3.2. 无界通配符 ?

初看起来好像 ? 和 Object 的区别不大(实际情况也是会被擦除为Object),但是如果你声明List<?>,那么将不可以插入任何值(除了null,但是这个没有什么意义)。这里的 ? 是告诉编译器,我不知道或者不关心将会接受什么类型,但是请使用Java泛型机制来处理它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.leeeyou.test.basic;

import java.util.ArrayList;
import java.util.List;

/**
* Created by leeeyou on 2017/4/14.
*/
public class TestGeneric2 {
public static void main(String[] args) {
List<?> data = new ArrayList<>();
data.add("");//编译报错
data.add(null);//编译通过

List<String> stringList = new ArrayList<>();
List<Object> objList = new ArrayList<>();

objList = stringList;//编译报错

List<?> resultList = new ArrayList<>();
resultList = stringList;////编译通过
}
}

List<String>能否转为List<Object>:不能,关键原因在于擦除,由于List<Object>中的类都是被Object替代,而List<String>中均被String替换。理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把List<Object>改成List<?>即可。List<String>是List<?>的子类型,因此传递参数时不会发生错误。类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

无界通配符的一个重要作用就是当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.HashMap;
import java.util.Map;

/**
* Created by leeeyou on 2018/8/24.
*/
public class UnboundedWildcard {

static Map map1;
static Map<?, ?> map2;
static Map<String, ?> map3;

static void assign1(Map map) {
map1 = map;
}

static void assign2(Map<?, ?> map) {
map2 = map;
}

static void assign3(Map<String, ?> map) {
map3 = map;
}

public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
assign3(new HashMap());//Unchecked assignment: 'java.util.HashMap' to 'java.util.Map<java.lang.String,?>'

assign1(new HashMap<String, Integer>());
assign2(new HashMap<String, Integer>());
assign3(new HashMap<String, Integer>());
}
}

2.3.3. 泛型中参数化类型为什么不考虑继承关系

先看下面两行代码:

1
2
ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误
ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误

我们先看第一种情况,将第一种情况拓展成下面的形式:

1
2
3
4
ArrayList<Object> arrayList1=new ArrayList<Object>();  
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2=arrayList1;//编译错误

实际上在第4行代码的时候,就会有编译错误。现在我们先假设它编译没错,当我们使用arrayList2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会发生ClassCastException。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

再看第二种情况,将第二种情况拓展成下面的形式:

1
2
3
4
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add(new String());
arrayList1.add(new String());
ArrayList<Object> arrayList2=arrayList1;//编译错误

这样的情况比第一种情况看上去似乎好些,最起码在我们用arrayList2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是这样做有什么意义?泛型出现的原因就是为了解决类型转换的问题,将类型错误的检测移入到编译器。我们使用了泛型,到头来还是要自己强转,违背了泛型设计的初衷,所以JVM不允许这么干。另外你如果又用arrayList2往里面add()新的对象,那等到取得时候,编译器怎么知道取出来的到底是String类型的,还是Object类型的呢?要格外注意,泛型中的引用传递的问题。


参考:

  1. java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
  2. Java编程思想 (第4版)
  3. Effective java 中文版(第2版)
  4. Java语法–通配符的上界通配符和下界通配符

这篇主要解决一个问题:怎样确定对象是否还“活着”?《深入理解Java虚拟机》中给出了两种方式:引用计数算法和可达性分析算法。

引用计数算法是给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。客观地说,引用计数算法实现简单,判定效率也很高,也有一些著名的应用案例。但是主流的Java虚拟机没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

在主流的商用程序语言(Java、C#)的主流实现中,都是通过可达性分析来判定对象是否存活的。 这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象Object 5、Object 6、Object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

jvm-object-accessibility-analysis

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。


参考:

  1. 深入理解Java虚拟机(第2版)

Java程序通过栈上的reference数据来操作堆上的具体对象,由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。

句柄方式是通过在Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。句柄的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(GC时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。如下图所示:

jvm-object-access-object-by-handle

直接指针方式,reference中存储的直接就是对象地址,所以虚拟机设计者必须考虑如何放置访问类型数据的相关信息。直接指针的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多也是一项非常可观的执行成本。Hotspot虚拟机采用的是第二种方式进行对象访问,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。如下图所示:

jvm-object-direct-pointer-access-to-objects


参考:

  1. 深入理解Java虚拟机(第2版)