赞
踩
第一部分 走进 Java
Java的优点:
Java 发展简史:
多核并行:
使用 Fork/Join 模式可以轻松利用多个 CPU 核心完成计算任务
64 位虚拟机:
Java 程序在 64 位虚拟机上需要付出更大的代价:
运行时数据区域:
程序计数器
指向下一条需要执行的指令;
每条线程都需要一个独立的程序计数器,各线程的计数器互不影响,独立存储,是线程私有内存;
若线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
若线程正在执行 Native 方法,则计数器为空;
此内存区域是唯 一 一 个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域;
虚拟机栈
属于线程私有的内存区域,生命周期随线程;
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存放局部变量、操作数栈等信息,每一个方法从调用到执行完成的过程就是栈帧在虚拟机栈中入栈到出栈的过程。因此,在递归调用时,如果递归的太深,会导致 StackOverflowError,如下面的代码:
- public int sum(int n) {
- if (n == 1) return 1;
- return sum(n - 1) + n;
- }
经常说的内存里面的栈,指的就是虚拟机栈,或者说是虚拟机栈中局部变量表部分;
这个局部变量表所需要的内存空间是在编译期间就确定的;
64 位的 long 与 double 占据两个局部变量空间;
此内存区域有两种异常:
本地方法栈
类似于虚拟机栈,不同之处就是虚拟机栈为执行 Java 方法服务,而本地方法栈为执行 Native 方法服务;
由于机器的内存是有限的,当机器捏内存不足时,抛出 OOM 异常
Java 堆
所有线程均可使用的内存区域,在虚拟机启动时创建,作用 就是 存放对象实例,所有的对象实例以及数组均在堆上面创建;
(但是 JIT 技术的发展会将频繁使用的对象,变成本地调用,所以“所有的对象均在堆上创建”不是那么绝对)
堆是GC的主要区域;
堆还可以继续细分,看图!
堆可以物理上不连续,但逻辑上必须连续;
此内存区域无法完成内存分配时且无法扩展时,抛出 OutOfMemoryError 异常;
方法区
所有线程均可使用的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
方法区,不属于堆,但是又被称为“永久代”,原因是:在 HotSoprt 虚拟机下,GC 被扩展到方法区,GC 本来主要工作在 新生代、老年代上,但此时又可以工作在方法区,于是取名“永久代”,其他虚拟机如:BEA 根本不存在永久代的说法;
用永久代实现方法区不好,会造成内存溢出,因为这一块的 GC 很难令人满意,它 GC 的主要目标是对常量池的回收和对类型的卸载;
JDK 1.7 中已经把原本放在永久代的字符串常量池移出到堆里面;
JDK 1.8 永久废除永久区;
此内存区域无法满足内存分配时,抛出 OutOfMemoryError 溢异常;
运行时常量池是方法区的一部分,【class文件含有常量池信息,类加载到内存后,JVM 会把 class 的常量池放到运行时常量池中,所以每一个类都有一个运行时常量池】常量还可以在运行期间产生,通过String.intern() 方法可以将该 String 存放到常量池中并返回该常量的引用,若下一次需要一个该 String 的时候,先检查常量池中是否存在同样的值,若存在返回引用,否则直接创建
关于 intern 方法的更多细节请访问:我的知乎
此内存区域无法完成内存分配时且无法扩展时、常量池无法申请到内存时,抛出 OutOfMemoryError 异常;
总结:OutOfMemoryError 可能的致因:内存溢出 或 内存泄漏,一般发生在老年代
非运行时数据区域:
直接内存,不是虚拟机运行时数据区域;
NIO 可以通过 Native 函数直接分配堆外内存,然后通过 Java 堆里面的 DirectByteBuffer 对象作为这个堆外内存的引用并操作它;
- 堆内存数据在 flush 远程时,会先复制到 直接内存 ,也就是从堆内存到非堆内存,然后再发送
- 堆外内存直接可发送,没有 copy 的过程;
- 在 Socket 通信中就常常需要这么操作,否则 GC 会导致数据失效;
此内存区域无法完成内存分配时且无法扩展时,抛出 OutOfMemoryError 异常;
对象的创建:划分空间 + 初始化
遇到 new 关键字的时候就需要加载类信息并创建对象,创建对象的过程就是把堆空闲内存划分一块出来,划分的方式又分为“指针碰撞” 和 “空闲列表”
分配又分为直接在堆上面分配(并发时需要同步控制:CAS + 失败重试)和本地线程分配缓冲(就是预先给此线程足够额外空间以给未来可能产生的对象提供内存空间,因此无需同步操作)
对象的访问:句柄 或 reference
GC 需要考虑三件事:哪些内存需要回收、什么时候回收、如何回收
而 GC 主要关注动态分配的内存,其余的会随线程消亡而消失
如何判断对象是否存活
原理:给对象中添加一个引用计数器,被引用计数器就加一,引用失效就减一,当引用为 0 的时候,则该对象就是“死了的”
缺点:无法处理循环引用的问题,例如 A 引用了 B,B 又引用了 A,虽然 A、B在未来不会被使用了,但是由于他们的引用不为 0,所以他们依然是“存活的”,导致 GC 无法回收;
通过一系列的 GC Roots 的对象作为起点,从这些节点开始往下搜索,如果某一个对象不在搜索范围内,则称该对象不可达,不可达的对象就是 GC 的目标,如图:由于对象 4、5、6 不可达,所以在 GC 范围内
即使对象4、5、6 没有与 GC Roots 关联,他们也不会立即被 GC,会有一次二次筛选,如果对象4、5、6 实现了 finalize() 方法那么他们有自救的机会,即虚拟机会判断是否执行他们的 finalize 方法,只要他们在 finalize 方法里面把自己关联上 GC Roots,就不会被 GC 调,但要是没有实现 finalize 方法或者在方法里面没有关联上 GC Roots,这一次筛选后在下一次 GC 时,对象就会被回收。finalize 方法只会执行一次,不推荐使用此方法。
引用的分类:
方法区也可以被回收
方法区里面的废弃常量、无用的类会被回收,没有被任何对象引用的常量就叫废弃常量,废弃常量会被清除,
无效的类(不存在该类的任何实例、该类的ClassLoader已被回收、没有被反射)会被清除;
垃圾收集算法
原理:先标记要清理的对象(通过引用计数法、可达性分析法确定标记哪些对象),然后统一回收;
缺点:标记与回收的效率低、容易产生内存碎片(要么导致一次新的 GC,要么 OOM)
原理:将可用内存划分为大小相等的 A、B 两块,每次只使用其中的一块,当 A 块不够用了,就将 A 中还存活的对象复制到 B 上,然后一次性清空 A,对整个半区进行 GC 可解决效率与内存碎片问题;
新生代回收过程:GC 开始前对象只会存在于 Eden 区和 From Survivor 区,To Survivor 区为空,GC 开始后 Eden 区存活的对象会被复制到 To 区,From 里面达到老年年纪的对象会被放到老年区,其余的会被复制到 To 区,然后 Eden 和 From 区被清空,然后 To 区和 From 区交换角色,最终保证 To 区一直是空的
原理:先标记要清理的对象(通过引用计数法、可达性分析法确定标记哪些对象),然后将所有存活的对象往一端移动,最后直接清理端外的全部“死亡”的内存,解决了内存碎片问题;
GC 时需要中断全部线程,分为:抢先式中断 和 主动式中断
抢先式中断:中断全部线程进行 GC
主动式中断:设置中断标志位,线程轮询此标志位,为 ture 则挂起自己
垃圾收集器
内存分配与回收策略
对象主要在 Eden 上分配,如果启用了线程分配缓冲,则优先在 TLAB 上分配;但是大对象(大型数组或字符串)直接进入老年代;新生代内存不足会发起 Minor GC
长期存活的对象直接进入老年代,每个对象都有一个 Age,默认当 Age >= 15 的时候会从新生代进入老年代,但是这个 Age 的规定并不是死的,也会动态调整,当老年代内存不足时发起 Major GC
Full GC:清理整个堆空间
目前,略
未来再更新
目前,略
未来再更新
JVM 与任何语言无关,包括 Java !他只与 class 文件绑定,所以不管什么语言,只要生成合格的 class 文件,就可以在 JVM 上运行,class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息;
Java 语言最终是由多条字节码命令组合而成的,因此字节码指令会比 Java 语言更强大,类似的可以说,Java 语言无法完成的任务可以让其他语言完成,大家都在 JVM 上运行;
class 文件是一组以 8 位字节为基础单位的二进制流,他的文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
魔数与 class 文件版本
很多文件存储标准中都使用魔数来进行身份识别,确保文件类型正确(因为文件类型后缀可以任意修改,而魔数进一步确定文件格式),class 文件的魔数为:0xCAFEBABE
class 文件还存储着版本号,版本号的确定保证了向下兼容,即高版本的 JVM 可以执行低版本的 class 文件,但是低版本的 JVM 不可以执行高版本的 class 文件;
常量池
常量池是 class 文件中的资源仓库,存放:字面量与符号引用;
字面量:常量、字符串、final 常量值;
符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符
Java 虚拟机指令只含操作码,没有操作数
/***
由于内容比较繁琐,更多内容日后更新
**/
在 Java 语言里面,类型的加载、连接和初始化过程都是在程序运行期间都是在程序运行期间完成的,多态就是基于这一点实现的:接口的实现类有很多,等到运行时动态确定用哪一个实现类,另外,RPC 调用也是基于 Java 语言的动态性实现的
类加载的生命周期:
加载、验证、准备、初始化、卸载的顺序是确定的,但是解析却不一定,有时在动态绑定时可能在初始化之后才开始解析;
什么时候“加载”,JVM 并没有强制规定,但是却对初始化做了强制规定:有且只有 5 种情况必须立即对“类”初始化
注意一下实例:
子类 A 继承 父类 B,B 含静态字段 value ,主类里面通过 A 调用父类的 value,即:A.value,结果是 B 被初始化了,A 没有,原因是:对于静态字段,只有直接定义这个字段的类才会被初始化;
类 C 含静态字段 value,在主类 main 里面直接调用 C.value(无继承关系),发现 C 类没有被初始化,原因是:编译阶段存在常量传播优化措施,C 的常量被转移到了主类中,于是运行时根本不会涉及到类 C;
当一个类在运行时要求其父类全部初始化了,但是不要求接口的父接口全部初始化;
加载一个类可以是从 class 文件中,也可以是动态生成的类,甚至可以是从网络中获取的字节等,来源不限;
两个代码一样的类如果是使用不同的加载器加载的,则这两个类不同,但要是是一个加载器加载的,那就相同了;
类加载器使用双亲委派模型:
此模型要求除了顶级加载器之外,其余的加载器都必须有自己的父类加载器(不是用继承实现的,代码里面组合实现),通常先将加载请求发送到顶级父类处理,如果父类无法处理再交给下一级加载器处理;
例如,现在有两个 Object 类,一个是 Java 原生的,一个是自己手写的 java.lang.Object ,如果不使用双亲委派模型,则系统中将会出现多个 Object,会变得非常混乱,安全性无法保证;
但是此模型只是一种规范,可以打破他,例如 SPI,JNDI 使用线程上下文加载器加载 SPI 代码,即父类加载器请求子类加载器加载类,虽然违背了双亲委派模型,但是却完成了功能,可以算一种【创新】了吧;
/**
其余内容日后再更
**/
栈帧:支持虚拟机进行方法调用与方法执行的数据结构,属于虚拟机栈内存区域的栈元素;存储了方法的局部变量、操作数栈、动态链接和方法返回地址;
每一个方法从调用到结束的过程都对应着栈帧在虚拟机栈里面的入栈与出栈的过程;
只有位于栈顶的栈帧才是有效的,称之为当前栈帧:
Javac 完成词法分析、语法分析、语义分析、字节码生成
日后更
前端编译器:Javac
后端编译器:JIT(C1、C2){ C1指:Client Compiler,C2指:Server Compiler }
静态提前编译器:AOT 编译器(GNU Compiler for the Java(GCJ)),直接把 *.java 文件编译成本地机器代码
Java 虚拟机设计团队通过添加“语法糖”(语法糖:对语言原有功能无影响,但是可以提高编码效率)来改进编码效率,而不是从底层上改进,例如:泛型,Java 的泛型是伪泛型,在编译后的字节码文件中被替换为原来的原生类型,并插入了强制转换代码,举例:
这是源代码:
- public class Test {
- public static void main(String[] args) {
- Map<String, String> map = new HashMap<>();
- map.put("key", "value");
- System.out.println(map.get("key"));
- }
- }
这是编译后的反编译代码:
- public class Test {
- public Test() {
- }
-
- public static void main(String[] args) {
- Map<String, String> map = new HashMap();
- map.put("key", "value");
- System.out.println((String)map.get("key"));
- }
- }
可以发现,存在代码强转,原来的泛型不存在了,此称之为:泛型擦除
扩展:此代码无法编译通过,由于存在泛型擦除,method 方法虽然被不同数据类型重载了,但是编译后 method 方法的参数都退化成 List<E> 了,存在签名一直的方法,于是编译不通过;
- public void method(List<String> list) {
-
- }
-
- public void method(List<Integer> list) {
-
- }
同样的自动装箱、拆箱、ForEach 都是语法糖
条件编译:需要在方法里面铜鼓条件编译的时候可以这样写
- if (true) {
- // do
- } else {
- // do
- }
- /************/
- while (false) {
- System.out.println(123);// 报错:方法不可达
- /**
- 条件为 true 时才会触发编译,
- false 没有编译的必要
- **/
- }
被执行的方法被 JIT 优化过了,方法执行时先走 JIT 编译的代码,不然再检测是否出现热点代码
Java 最初是解释执行的,但是会把热点代码通过 JIT 编译成本地机器码;
现在的 Java 是解释器与编译器并存的架构,通过终端输入:java -version 可以查看你的 JDK 是属于解释的、编译的、还是混合的:
- java version "1.8.0_171"
- Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
- Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
最后一行【mixed mode】,说明我的 JDK 是混合编译的
解释器的优势:启动快、节约内存
编译器的优势:执行快
触发 JIT 编译的条件是代码够“热”,判断热点代码的方法是:基于采样的热点探测、基于计数器的热点探测
虚拟机定期检查线程的栈顶,如果发现某个方法经常出现在栈顶,那他就是热点方法;
优点:简单、高效;
缺点:易受干扰;
虚拟机为每个方法建立计数器,当计数器达到阈值就判定为热点方法并进行 JIT 编译;
HotSport 虚拟机又为每个方法准备了方法调用计数器、回边计数器,这两个计数器
方法调用计数器:统计方法被调用的次数,执行方法是如果该方法没有 JIT 编译的版本则解释执行此方法,然后计数器加一,接着判断方法调用计数器、回边计数器的和是否大于方法调用计数器的阈值,大于就提交 JIT 编译请求;但是如果一个方法虽然被方法调用计数器计数了,没有达到阈值,又在一段时间内没有被调用过,那么他的计数值将会在一定时间内衰退【热度衰减,与实现世界一样,烧红的烙铁不继续加热就会降温】
回边计数器:统计循环体代码执行的次数;原理:回边计数器达到阈值后会触发 OSR 编译【On-Stack Rplacement,循环体替换不像 JIT 编译后可以等待下一次调用,循环体需要编译后立即生效且在循环体结束前立即生效,所以需要动态替换栈顶代码】
- 前面提到了,Javac 是编译前端,完成词法、语法、语义的分析并生成字节码,再由 JVM 来动态链接,执行
-
- 因此,Java 代码在解释执行模式下或混合模式下,触发 JIT 编译前是解释执行的,之所以需要解释字节码而不是源代码,因为 1. JVM 只识别字节码、2. 字节码解释效率更高【虽然有直接解释 Java 源代码的技术】
-
- 其实 Python 这门解释性语言也是先编译成字节码,再解释字节码的
-
- JDK 9 开始自带一个 jshell ,可直接解释执行 Java 源代码
- 编译流程:
- 源代码:
- --词法分析——>token 串
- --语法分析——>语法树
- --语义分析——>抽象语法树
- --代码生成——>目标代码
-
- 执行:
- 目标代码:
- --操作系统/硬件——> 结果

