深刻理解Java虚拟机:JVM高级特性与最佳实践(第三版)周志明 读书笔记选取了书中部份内容,Java虚拟机更多的是一种规范,具体的Java虚拟机实现是有不少的。做者提到本文多数是以Hotspot虚拟机做为讲解。html
运行时数据区:计数器,Java虚拟机栈(局部变量表,操做数栈)方法区(类型信息,运行时常量池
等), Java堆,直接内存。可能产生内存溢出场景。虚拟机对象建立,内存布局与访问定位,前端
强引用,软引用,弱引用,虚引用,分代收集,并发可达性分析,标记-清除,标记-复制,标记-整理,java
Class类文件结构,字节码指令集程序员
加载,连接,初始化,类加载器,双亲委派模型算法
运行时栈结构,方法解析分派,动态类型语言支持,基于栈的字节码执行数据库
从Java文件到字节码阶段:Java注解处理器,泛型,装拆箱,插入式注解处理器编程
从字节码到机器码:AOT,JIT , Android Dalvik,Android ART后端
对于C/C++程序员来讲,担负每个对象生命从开始到终结的维护责任。而对于Java程序员来讲,Java帮助程序员自动管理内存,不须要写显式的代码去释放内存。但虚拟机不是万能的,一旦出现内存泄漏和溢出问题,若是不了解虚拟机怎样使用内存,将难以排查错误和修正问题。 本章从概念上介绍Java虚拟机内存的各个区域,及其可能产生的问题。数组
Java虚拟机在执行Java程序时,会将它管理的内存分红功能不一样的运行时数据区域。这些区域有着不一样的用户,不一样的建立和销毁时间。 有的区域随着虚拟机进程生命周期,有的区域则依赖用户线程的启动和结束而创建和销毁。根据《Java虚拟机规范》规定,Java虚拟机管理的内存包括如下运行时数据区。缓存
Java虚拟机基于栈的方式去执行程序。每个线程都会有相应的虚拟机栈,而虚拟机栈的栈帧对应于Java方法。
程序计数器(Progrom Counter Register),记录当前线程执行的字节码行号指示器。一般来讲,字节码解释器工做时就是经过改变计数器的值来选取下一条须要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都依赖计数器完成。每一个线程都有独立的计数器,互不影响。计数器分配在线程私有的内存空间中。此区域不会出现OOM的状况。
复制代码
Java虚拟机栈也是线程私有的,它的生命周期与线程相同,描述的是Java方法执行的线程内存模型,即每一个方法被执行时,都会同步建立一个栈帧(Stack Frame) 存储局部变量表,操做数栈,动态链接等。方法的调用与执行完毕,对应一个栈帧的入栈和出栈。
复制代码
在Java源码编译成字节码时,一个栈帧须要多大的局部变量表,须要多深的操做数栈就已经被分析计算出来,即编译事后就已经可以须要多大内存,内存取决于源码和具体的虚拟机栈内存布局形式。 在《Java虚拟机规范》中提到:若是线程请求的栈深度大于虚拟机所容许的深度,将会抛出StackOverflowError异常;若是虚拟机栈容量能够动态扩展,无限扩展,内存不足会抛出OutOfMemoryError异常。 Hotspot虚拟机栈容量不可动态扩展,但若是线程申请栈空间失败,仍然会OOM。
相较于Java虚拟机栈执行Java方法,本地方法栈执行Native方法。做用是类似的,也会有一样的异常问题。在HotSpot虚拟机中,本地方法栈与Java虚拟机栈合二为一。
复制代码
Java堆是全部线程共享的内存区域,在虚拟机启动时建立。用来存放对象实例。数组也是一种对象实例。
Java堆是垃圾收集器管理的内存区域。基于分代收集理论设计,多数虚拟机的堆内存能够分为新生代、老年代、永久代,Eden,Survivor等。随之垃圾收集器技术的发展,也出现了不采用分代设计的新垃圾收集器,那就不存在上述所谓的代划分。
复制代码
Java堆中,能够划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)。咱们说TLAB是现成独享的,但只是分配是独享的,读操做和垃圾回收等动做上是线程共享的。TLAB一般是在Eden区分配,由于Eden区自己不大,TLAB实际内存也很是小,默认占Eden空间的1%,因此必然存在一些大对象没法在TLAB直接分配。 不管怎么划分,都不会改变堆存放对象实例的做用。各类划分是为了更好的分配和回收内存。 《Java虚拟机规范》规定,逻辑上连续的内存空间,在物理上能够不连续。但多数虚拟机实现出于实现简单和存储高效,也会要求连续的物理内存空间。 主流Java虚拟机的堆内存空间都是可扩展的,但有上限值。当对象实例没法被分配内存,且堆达到上限。Java虚拟机便会抛出OutOfMemoryError异常。
方法区(Method Area),运行时常量池(Runtime Constant Pool)
复制代码
方法区,线程共享,存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。 JDK8,再也不使用永久代(Permanent Generation Space)实现方法区,而是在本地内存中实现的元空间(Metaspace)来代替。而字符串常量移到Java堆。这部分的内存回收目标主要针对常量池的回收和对类型的卸载。 运行时常量池,存放常量池表(Constant Pool Table),即Class文件编译期生成的各类字面量与符号引用。 根据《Java虚拟机规范》规定,若是方法区没法知足新的内存分配,将会抛出OutOfMemoryError异常。
直接内存(Direct Memory). NIO(New input/output)是 JDK1.4新加入的类,引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可使用Native函数直接分配堆外内存,而后经过堆上DirectByteBuffer对象对这块内存进行引用和操做。直接内存的大小不受JVM的限制,但一样可能会OutOfMemoryError异常。
以HotSpot为例,讲述在Java堆中对象的建立、结构和访问。
分配堆内存 根据内存是否规整,分为两种:指针碰撞(Bump The Pointer)和空闲列表(Free List).前者内存规整。 解决并发状况下的线程安全问题的两种方式
在《Java虚拟机规范》中规定,栈上的reference类型数据只是一个指向对象的引用。实际经过引用访问对象有两种方式:
模拟Java堆、虚拟机栈、本地方法栈、方法区、运行时常量池,本地直接内存的溢出。
/***Intellij IDEA 配置 Run Configgurations * VM options:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为同样) */
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list=new ArrayList<OOMObject>();
while (true){
list.add(new OOMObject());
}
}
}
复制代码
运行结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid58651.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [27770272 bytes in 0.159 secs]
复制代码
HotSpot不区分虚拟机栈和本地方法栈,须要设置 -Xss。 -Xoss(设置本地方法栈)没有效果。
复制代码
两种异常:
HotSpot栈内存不容许动态扩展,咱们使用-Xss参数减小栈内存容量。
// VM Args:-Xss160k
public class JavaVMStackOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
System.out.println("stack length:" + stackLength);
stackLeak();
}
public static void main(String[] args) throws Exception {
JavaVMStackOF oom = new JavaVMStackOF();
try {
oom.stackLeak();
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
复制代码
运行结果
...
stack length:754
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:9)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
at oom.JavaVMStackOF.stackLeak(JavaVMStackOF.java:10)
...
复制代码
在JDK6及之前的HotSpot中,常量池分配在永久代中,经过 -XX:PermSize 和 -XX:MaxPermSize限制永久代的大小。String::intern() 能够将一个字符串对象添加到常量池,若是常量池中不包此字符串对象。 在JDK6的HotSpot中
/*** VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M */
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
// 在short范围内足以让6MB的PermSize产生OOM了
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}
复制代码
运行结果,也验证了字符串常量在永久代中
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18
复制代码
因为字符串常量转移到了Java堆中, 因此在JDK7中设置 -XX:MaxPermSize, 或者在JDK8中设置 --XX:MaxMeta-spaceSize都不会出现JDK6中的溢出问题。但咱们能够限制最大堆内存空间-Xmx6m,从而产生OOM。
/*** VM Args:-Xmx6m */
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}
复制代码
运行结果,也验证了字符串常量在堆中
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:27)
复制代码
直接内存(Direct Memory)的容量大小可经过 -XX:MaxDirectMemorySize指定。
/*** VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {
public static void main(String[] args) throws Exception {
int count=1;
Field unsafeFiled=Unsafe.class.getDeclaredFields()[0];
unsafeFiled.setAccessible(true);
Unsafe unsafe= (Unsafe) unsafeFiled.get(null);
while (true){
unsafe.allocateMemory(1024*1024*1024);
System.out.println(count++);
}
}
}
复制代码
运行结果
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at oom.DirectMemoryOOM.main(DirectMemoryOOM.java:17)
复制代码
到目前为止,明白了虚拟机中内存的划分,以及出现内存溢出的场景。下一章将详细讲解Java垃圾收集机制如何避免内存溢出。
垃圾收集(Garbage Collection,GC),1960年麻省理工Lisp语言,使用动态内存分配和垃圾收集技术。 当Lisp胚胎时期,其做者John McCarthy就思考过GC须要完成的三件事情:
Java虚拟机内存运行时区间中,程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭。在编译器其大小基本肯定。而GC关注的是线程共享的区域 Java堆和方法区。
GC回收堆以前,如何断定哪些对象须要被回收,或者说这些对象已死?
可固定做为GC Roots的对象包括:
除固定外,还有临时性的其余对象等。
Java将引用分为四种:
要宣告一个对象死亡,至少要经历两次不可达标记过程。重载对象的finalize()方法,能够再第一次标记后执行,从新挂上引用链避免被回收,但finalize()只会被执行一次。
这里咱们讨论的是追踪式垃圾收集。
分代收集(Generational Collection)的理论假说基础:
绝大多数对象都是朝生夕灭的
熬过越屡次垃圾收集过程的对象越难以消亡
跨代引用相对同代引用来讲,仅占极少数
基于弱分代/强分代假说,通常Java虚拟机至少会把Java堆划分为
每次垃圾收集,新生代大量死去的对象会被回收,存活的对象将会逐步晋升到老年代中。大对象直接进入老年代。在新生代中创建一个全局的数据结构(记忆集,Remebered Set),把老年代分红多个小块,标识出某些块存在跨代引用。 针对不一样级别的分代的收集,咱们定义一下名词:
标记-清除(Mark-Swap) 1960 Lisp John McCarthy,基础性算法。 缺点:
1969 Fenichel "半区复制" SemisSpace Copying 解决内存碎片化和执行效率问题,缺点明显:浪费一半内存。
IBM研究发现98%的新生代对象熬不过第一轮收集,所以不用1:1分配内存 1989年 Andrew Appel ,提出更优化的半区复制分代策略。 能够看到始终有一个 Survivor(10%新生代内存)做为保留,用来存放回收后存活对象,若果Survivor空间不够,则须要老年代进行分配担保(Handle Promotion)
标记-复制算法适用于新生代,即大量对象会被回收,须要复制的对象不多。老年代对象存活率高,就不适用了。
标记-整理(Mark-Compact) 1974年 Edward Lueders。 移动式回收算法,标记-清除是非移动式的。 移动对象则回收时复杂,不移动对象则分配内存时复杂。
全部垃圾回收算法在根节点枚举时,都须要暂停用户线程,即 Stop The World! HotSpot采用准确式(Exact)垃圾回收,使用称为OopMap的数据结构,记录栈上本地变量到堆上对象的引用关系。 从而减小根节点枚举耗费的大量时间。 找出栈上的指针/引用 介绍了保守式,半自动式,准确式垃圾回收,同时也引出了OopMap。
根节点枚举须要暂停线程,总不能在每条指令后都去中断线程,因此有些固定的指令位置,做为中断的点,称之为 safe point。采用主动式中断,即达到安全点,检查是否要执行中断线程。 安全点的位置通常为:
safe region。安全区域指在某个代码片断中,引用关系不会发生变化,在这个区域内能够安全的开始垃圾收集。
RememberSet,记录非收集区到收集区的引用,避免把整个非收集区加入到GC Root扫描。好比说收集新生代对象时,避免整个老年代加入GCRoot扫描。
复制代码
从精度上来看,记忆集能够分为
卡表,即为常见的卡精度的记忆集。 卡表简单来讲,能够只是一个字节数组(Card Table)。每一个数组元素都对应一个卡页(Card Page),卡页是某块特定大小的内存块,通常来讲大小为2的N次幂字节数,HotSpot中为512字节。只要卡页中有对象存在跨代引用,则对应卡表元素标记为1,即元素变脏(Dirty)
什么时候去记录RememberSet? 写屏障(Write Barrier),虚拟机层面对"引用类型字段赋值"动做的AOP切面,虚拟机为赋值操做生成相应指令。 环形(Around)通知,提供写前屏障(Pre-Write Barrier)和写后屏障(Post-Write Barrier)。
假设处理器的缓存行大小为64字节,因为一个卡表元素占1个字节,64个卡表元素将共享同一个缓 存行。卡表在高并发下的伪共享(False Sharing)问题, 写脏前,先判断是否已脏。 在JDK 7以后,HotSpot虚拟机增长了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断
前面咱们经过OopMap、安全区域、RememberSet等手段,提高了根节点枚举的速度。根节点枚举带来的停顿已经至关短暂和固定了,而从GC Roots继续往下遍历对象的停顿时间与堆容量成正比。 可达性分析(标记)算法要求全过程在一个一致性的快照中分析,势必要冻结所有用户线程,且冻结的时间彻底不可控。在堆容量过大状况下,冻结时间是没法接受的。所以,可达性分析过程,若是能与用户线程并发执行,是最好不过了。 咱们先来看并发可达性分析过程 即三色标记 并发可达性分析又会引发两类问题:
当且仅当同时知足下面两个条件,会产生对象消失问题,即本来应当为黑色的对象被误标为白色(Wilson,1994年证实):
在介绍以前咱们先明确几个概念:
新生代,无并行,无并发,标记复制,简单高效,额外内存消耗(Memory Footprint)最小,适用于单核/少核,JDK1.3.1以前。
新生代,并行,Serial的多线程版本,标记复制, [JDK1.3 - JDK8)
新生代,并行 ,无并发,JDK1.4,标记复制,注重吞吐量
老年代,无并行,无并发,Serial老年代版本,标记整理
老年代,并行,无并发,Parallel Scavenge 老年代版本,标记整理,注重吞吐量
Concurent Mark Sweep, 老年代,并行,并发, [JDK5 - JDK8],标记清除,注重低延迟, 并发标记使用增量更新。四个步骤:
CMS的三个明显缺点:
Garbage First,JDK7完善, JDK9开始成为默认垃圾收集器。 新生代,老年代。Region分区,局部标记-复制,总体标记-整理,注重低延迟,并发标记使用原始快照。 大对象(内存超过Region内存的一半)直接进入 Humongous Region区域。Region是回收的最小单元。每个Region均可以根据须要扮演Eden空间,Survivor空间或者老年代空间。可预测时间停顿模型。 四个步骤:
G1是垃圾收集器技术发展历史上的里程碑式的结果,开创了面向局部手机的设计思路和基于Region的内存布局形式。从G1开始,垃圾收集器的设计导向变为追求应付内存分配速率(Allocation Tate),而不追求一次把整个Java堆清理干净。G1的更多介绍
略,实在不会
略,实在不会
代码编译的结果从本地机器码变为字节码,是存储格式发展的一小步,倒是编程语言发展的一大步。
程序语言 --> 字节码 --> 二进制本地机器码
无关性的基石 -- 字节码(Byte Code) 平台无关性,语言无关性
Class文件以8个字节为单位的二进制流,各数据项严格按照顺序紧凑排列在文件中,中间没有任何分隔符。 Class文件结构中只有两种数据类型:
类型 | 名称 | 数量 | 解释 |
---|---|---|---|
u4 | magic | 1 | 4字节魔数 0xCAFEBABE |
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 | 属性表集合 |
先来一段代码 TestClass.Java
package clazz;
public class TestClass {
public static void main(String[] args) { }
private int m;
public int inc(){return m+1;}
}
复制代码
经过编译获得二进制字节码文件 TestClass.class
javap -v TestClass.class获得字节码中包含的类信息。咱们如今要作的就是模拟javap这个解析的过程。
Last modified 2020-7-29; size 483 bytes
MD5 checksum ad62060802ee27c385e20042d24e8b38
Compiled from "TestClass.java"
public class clazz.TestClass minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#23 // clazz/TestClass.m:I
#3 = Class #24 // clazz/TestClass
#4 = Class #25 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lclazz/TestClass;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 inc
#19 = Utf8 ()I
#20 = Utf8 SourceFile
#21 = Utf8 TestClass.java
#22 = NameAndType #7:#8 // "<init>":()V
#23 = NameAndType #5:#6 // m:I
#24 = Utf8 clazz/TestClass
#25 = Utf8 java/lang/Object
{
public clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lclazz/TestClass;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lclazz/TestClass;
}
SourceFile: "TestClass.java"
复制代码
魔数(Magic Number) CAFEBABE ,表示这是一个Class类型文件,4字节。 minor version: 0(0x0000),2字节。 major version: 51 (0x0033),2字节。
接在魔数和版本后面的是常量池。 常量池中存放两大类型常量:
常量池中的常量有17种类型,好比说 CONSTANT_Methodref_info,CONSTANT_Classref_info,CONSTANT_Utf8_info等等。 每种类型常量的结构也不近相同。共同点是,都已u1的tag开头,表示类型。《深刻理解Java虚拟机》中,列出了完整的定义,下面简单举例。
CONSTANT_Methodref_info:
CONSTANT_Classref_info:
常量池项目数量:25 (0x001A是26,常量池索引值从1开始,0保留,因此实际只用25个常量,0能够理解成不引用常量池中的项目)。
第一项(0A 00 04 00 16), #1 = Methodref #4.#22:
值得注意的是 父类方法类型是Methodref,类方法inc类型是utf8。
第二项(09 00 03 00 17) , #2 = Fieldref #3.#23 // clazz/TestClass.m:I
剩余的23个项目,太多了,懂意思就行了。
结束了常量池25项解析,接着是Class访问标志(access_flags) 00 21 表示 0x0020 & 0x0001
方法数量,u2,0x0003,有3个方法 方法表结构
在方法表中,咱们遇到了一个属性"Code",还有其余属性。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | 由final关键字定义的常量值 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 记录类文件名称 |
... | ... | ... |
解析工做就不作了。
Java虚拟机指令,单字节操做码(OpCode)+操做数(Operand,零或多个)。面向操做数栈,而非寄存器。多数指令不包含操做数,指令参数放在操做数栈。 指令执行过程简易伪代码
do{
自动计算PC寄存器值加1;
根据PC寄存器指示位置,从字节码流中取出操做码;
if(字节码存在操做数) 从字节码流中取出操做数;
执行操做码所定义的操做;
}while(字节码流长度>0);
复制代码
大多数指令都包含其操做要求的数据类型,例如iload,从局部变量表中加载int型数据到操做数栈中。 i表明int,l表明long,f表明float,a表明reference。 也有些指令跟数据类型无关,好比 goto。
其余等等。。这章就了解下字节码构成和指令。
类加载机制:从Class文件到内存中Java类型的过程。各个阶段时间段上能够有重叠。 类加载是在运行期间执行的,也描述为动态加载和动态链接。
对于何时开始加载, 《Java虚拟机规范》没有强制约束。可是严格规定了有且只有六种状况,若是类没有初始化,必须当即对类进行"初始化" (加载、链接必然会先执行),称之为主动引用:
不会触发类初始化的几个场景举例:
//场景一 经过子类引用父类的静态字段,不会致使子类初始化
public class SuperClass {
static { System.out.println("SuperClass init!");}
public static int value=123;
}
public class SubClass extends SuperClass{
static { System.out.println("SubClass init!");}
}
public class NoInitialization1 {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
复制代码
//场景二 经过数组定义引用类,不会触发此类的初始化
public class NoInitialization2 {
public static void main(String[] args) {
SuperClass[] scarray=new SuperClass[10];
}
}
复制代码
//场景三 引用在编译期已被放入常量池的常量。
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWRLD="hello world";
}
public class NoInitialization3 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWRLD);
}
}
复制代码
对于场景三咱们查看NoInitialization3的字节码发现,“hello world”已经在其常量池中,使用 ldc指令将常量压入栈中。而System.out则是使用getstatic指令。这个地方不涉及到ConstClass的初始化
Constant pool:
#4 = String #25 // hello world
#25 = Utf8 hello world
{
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
复制代码
加载(loading):从静态文件到运行时方法区。完成三件事情:
使用Java虚拟机内置的引导类加载器,或者用户自定义的类加载器。
确保字节流符合《Java虚拟机规范》的约束,代码安全性问题验证。验证是重要的,但不是必须的。 四个阶段:
一般状况下,为类变量(静态变量),分配内存并设置初始值(零值)。初值并非代码中赋的值123。123要等到初始化阶段。
public static int value = 123;
复制代码
编译成class文件
public static int value;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
...
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
0: bipush 123
2: putstatic #2 // Field value:I
5: return
...
复制代码
某些状况下,设置初始值为456。好比final修饰的变量。由于变量值456,会提早加入到常量池。
public static final int value2 = 456;
复制代码
public static final int value2;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 456
复制代码
将常量池内的符号引用替换为直接引用的过程。 好比说这种,咱们要把 #2替换成实际的类引用,若是是未加载过的类引用,又会涉及到这个类加载过程。
getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
复制代码
执行类构造器()方法,非实例构造器()方法 。 ()方法:执行类变量赋值语句和静态语句块(static{})。顺序为其在源文件中顺序决定。 举例1:非法向前引用变量。 value的定义在 static{} 以后,只能赋值,不能读取值。
public class PrepareClass {
static {
value=3;
System.out.println(value);// value: illegal forword reference
}
public static int value=123;
}
复制代码
可是下面就能够
public class PrepareClass {
public static int value=123;
static {
value=3;
System.out.println(value);// value: illegal forword reference
}
}
复制代码
class文件参考
0: bipush 123
2: putstatic #2 // Field value:I
5: iconst_3
6: putstatic #2 // Field value:I
9: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
12: getstatic #2 // Field value:I
15: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
18: return
复制代码
举例2: ()执行顺序。子类初始化时,要先初始化父类
public class TestCInitClass2 {
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
复制代码
输出:
2
复制代码
Java虚拟机必须保证()方法在多线程环境下的同步问题。
实现“经过一个类的全限定名来获取其二进制字节流”的代码,称之为“类加载器”(Class Loader)。
类与其加载器肯定了这个类在Java虚拟机中的惟一性。
三层类加载器,绝大多数Java程序会用到如下三个系统提供的类加载器进行加载:
除了以上三个还有用户自定义的加载器,经过集成java.lang.ClassLoader类来实现。
加载Java的核心库,native代码实现,不继承java.lang.ClassLoader
URL[] urls= sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url);
}
结果输出:
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/resources.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/rt.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jsse.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jce.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/charsets.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/lib/jfr.jar
file:../jdk1.8.0_73.jdk/Contents/Home/jre/classes
复制代码
加载Java的扩展库,加载ext目录下的Java类
URL[] urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
复制代码
加载Java应用的类。经过ClassLoader.getSystemClassLoader()来获取。
URL[] urls= ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
结果输出:
file:/.../jdk1.8.0_73.jdk/Contents/Home/jre/lib/ext/sunec.jar
...
file:/.../jdk1.8.0_73.jdk/Contents/Home/lib/tools.jar
file:/.../java_sample/out/production/java_sample/ //这是咱们的应用程序
file:/Applications/IntelliJ%20IDEA.app/Contents/lib/idea_rt.jar
复制代码
ClassLoader.loadClass
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
复制代码
AppClassLoader,ExtClassLoader都继承URLClassLoader。 URLClassLoader.findClass(name)
protected Class<?> findClass(final String name)throws ClassNotFoundException {
// 一、安全检查
// 二、根据绝对路径把硬盘上class文件读入内存
byte[] raw = getBytes(name);
// 三、将二进制数据转换成class对象
return defineClass(raw);
}
复制代码
若是咱们本身去实现一个类加载器,基本上就是继承ClassLoader以后重写findClass方法,且在此方法的最后调包defineClass。 ** 双亲委派确保类的全局惟一性。 例如不管哪一个类加载器须要加载java.lang.Object,都会委托给最顶端的启动类加载器加载。
参考: 通俗易懂 启动类加载器、扩展类加载器、应用类加载器 深刻探讨 Java 类加载器
线程上下文类加载器(context class loader),能够从java.lang.Thread中获取。 双亲委派模型不能解决Java应用开发中遇到的全部类加载器问题。 例如,Java提供了不少服务提供者接口(Service Provider Interface,SPI),容许第三方提供接口实现。常见的SPI有JDBC,JCE,JNDI,JAXP等。SPI接口由核心库提供,由引导类加载器加载。 而其第三方实现,由应用类加载器实现。此时SPI就找不到具体的实现了。 SPI接口代码中使用线程上下文类加载器。线程上下文类加载器默认为应用类加载器。
虚拟机是相对于物理机的概念。 物理机的执行引擎是直接创建在处理器,缓存,指令集合操做系统底层上。 虚拟机的执行引擎是创建在软件之上,不受物理条件限制,定制指令集与执行引擎。 虚拟机实现中,执行过程能够是解释执行和编译执行,能够单独选择,或者混合使用。 但全部虚拟机引擎从统一外观(Facade)来讲,都是输入字节码二进制流,字节码解析执行,输出执行结果。
本章从概念角度讲解虚拟机的方法调用和字节码执行。
Java虚拟机以方法做为最基本的执行单元。每一个方法在执行时,都会有一个对应的栈帧(Stack Frame) .栈帧同时也是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。 一个栈帧须要多大的局部变量表,须要多深的操做数栈,早在编译成字节码时就写到了方发表的Code属性中。 Code: stack=2, locals=1, args_size=1
所以一个栈帧须要分配多少内存,在运行前就已肯定,取决于源码和虚拟机自身实现。
局部变量表容量最小单位为变量槽(Variable Slot), 《Java虚拟机规范》规定一个变量槽能够存放一个boolean,byte,char,init,float,reference或returnAddress类型的数据。32位系统能够是32位,64位系统能够是64位去实现一个变量槽。对于64位的数据类型(long和double),以高位对齐的方式分配两个连续的变量槽。 因为是线程私有,不管两个连续变量槽的读写是否为原子操做,都不会有线程安全问题。
当一个方法被调用时,会把参数值放到局部变量表中。类方法参数Slot从0开始。实例方法参数Slot从1开始,Slot0给了this,指向实例。 咱们比较类方法和实例方法的字节码。
public static int add(int a, int b) {return a + b;}
public int remove(int a, int b) {return a - b;}
复制代码
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 a I
0 4 1 b I public int remove(int, int);
flags: ACC_PUBLIC
Code:
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lexecute/Reerer;
0 4 1 a I
0 4 2 b I
复制代码
当变量的做用域小于整个方法体时,变量槽能够复用,为了节约栈内存空间。好比 {},if(){}等代码块内。变量槽复用会存在“轻微反作用”,内存回收问题。
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007701 secs]
[Full GC (System.gc()) 66040K->65934K(251392K), 0.0040938 secs] //解释: 虽然placeholder的做用域被限制,但gc时,局部变量表仍然引用placeholder,没法被回收。 复制代码
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int i=0;
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66040K(251392K), 0.0007556 secs]
[Full GC (System.gc()) 66040K->398K(251392K), 0.0044978 secs] //解释: 虽然placeholder的做用域被限制,int i=0复用了slot0,切断了局部变量表的引用placeholder。 复制代码
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
placeholder=null;
}
System.gc();
}
//执行结果
[GC (System.gc()) 69468K->66088K(251392K), 0.0022762 secs]
[Full GC (System.gc()) 66088K->398K(251392K), 0.0050265 secs] //解释 主动释放placeholder 复制代码
类变量在准备阶段,会被赋默认零值。而局部变量没有准备阶段。因此下面代码是编译不经过的,即使编译经过,在检验阶段,也会被发现,致使类加载失败。
public static void fun4(){
int a;
//编译失败,Variable ‘a’ might not have been initialized
System.out.println(a);
}
复制代码
操做数栈(Operand Stack) 字节码指令读取和写入操做数栈。操做数栈中元素的数据类型必须与指令序列严格匹配。编译阶段和类检验阶段都会去保证这个。 在大多数虚拟机实现中,上面栈帧的操做数栈与下面栈帧的局部变量会有一部分重叠,这样不只节约了空间,重要的是在方法调用时直接公用数据,无须而外的参数复制。
在类加载过程当中,会把符号引用解析为直接引用。方法调用指令以常量池中的符号引用为参数。这些方法符号引用一部分在类加载或者第一次使用时转化为直接引用,这种转化被称为静态解析。另一部分则须要在每次运行期间转化为直接引用,这部分称之为动态链接。
正常调用完成和异常调用完成。 恢复主调方法的执行状态。
Java虚拟机中的5条方法调用指令:
方法按照类加载阶段是否能转化成直接引用分类,能够分为:
非虚方法的调用称之为解析(Resolution),"编译器可知,运行期不可变",即类加载阶段把符号引用转化为直接引用。 而另一个方法调用的方式称之为分派(Dispatch)。
分派(Dispatch)是静态或者动态的,又或者是单分派或者多分派。重载或者重写会出现同名方法。同名方法的选择,我能够称之为分派
Method Overload Resolution, 这部份内容实际上叫作方法重载解析。静态分派发生在编译阶段。 先来看一段代码,sayHello方法重载。
//方法静态分派
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public static void sayHello(Man man){System.out.println("hello,gentleman!"); }
public static void sayHello(Human guy){ System.out.println("hello,guy!");}
public static void sayHello(Woman women){System.out.println("hello,lady!");}
public static void main(String[] args) {
Human man=new Man();
Human woman=new Woman();
StaticDispatch dispatch=new StaticDispatch();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
//执行结果:
hello,guy!
hello,guy!
复制代码
对应Class字节码
public static void main(java.lang.String[]);
Code:
stack=2, locals=3, args_size=1
0: new #7 // class execute/StaticDispatch$Man
3: dup
4: invokespecial #8 // Method execute/StaticDispatch$Man."<init>":()V
7: astore_1
8: new #9 // class execute/StaticDispatch$Woman
11: dup
12: invokespecial #10 // Method execute/StaticDispatch$Woman."<init>":()V
15: astore_2
16: new #11 // class execute/StaticDispatch
19: dup
20: invokespecial #12 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #13 // Method sayHello:(Lexecute/StaticDispatch$Human;)V
34: return
复制代码
第0~15行,咱们构建了Man对象和Woman对象,并放入了局部变量表中。 第26行,执行方法Method sayHello:(Lexecute/StaticDispatch Human;)V, 实际执行的都是sayHello(Human)。而不是sayHello(Man)或者sayHello(Woman)。 这里涉及到两个类型:
编译期并不知道对象的实际类型,因此按照对象的静态类型去分派方法。
与重写(Override)密切关联。动态分派发生在运行期间。在运行时,肯定方法的接收者(方法所属对象)
//方法动态分派
public class DynamicDispatch {
static abstract class Human {
public void sayHello() {System.out.println("hello,guy!");}
}
static class Man extends Human {
public void sayHello() {System.out.println("hello,gentleman!"); }
}
static class Woman extends Human {
public void sayHello() {System.out.println("hello,lady!");}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();//hello,gentleman!
woman.sayHello();//hello,lady!
man = new Woman();
man.sayHello();//hello,lady!
}
}
//执行结果:
hello,gentleman!
hello,lady!
hello,lady!
复制代码
对应字节码
0: new #2 // class execute/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method execute/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class execute/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
24: new #4 // class execute/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method execute/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method execute/DynamicDispatch$Human.sayHello:()V
36: return
复制代码
第7行,astore_1存储了Man对象 第15行,astore_2存储了Woman对象 第16,17行,aload_1,invokevirtual.实际调用的是Man.sayHello()方法 第20,21行,aload_2,invokevirtual.实际调用的是Woman.sayHello()方法 第31行,astore_1存储了Woman对象 第32,33行,aload_1,invokevirtual.实际调用的是Woman.sayHello()方法
运行期间,选择是根据man和woman对象的实际类型分派方法。
小知识:字段永远不参与多态,方法中访问的属性名始终是当前类的属性。子类会遮蔽父类的同名字段
方法的宗量:方法的接收者与方法的参数 单分派:基于一种宗量分派 多分派:基于多种宗量分派。 当前Java语言是一门静态多分派,动态单分派的语言。编译期根据方法接收者和参数肯定方法的符号引用。运行期根据方法的接收者,解析和执行符号引用。
考虑下面一段代码
public class Dispatch {
static class Father{
public void f() {System.out.println("father f void");}
public void f(int value) {System.out.println("father f int");}
}
static class Son extends Father{
public void f(int value) {System.out.println("Son f int"); }
public void f(char value) { System.out.println("Son f char");}
}
public static void main(String[] args) {
Father son=new Son();
son.f('a');
}
}
//执行结果: Son f int
复制代码
字节码
0: new #2 // class execute/Dispatch$Son
3: dup
4: invokespecial #3 // Method execute/Dispatch$Son."<init>":()V
7: astore_1
8: aload_1
9: bipush 97
11: invokevirtual #4 // Method execute/Dispatch$Father.f:(I)V
复制代码
首先是编译期的静态分派,先选择静态类型Father,因为Father中没有f(char),则会选择最合适的f(int),肯定方法为Father.f:(I)V。其次是运行期,接收者为Son,Son中有重写的f:(I)V。因此最终执行的是Son.f:(I)V
虚方法表,接口方法表,类型继承分析,守护内联,内联缓存
动态类型语言的关键特征:类型检查的主体过程是在运行期而不是编译器,好比说Groovy、JavaScript、Lisp、Lua、Python。 静态类型语言:编译器就进行类型检查,好比C++,Java。
Java虚拟机须要支持动态类型语言,因而在JDK7发布 invokedynamic指令。
略
略
略
略
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
复制代码
stack=2, locals=4, args_size=1
0: bipush 100 //常量100压入操做数栈顶
2: istore_1 //栈顶元素(100)存入变量槽1,同时消费掉栈顶元素
3: sipush 200 //常量200压入操做数栈顶
6: istore_2 //栈顶元素(200)存入变量槽2,同时消费掉栈顶元素
7: sipush 300 //常量300压入操做数栈顶
10: istore_3 //栈顶元素(300)存入变量槽3,同时消费掉栈顶元素
11: iload_1 //将局部变量slot1值100压入操做数栈顶
12: iload_2 //将局部变量slot2值200压入操做数栈顶
13: iadd //消费栈顶100和200,获得300,并压入栈顶
14: iload_3 //将局部变量slot3值300压入操做数栈顶
15: imul //消费栈顶300和300,获得90000,并压入栈顶
16: ireturn //消费栈顶90000,整型结果返回给方法调用者
复制代码
先明确几个概念 即时编译器(JIT编译器,Just In Time Compiler),运行期把字节码变成本地代码的过程。 提早编译器(AOT编译器,Ahead Of Time Compiler),直接把程序编译成与目标及其指令集相关的二进制代码的过程。
这里讨论的“前端编译器”,是指把*.java文件转换成*.class文件的过程,主要指的是javac编译器。
Javac编译器是由Java语言编写。分析Javac代码的整体结构来看,编译过程大体分为1个准备过程和3个处理过程。以下:
若是注解处理产生新的符号,又会再次进行解析填充过程。 Javac编译动做的入口com.sun.tools.javac.main.JavaCompiler类。代码逻辑主要所在方法compile(),compile2()
对应parserFiles()方法 词法分析:源码字符流转变为标记(Token)集合的过程。标记是编译时的最小元素。关键字、变量名、字面量、运算符都是能够做为标记。 如“int a = b + 2”, 包含了6个标记,int,a , =, b, +, 2 。词法分析由com.sun.tools.javac.parser.Scanner实现。 语法分析:根据标记序列构造抽象语法树的过程。抽象语法树(Abstract Syntax Tree,AST),描述代码语法结构的树形表示形式,树的每一个节点都表明一个语法结构,例如包,类型,运算符,接口,返回值等等。com.sun.tools.javac.parser.Parser实现。抽象语法树是以com.sun.tools.javac.tree.JCTree类表示。 后续的操做创建在抽象语法树之上。
对应enterTree()方法。
JDK6,JSR-269提案,“插入式注解处理器”API。提早至编译期对特定注解进行处理,能够理解成编译器插件,容许读取、修改、添加抽象语法树中的任意元素。若是产生改动,编译器将回到解析及填充符号表过程从新处理,直到不产生改动。每一次循环过程称为一个轮次(Round). 使用注解处理器能够作不少事情,譬如Lombok,能够经过注解自动生成getter/setter方法、空检查、产生equals()和hashCode()方法。
抽象语法树可以表示一个正确的源程序,但没法保证语义符合逻辑。语义分析的主要任务是进行类型检查、控制流检查、数据流检查等等。 例如
int a = 1;
boolean b = false;
char c = 2;
//后续可能出现的运算,都是能生成抽象语法树的,但只有第一条,能经过语义分析
int d= a + c;
int d= b + c;
char d= a + c;
复制代码
在IDE中看到的红线标注的错误提示,绝大部分来源于语义分析阶段的结果。
attribute()方法,检查变量使用前是否已被声明,变量与赋值的数据类型是否匹配等等。 3个变量的定义属于标注检查。标注检查顺便会进行极少许的一些优化,好比常量折叠(Constant Folding).
int a = 1 + 2; 实际会被折叠成字面量“3”
复制代码
flow()方法,上下文逻辑进一步验证,好比方法每条路径是否有返回值,数值操做类型是否合理等等。
语法糖(Syntactic Sugar),编程术语 Peter J.Landin。减小代码量,增长程序可读性。好比Java语言中的泛型(其余语言的泛型不必定是语法糖实现,好比C#泛型直接有CLR支持),变长数组,自动装箱拆箱等等。 解语法糖,编译期将糖语法转换成原始的基础语法。
JDK5,Java的泛型实现称为“类型擦除式泛型”(Type Erasure Generic),相对的C#选择的是“具现化泛型”(Reified Generics),C#泛型不管在源码中,仍是编译后的中间语言表示(此时泛型都是一个占位符),List 与List是两个不一样的类型。而Java泛型,只是在源码中存在,编译后都变成了统一的类型,称之为类型擦除,在使用处会增长一个强制类型转换的指令。
Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("hello", "你好");
System.out.println(stringMap.get("hello"));
Map objeMap = new HashMap();
objeMap.put("hello2", "你好2");
System.out.println((String)objeMap.get("hello2"));
复制代码
截取部分字节码
0: new #2 // class java/util/HashMap
4: invokespecial #3 // Method java/util/HashMap."<init>":()V
13: invokeinterface #6, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25: invokeinterface #8, 2 // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
30: checkcast #9 // class java/lang/String
33: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: new #2 // class java/util/HashMap
40: invokespecial #3 // Method java/util/HashMap."<init>":()V
49: invokeinterface #6, 3 // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
61: invokeinterface #8, 2 // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
66: checkcast #9 // class java/lang/String
69: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
72: return
复制代码
能够看到两部分代码在编译后是同样的。 第0行,new HashMap<String, String>() 实际构造的是java/util/HashMap。 第30行,stringMap.get("hello") ,checkcast指令,作了一个类型转换
2004年,Java5.0。为了保证代码的“二进制向后兼容”,引入泛型后,原先的代码必须可以编译和运行。例如Java数组支持协变,集合类也能够存入不一样类型元素。 代码以下
Object[] array = new String[10];
array[0] = 10; // 编译期不会有问题,运行时会报错
ArrayList things = new ArrayList();
things.add(Integer.valueOf(10)); //编译、运行时都不会报错
things.add("hello world");
复制代码
若是要保证Java5.0引入泛型后,上述代码依然能够运行,有两个选择:
为什么C#与Java的选择不一样,主要是C#当时才2年遗留老代码少,Java快10年了老代码多。类型擦除是偷懒留下的技术债。
类型擦除除了前面提到的编译后都变成了统一的裸类型以及使用时的类型检查和转换以外还有其余缺陷。 1)不支持原始类型(Primitive Type)数据泛型,ArrayList须要使用其对应引用类型ArrayList,致使了读写的装箱拆箱。 2)运行期没法获取泛型类型信息,例如
public <E> void doSomething(E item){
E[] array=new E[10]; //不合法,没法使用泛型建立数组
if(item instanceof E){}//不合法,没法对泛型进行实例判断
}
复制代码
当咱们去写一个List到数组的转换方法时,须要额外传递一个数组的组件类型
public static <T> T[] convert(List<T> list,Class<T> componentType){
T[] array= (T[]) Array.newInstance(componentType,list.size());
for (int i = 0; i < list.size(); i++) {
array[i]=list.get(i);
}
return array;
}
复制代码
3)类型转换问题。
//没法编译经过
//虽然String是Object的子类,但ArrayList<String>并非ArrayList<Object>的子类。
ArrayList<Object> list=new ArrayList<String>();
复制代码
为了支持协变和逆变,泛型引入了 extends ,super
//协变
ArrayList<? extends Object> list = new ArrayList<String>();
//逆变
ArrayList<? super String> list2 = new ArrayList<Object>();
复制代码
2014年,Oracle,Valhalla语言改进项目内容之一,新泛型实现方案
自动装箱,自动拆箱,遍历循环,变长参数,条件编译,内部类,枚举类,数值字面量,switch,try等等。
前面一章讲的是从*.java到*.class的过程,即源码到字节码的过程。 这一章讲的是从二进制字节码到目标机器码的过程,分为两种即时编译器和提早编译器。
目前主流的两款商用Java虚拟机(HotSpot、OpenJ9)里,Java程序最初都是经过解释器 (Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认 定为“热点代码”(Hot Spot Code),为了提升热点代码的执行效率,在运行时,虚拟机将会把这些代 码编译成本地机器码,并以各类手段尽量地进行代码优化,运行时完成这个任务的后端编译器被称 为即时编译器。
于程序运行的资源,
及须要一段时间预热后才能到达最高性能的问题。这种提早编译被称为动态提早编译(Dynamic AOT)或者索性就大大方方地直接叫即时编译缓存(JIT Caching)
Android虚拟机历程: Android4.4以前 Dalvik虚拟机 即便编译器 Android4.4开始 Art虚拟机 提早编译器,致使安装时须要编译App,很是耗时,但运行性能获得提高 Android7.0开始 从新启用解释执行和即时编译,系统空闲时间时自动进行提早编译。
略
介绍虚拟机如何实现多线程,多线程之间因为共享数据而致使的一系列问题及解决方案
介绍Java虚拟机内存模型前,先了解下物理机的并发问题。
Java虚拟机有本身的内存模型,也会有与物理机类型的问题。
Java内存模型规定:因此变量都存储在主内存(Main Memory)中,线程有本身的工做内存,工做内存保存变量在主内存副本。线程对变量的读写只能再工做内存(Working Memory)中,线程间共享变量须要经过主内存完成。 JVM内存模型的执行处理将围绕解决两个问题展开:
主内存与工做内存的交互协议定义以下操做,Java虚拟机必须保证这些操做是原子性的。
若是要把变量从主内存拷贝到工做内存,必须顺序执行 read和load,但不要求必定连续。 若是要把变量从工做内存同步到主内存,必须顺序执行 store和write,但不要求必定连续。
Java内存模型是围绕着在并发过程当中如何处理这3个特性来创建的,归根结底是为了实现共享变量在多个工做内存中的一致性,以及并发时,程序能如期运行。
happens-before
内存屏障是被插入到两个CPU指令之间的一种指令,用来禁止处理器指令发生指令重排序。
volatile主要有下面两种语义
保证了不一样线程对该volatile型变量操做的内存可见性,但不等同于并发操做的安全性
volatile型变量使用场景总结起来就是"一次写入,处处读取",某个线程负责更新变量,其余线程只读取变量,并根据变量新值执行相应逻辑,例如状态标志位更新,观察者模型变量值发布