如何理解"一次编译,到处运行":
Java源码首先被译成字节码,再由不同平台的JVM解析,Java语言在不同平台运行时不需要重新编译,Java虚拟机在执行字节码时,把字节码转换成具体平台中的机器码——Java虚拟机实现了跨平台特性;
程序计数器:每个线程都需要一个独立的程序计数器;
Java虚拟机栈:(通常“栈”所指)
主要指局部变量表——存放编译期就已知的基本数据类型、对象引用等;
本地方法栈(和虚拟机栈类似,为本地方法服务);
Java堆
Java堆是虚拟机管理的内存中最大的一块,被所有线程共享,在虚拟机启动时创建,存放实例对象(注:栈上分配,标量替换会进行优化);Java堆可以划分多个线程私有的分配缓冲区(更好地分配、回收内存);当Java堆内存没有完成实例分配且无法扩展时,就会抛出OutOfMemoryError异常.
方法区(Non-Heap):
线程共享;存储被虚拟机加载的类型信息、常量、静态变量等;运行时常量池是方法区的一部分;
对象的创建分配内存:指针碰撞(bump the pointer),空闲列表(free list);
本地线程分配缓冲(TLAB);
对象的访问定位:句柄访问,指针访问;
判断对象是否存活:
垃圾收集算法:
分代收集理论——将GC堆分成新生代和老年代提高回收效率;
一些概念的区分:
Minor GC触发条件
对象在新生代Eden区中分配,当Eden区内存空间不足将发起一次Minor GC;
Full GC触发条件
标记-清除算法
最基础的算法;
缺点:执行效率不稳定,会随着对象的增多执行效率降低;产生大量不连续的内存碎片;
标记-复制算法
常用于回收新生代;
内存分配担保(“Appel式回收”):在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间以判断Minor GC是否安全,进一步判断是否需要Full GC;
缺点:在存活率较高(如老年代对象)时进行较多的复制操作,效率降低;
Visual VM:多合一故障处理工具;
在Java中,要重载(overload)一个方法,除了要与原方法名称相同外,还必须要拥有一个与原方法不同的特征签名——(方法中各个参数在常量池中的字段符号的引用的集合),因为返回值不在特征签名中,所以Java不能通过返回值类型进行重载;
Java程序的方法体经过javac编译处理之后,变成字节码指令存储在Code属性中,Code属性出现在方法表的属性集合中;
加载:加载来自外部的二进制字节流,存储在方法区.随后会在Java堆中实例化一个java.lang.Class类的对象,作为程序访问方法区中的类型数据的外部接口.
连接:包括验证(检验二进制流是否符合规范),准备(为类变量分配内存,设置初始值),解析(将常量池中的符号引用替换为直接引用).
初始化:执行类构造器
类加载器
双亲委派工作模型
当一个类加载器收到一个类的加载请求,不会自己尝试加载这个类,而是把这个请求委派给父类加载器完成,所有加载器最终传送到顶层的启动类加载器,只有当父加载器没有找到所需的类时候,子加载器才会尝试自己完成加载;
优点:避免了类的重复加载;保证Java核心API不被篡改;
//双亲委派机制的实现
protected syschronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
//首先检查类是否已被加载过
}
Java模块化系统
分派
静态分派:javac在编译阶段根据参数的静态类型决定方法执行版本(重载)
动态分派:运行期间根据实际类型确定方法执行版本(重写)
动态类型语言
动态类型语言的关键特征是其类型检查的主体过程是在运行期而非编译期;
运行时异常:只要代码不执行到这一行就不会出现问题;
连接时异常:(例如NoClassDefFoundError)导致连接时异常的代码放在一条根本不会被执行的路径上,类加载过程(连接发生)中抛出异常;
在部署Web应用时,单独一个ClassPath不能满足需求,因此各种Web服务都提供了好几个有不同含义的ClassPath路径供用户存放第三方类库(一般以"lib"或"classes"命名);被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常每一个目录都会有一个响应的自定义类加载器去加载放置在里面的Java类库;(Tomcat目录结构为例)
语法糖:
在计算机语言中添加某种语法,对编译结果和功能无影响,但能减少代码量,增加程序可读性;
Java类型擦除式泛型的缺陷:
Signature:
Signature属性的作用是存储一个方法在字节码层面的特征签名——Java的泛型属于类型擦除式泛型,擦除法仅仅是对方法的Code属性中的字节码进行擦除;实际上元数据保留了泛型信息——这也是我们在编码时能够通过反射获得参数化类型的根本依据;
自动装箱/拆箱:
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型;
前端编译器:完成从程序到抽象语法树或中间字节码的生成;
即时编译器:为了提高“热点代码”的执行效率,在运行时,虚拟机会把这些代码编译成本地机器码,进行代码优化,完成这个任务的后端编译器即为即时编译器;
当出现“罕见陷阱”,通过“逆优化”回到解释状态继续执行;
分层编译:编译器根据优化、编译的规模和耗时,划分不同编译层次;
分层编译的交互关系:
“热点探测”(判断是否为热点代码):
提前编译器,主要两个分支:
即时编译 | 提前编译 |
---|---|
性能分析制导优化;激进预测性能优化;链接时优化 | 没有执行时间和资源限制的压力 |
编译器优化技术
方法内联(把目标方法代码原封不动地“复制”到调用的方法中)
阻止继承:final类和方法:
虚方法和内联会产生矛盾,Java虚拟机引入CHA(类型继承关系分析)技术以及使用内联缓存(Inline Cache)减小方法调用开销;
逃逸分析
为什么要逃逸分析:
分析对象动态作用域,为其他优化措施提供依据;从不逃逸、方法逃逸(一个对象在方法中被定义后,可能被外部方法所引用)到线程逃逸(比如赋值给可以在其他线程中访问到的实例变量),称为对象由低到高的不同逃逸程度,通过分析逃逸程度从而采取不同程度的优化.
栈上分配:
标量替换:
假如逃逸分析能够证明能够证明一个对象不会被外部方法访问,并且这个对象可以被拆散,可能不会创建这个对象而改为直接创建若干个被方法调用的成员变量(聚合量替换为标量).
同步消除:
逃逸分析确定变量不会逃逸线程,则可以安全地消除对这个变量的同步措施;
公共子表达式消除
如果一个表达式E被计算过了,且从之前到现在E中所有变量值没有变化,则E就为公共表达式;
每条线程的工作内存保存了该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,不同线程之间无法访问对方工作内存中的变量。交互关系如图:
主内存与工作内存的之间的具体的交互协议必须保证read,write,lock,unlock等操作都是原子的,不可再分的。
原子性:read,load,assign,use,store等操作保证原子性,不可中断;
可见性:当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改;常用方法:volatile的特殊规则保证新值能够立即同步到主存,以及synchronized方法等;
有序性:volatile和synchronized保证线程的有序性;
先行发生原则:用来判断数据是否存在竞争,线程是否安全(包括程序次序规则、管程锁定规则、volatile变量规则等等都是Java内存中模型中"天然的"先行发生关系)
线程实现的三种方式:
线程安全的实现方法
互斥同步(悲观策略——共享数据无论是否出现竞争都会加锁):
① sychronized持有锁(重量级的操作)
② Lock接口,以非块结构实现互斥同步.重入锁(ReentrantLock)相比sychronized增加了可等待中断、可实现公平锁、锁绑定多个条件等特性.
非阻塞同步(乐观策略):
依赖硬件指令集的发展(需要确保操作和冲突检测这两个步骤具有原子性)
CAS操作:需要三个操作数:V(内存位置),A(旧的预期值),B(准备设置的新值)当且仅当V符合A时才会用B更新A的值,否则不执行更新,上述处理是一个原子操作不会被其他线程中断.(ABA问题解决办法:通过控制变量值的版本保证CAS的正确性)
无同步方案:
可重入代码:具有不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的变量由参数传入,不调用非可重入的方法
线程本地存储:把共享数据的可见范围限制在同一个线程之内,例如LocalThread.对于多线程访问的数据可用volatile关键字实现可见性.
锁优化
自旋锁:如果物理机器上有一个以上的处理器核心,能够让两个或以上的线程并行执行,可以让后面“请求锁”的线程执行一个忙循环(自旋),但不放弃处理器的执行时间,避免了线程切换的开销,但占用了处理器的时间;
锁消除:虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除;