可以使用 -XX: +PrintCompilation 参数输出被 JIT 编译的方法名
/**
其余内容日后更新
**/
Amdahl 定律:加快某一部件执行速度所能获得的系统性能加速比受限于该部件的执行时间占系统中总执行时间的百分比
加速比 = 改进前的总执行时间 / 改进后的总执行时间
由于 CPU 与内存速度不匹配,所以需要 Cache ,但是引入缓存又引发了缓存一致性的问题,为了解决 Java 语言在不同平台上访问内存一致性的问题,引入 Java 内存模型(JMM);
JMM 要点如下:
以上三个特性出现问题将导致线程不安全
线程安全在下一节讲
happens-before 原则:
举例:
- i = 1;// A线程执行
- j = i;// B线程执行
- i = 2;// C线程执行
三个线程同时执行后,请问 j 为 1 还是 2?
答案:不知道
因为线程 B 现在无法观察到 i 的变化,说 j = 1,是依据先行发生原则的“程序次序规则”,B 线程可以观察到 A 线程的操作;说 j = 2,是因为线程 C 提前在 A 和 B 之间执行了,所以如何优化它?
按照 happens-before 原则套,让他符合这些原则之一即可,例如,给变量 i 加上 volatile 关键字
时间上优先发生并不代表代码写在前面,时间顺序与 happens-before 原则无关
Java 的线程使用的是操作系统的线程模型,因此 Thread 类里面的核心方法全是 Native 方法;
何为线程安全:并发下的执行结果是正确的就叫线程安全;
一般涉及到线程安全的时候讨论的都是共享数据,数据独占不存在线程安全性问题;
线程安全的实现方法:
对共享数据加 final 关键字,即可将其变为线程安全的,例如 final int a = 3;
又例如 String 类就是一个 final 类,它是安全的;
但是自定义的 final 类可能依然不安全,例如:final User 类,User 里面有一个字段 money 不是 final 类型的,于是此 User 依然不安全,要学 String,所有属性均是 final 的才安全;
枚举类天生就是安全的,所以用它做线程安全的单例模式特别好
同步:共享数据在某一时刻只能被一个线程占用
互斥:实现同步的手段,如使用信号量、临界区、互斥量
实例:synchronized 关键字
通俗地说就是,先尝试操作,如果没有其他线程争用共享数据,那么操作成功;如果存在线程争用共享数据,则不断重试直到成功
实例:CAS 操作
CAS(V, A, B)
V:内存正储存的值
A:旧值
B:新值
原理:如果 旧值 == 内存 里面的值,就用 新值 更新 内存 里面的值
例如递归,执行的时候被中断执行其他任务,然后再返回执行自己的事,这样的代码是安全的
示例代码:
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class ThreadLocalExample1 {
-
- private static final ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 1;
- }
- };
-
-
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
-
- for (int i = 0; i < 1000; i++) {
- service.execute(new Runnable() {
- @Override
- public void run() {
- count.set(count.get() + 99);
- System.out.println(count.get());
- /**
- * 在线程池中使用ThreadLocal的时候更要注意清理threadLocal
- * 因为线程池会复用线程,
- * 在复用线程执行任务时会使用被之前的线程操作过的 value 对象
- * 所以要是不清理,就会出现线程拿到的变量不是原始变量的副本,
- * 导致结果出错
- */
- count.remove();
- }
- });
- }
- /**
- * 不管其他线程如何修改可变变量,他们都是修改的此变量的副本,不影响原来的值【但是那些线程却得到了自己想要的结果】
- *
- * 最终的输出结果还是原来的初始设定
- */
- System.out.println("final: " + count.get());
- service.shutdown();
- count.remove();
- }
-
- }
-
- /**********************************************/
-
- public class ThreadLocalExample2 implements Runnable {
-
- private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 0;
- }
- };
-
- private int index;
-
- public ThreadLocalExample2(int index) {
- this.index = index;
- }
-
- @Override
- public void run() {
- System.out.println("线程" + index + "的初始value:" + value.get());
- for (int i = 0; i < 5000; i++) {
- value.set(value.get() + 1);
- }
- System.out.println("线程" + index + "的【累加】value:" + value.get());
- }
-
-
- public static void main(String[] args) {
- for (int i = 0; i < 100; i++) {
- new Thread(new ThreadLocalExample2(i)).start();
- }
- }
- }
-
- /**********************************************/
-
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class ThreadLocalExample3 extends Thread{
-
- private static final ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 1;
- }
- };
-
- @Override
- public void run() {
- count.set(count.get() + 99);
- System.out.println(count.get());
- }
-
- public static void main(String[] args) {
- for (int i = 0; i < 100; i++) {
- ThreadLocalExample3 example3 = new ThreadLocalExample3();
- Thread thread = new Thread(example3);
- example3.start();
- //System.out.println(count.get());
- // ThreadLocal最佳实践:为了防止内存泄漏,必须手动清空Map
- count.remove();
- }
- }
-
- }
-

锁优化:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。