Java发展重大事件
背景介绍
跨语言平台
基本概念
JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆和一个存储方法域。 JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
运行过程
我们都知道Java源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。
也就是如下:
① Java 源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
JVM生命周期
-
虚拟机的启动 Java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类来完成的,这个类由虚拟机的具体实现指定。
-
虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
- 程序开始执行时它才运行,程序结束时他就停止
- 执行一个所谓的Java程序的时候,真正执行的是一个Java虚拟机的进程
虚拟机分类
- Oracle HotSpot
- Oracle JRockit
- IBM J9
类加载器
TODO
字节码文件
0000
8421
Class文件是一组以8位(一个字节)字节为基础单位(0xFF)的二进制流、半个字节4位用一个0xF表示
4位2进制可用1位16进制数表示一个字节8位可用两个个16进制代替0xFF
- 无符号数:基本类型数据,一 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以_info结尾,用于描述有层次关系的复合结构的数据。
每个Class文件的头4个字节0xCAFEBABE称为魔数(Magic Number),用来确定这个文件是否为能被虚拟机接受的Class文件格式。
加载过程
分类
- Bootstrap ClassLoader
- Extension ClassLoader / Platform ClassLoader
- AppClassLoader
双亲委派模型
Tomcat类加载器
JVM内存分区
运行时数据区
程序计数器
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。
正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。
字节码指令 >> 机器指令
虚拟机栈
是描述 java 方法执行的内存模型
每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束。
局部变量表
- 局部变量表也成为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 这些数据类型包括各种基本数据类型、对象引用(reference),以及returnAddress类型
- 由于局部变量表示建立在线程栈上,是线程的私有化数据,因此不存在数据安全问题
- 局部变量表所需要的容量大小是在编译期确定下来的,并保存在方法的Code属性maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
Slot
操作数栈
动态链接
指向运行时常量池的方法引用
方法的调用
动态语言和静态语言
虚方法表
返回地址
虚拟机栈大小调整
- -Xss226k
栈工作原理
本地方法栈
本地方法区和 Java Stack(虚拟机栈) 作用类似,区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则 Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个 C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
堆-Heap
一个进程 >> JVM实例 >> 运行时数据区 >> 方法区、堆区
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
堆空间参数
堆优化技术
逃逸分析 >> 栈上分配 >> 标量替换
逃逸分析并不成熟
- 关于逃逸分析的论文在1999年就己经发表了,但直到JDK 1.6才有实现,而且这项技 术到如今也并不是十分成熟的。
- **其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分 析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复 杂的分析的,这其实也是一个相对耗时的过程。
- 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃 逸分桥的近程就白省浪费掉了。
- 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
- 注意到有一些观点,认为通过逃逸分析,JVN会在栈上分配那些不会逃逸的对象,这 在理论上是可行的, 但是取决于JVy设计者的选择。据我所知, Oracle Hotspot JV4中并未这么做,这一点在逃逸分析相关的文档里己经说明,所以可以明确所有的 对象实例都是创建在堆上。
- 目前很名书籍还是基于JDK 7以前的版本,JDK己经发生了很大变化, intern字符串的缓存和静态变量曾经都被分配在永久代上, 而永久代己经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,市 是直接在堆上分配,所以这一点同样待合前面一点的结论:对象实例都是分配在堆上
标量替换
堆空间大小设置
Young / Old Generation
新生代
老年代
OOM
直接内存
方法区、永久代
但是1.7及以后,静态变量和对象实例一起放到堆当中!
永久代为什么被元空间替代
栈、堆、方法区交互关系
常量池
1. 全局字符串池(string pool也有叫做string literal pool)
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
2. class文件常量池(class constant pool)
class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
3.运行时常量池(runtime constant pool)
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
举个实例来说明一下:
public class HelloWorld {
public static void main(String []args) {
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true
}
}
-
首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值
-
第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例都存在堆当中。
-
当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同。
-
str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值。
-
str5在解析的时候就也是指向存在于StringTable中的”def”的引用值。
Summary:
上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。
-
全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
-
class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
-
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
对象实例化
深拷贝 浅拷贝
拷贝,顾名思义就是为了获得一个相同的对象,而不需要我们再人为的创建和赋值。java中的拷贝需要实现java.lang.Cloneable接口,然后重写clone()方法,这个无论深、浅拷贝都需要这样做。
而深拷贝和浅拷贝的区别,则在于java对象中的基本数据属性和引用数据类型属性。
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
- 浅拷贝
- 深拷贝
- 利用序列化实现深拷贝
对象创建过程
- 字节码
- 对象内存布局
- 对象访问定位
-
句柄访问
-
- 直接指针(Hotspot)
执行引擎
GC
垃圾:运行程序中没有任何指针指向的对象
垃圾标记算法
1.引用计数算法
2.可达性分析算法
GC Roots
finalization
MAT
JProfiler
GC 算法
标记-清除 (Mark-Sweep)
复制 (Copying)
标记-压缩 (Mark-Compact)
分代收集算法
增量收集算法
分区算法
System.gc()
内存泄漏
Stop The World
并发 并行
垃圾回收的并发与并行
安全点
引用
- 软引用
- 弱引用
Minor GC / Magor GC / Full GC
TLAB
评估GC性能指标
垃圾回收器
垃圾回收器发展史
七款经典垃圾回收器与分代之间的关系
分类
Serial / Serial Old(标记整理)
ParNew (并行GC)
Parallel Old
多线程标记整理
Parallel Scavenge
多线程复制算法,更加高效
CMS (标记清除!!!)
多线程标记清除 Concurrent Mark Sweep
G1 (复制 + 标记整理)
ZGC
Java11
为大堆提供了非常短的停顿时间
Summary
JVM调优
- 项目搭建之前的预设置
- 监控调优
- OOM调优