Java 优点:
1. 摆脱了硬件的束缚,一次编写,到处运行
2. 相对安全的内存和访问机制
3. 实现了热点代码检测和运行时编译及优化
4. 完善的应用程序结构
Clojure,JRuby,Groovy运行于Java虚拟机上的语言及相关程序都属于Java技术体系一员
并行处理用Clojure,展示层用JRuby,中间层则用Java
- JDK(Java程序设计语言 / Java虚拟机 / Java API)是用于支持Java程序开发的最小环境
- JRE(Java SE API子集 / Java虚拟机)是支持Java程序运行的标准环境
Javac将Java源代码转化为二进制JVM可识别语言,而JVM将JVM转化为机器能够识别的机器语言。
JVM组成:
1.程序计数器:字节码解释器通过改变计数器的值选取下一条执行的字节码指令。
2.虚拟机栈:每个方法从调用至执行完全的过程,对应着一个栈桢在虚拟机中入栈出栈的过程。
存储:对象引用,局部变量 (-Xss 设置栈大小)
栈桢:局部变量表、操作数栈、动态连接、方法出口等信息
局部变量表:所需内存空间完成分配,当进入一个方法需要在桢中分配多大空间已确定
- StackOverflowError 栈深度大于虚拟机深度
- OutOfMemoryError 若扩展时无法申请到足够内存
3.本地方法栈:为虚拟机使用到的Native方法服务,也会有StackOverflowError 和 OutOfMemoryError
4.Java堆:所有线程共享的内存区域,存放对象实例,分为Eden区,From Survior空间,To Survior空间等。Java堆可以处于物理上不连续内存空间中,只要逻辑上连续即可
s0 与 s1大小相同,并在MinorGC后会互换角色
- young:MinGC 复制算法
- old:FullGC 标记清除算法
- Permanent:不回收
5.方法区:共享区域,用于存储虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等。并非“永久”,当方法区无法满足分配需求时将抛出OutOfMemory 异常。
运行时常量池:存放编译期生成的各种方面量和符号引用
6.直接内存:NIO类基于通道与缓冲区,使用Native函数库直接分配堆外内存,Java堆中的DirectByteBuffer作为这块内存的引用操作。
OOM异常:OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有OOM异常
- Java Heap异常:不断创建对象---->检查虚拟机的参数-xmx
- 虚拟机栈和本地方法栈溢出:
(1) 线程请求的栈深度大于虚拟机最大深度 StackOverFlowError
(2) 在扩展栈时无法申请到足够内存空间 OutOfMemoryError
- 运行时常量池溢出
- 方法区溢出
对象的创建:
(1) 首先检查这个指令的参数是否在常量池中定位到一个类的符号引用
(2) 虚拟机为新生对象分配内存
选择哪种分配方式由Java堆是否规整决定,而是否规整由是否带有压缩整理的垃圾回收器决定:
- Serial ParNew:带有压缩的收集器,采用指针碰撞
- CMS:采用空闲列表
- 执行new指令之后,执行<init>方法
对象在内存中存储布局:(1)对象头 (2)实例数据 (3)对齐填充
(1)对象头:
- Mark Word,非固定数据结构
对象自身的运行数据,如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 类型指针,对象指向它的类元数据指针
(2)实例数据:
真正存储的有效信息,存储顺序会受虚拟机分配策略参数,字段的影响
(3)对齐数据:非必然,占位符
对象的访问方式:(1)使用句柄 (2)直接指针
垃圾回收中对象死亡的判定:
(1)引用计数法
(2)可达性分析算法,以GC Roots 对象作为起点,从这些节点向下搜索,搜索走过的路径为引用链
GC Roots:
1.虚拟机栈引用的对象
2.方法区
静态属性引用对象
3.方法区中
常量引用对象
4.本地方法栈中JNI引用对象
对象死亡“两次标记”:
- 没有GC Roots相连接的引用链,放入到F-Queue队列之中
- GC对F-Queue中对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己,只需要与任意对象关联即可,届时将被移出F-Queue
方法区回收:
永久代垃圾回收两部分内容:废弃常量,无用的类
“无用的类”判定:
(1)实例回收 (2)ClassLoader被回收 (3)无法在任何地方通过反射访问该类的方法
在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi,这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,保证永久代不会溢出。
垃圾收集算法:
(1)标记-清除算法:
过程:a、标记 b、清除 从根节点起,不可达对象均为垃圾对象
缺点:会产生空间碎片
(2)复制算法:适用于新生代,因为大部分对象被清空
将原有内存空间的分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存中的所有对象,交换角色
eden--->from---->to---->老年代
(3)标记-整理算法:适用于老年代,大部分对象存活
将所有存活对象压缩到内存一段,清理边界外的所有空间
(4)分代收集算法
新生代:复制算法
老年代:标记-整理算法
(5)引用计数法,增量算法
HotSpot 利用 Oopmap存放对象引用
GC安全点:在特定位置,即安全点处暂停
安全点选定:是否让程序长时间执行的特征(指令序列复用),如方法调用,循环跳转,异常跳转等
到达安全点的方案:抢先式中断、主动式中断
安全区域:在一段代码片段之中,引用关系不会发生变化,这个区域中任意地方开始GC都是安全的。
垃圾分类器分类:
- 线程数(串行、并行)
- 工作模式(并发、独占)
- 碎片处理(压缩、非压缩)
- 分代(新生、老年)
垃圾分类器评价指标:吞吐量、垃圾回收器负载、停顿时间、垃圾回收频率、反应时间、堆分配
新生代串行收集器 | 最古老的一种,使用了复制算法 | -xx:UseSerialGC |
老年代串行收集器 | CMS备用收集器,使用了标记-整理算法 | |
并行收集器 | 多个串行一起运行 | -xx:+UseParNewGC 新生代使用并行收集器,老年代使用串行收集器 -xx:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS |
新生代并行收集器 Parallel Scavenge | 关注点:吞吐量,使用复制算法 | -xx:+UseParallelGC -xx:+UseParallelOldGC -xx:MaxGCPauseMillis 设置最大垃圾收集停顿时间 -xx:GCTimeRatio 设置吞吐量大小 -xx:+UseAdaptiveSizePolicy 打开自适应GC策略 |
老年代并行收集器 | -xx:+UseParallelOldGC 新生代+老年代使用并行收集器 -xx:+ParallelGCThreads 设置垃圾收集时的线程量 | |
CMS收集器 | 关注系统停顿时间,多线程并行回收 使用标记-清除算法 初始标记(独占)主要标记根 -并发标记 顺着根标记回收内存 -重新标记(独占) -并发清除 -并发重置 | -xx:ParallelCMSThread 设定CMS线程数量 -xx:CMSInitialOccupancyFraction 老年代达到68%,CMS开始回收 -xx:+UseCMSCompactAtFullCollection 使CMS在垃圾收集后,进行一次碎片整理 -xx:CMSFullGCsBeforeCompaction 设定CMS回收后,进行一次内存压缩 |
G1收集器 | 基于标记-压缩算法 |
G1:独立区域,新生代,老年代不再物理隔离。,可预测停顿时间。后台维护优先列表(优先回收垃圾最多的区域)
G1适用:
- 服务端多核CPU,JVM占用较大
- 产生大量内核碎片,经常压缩空间
- 防止高并发下应用雪崩现象
G1步骤:(1)初始标记 (2)并发标记 (3)最终标记 (4)筛选回收
- 初始标记:标记一下GC Roots关联的对象
- 并发标记:进行GC Roots Tracing过程
- 重新标记:修正并发标记期间因用户程序继承运作导致的记录变动
CMS缺点:
- 收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾
- 空间碎片产生
G1 与 CMS对比:
G1在压缩空间方面存在优势,Eden、Survior、Old区不再固定,堆空间划分成相互独立的区块,且不连续。内存效率能灵活
CMS收集器和G1收集器的区别
区别一: 使用范围不一样CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样CMS收集器 G1收集器
1. 初始标记 1.初始标记
2. 并发标记 2. 并发标记
3. 重新标记 3. 最终标记
4. 并发清楚 4. 筛选回收
空间分配担保:确保老年代有足够的空间容纳Survior对象,Minor GC 之前,检查老年代最大连续空间是否大于新生代对象总空间:
- 是,Minor GC安全
- 不是,检查HandlerPromotionFailure值是否允许担保失败,否-->Full GC
MinGC:Eden区满,Eden申请空间失败
FullGC:调用System.gc;老年代不足;方法区不足
虚拟机性能监控
- jps:虚拟机进程状况工具,列出正在运行的虚拟机进程
- jstat:虚拟机统计信息监控工具
- jinfo:查看和调整虚拟机各项参数
- jmap:堆转储快照---> 生成dump文件
- jhat:jmap + jhat 堆转储快照分析工具(很少使用)
- jstack:Java堆栈跟踪工具,生成虚拟机当前时刻的线程快照,分析某一时刻程序卡死的原因
- HSDIS:本地代码还原为汇编代码输出,还生成大量有价值的注释
- javap:分析Class文件字节码工具
JDK可视化工具
- JConsole:Java监视与管理平台
线程长时间停顿原因:1、等待外部资源 2、死循环 3、锁等待
- VisualVM (通过HotSwap技术动态加入原本不存在的代码)
对应用程序实际性能影响最小,可以直接应用在生产环境中。
功能:
1、显示虚拟机进程以及进程配置,环境环境(jps、jinfo)
2、监控应用程序CPU,GC,堆,方法区及线程信息(jstat、jstack)
3、dump以及分析堆转储快照(jmap、jhat)
4、被调用最多,运行时间最长的方法
JVM基于栈的理由:
(1)保证没有或很少寄存器上也能正确执行Java代码,与平台无关
(2)编译后的class文件更加紧凑
类文件结构(面向JVM,Oolang语言)
各种不同平台的虚拟机都统一使用程序存储格式--字节码,构成与平台无关的基石。
Java语言拆分:Java语言规范,Java虚拟机规范
Java虚拟机不和包括Java在内的任何语言绑定,任何语言都能表示成Java虚拟机接收的Class文件
虚拟机只知名二进制流从一个Class文件中获取,但没有指明从哪里获取,怎么获取
Class文件的来源
- 从zip包获取,jar,ear,war格式的基础
- 网格获取,Applet
- 运行时计算生成(动态代理技术)
- 其他文件生成,Jsp文件生成对应的Class类
- 数据库中读取
虚拟机语言无关性:
Java 程序 ---> Javac 编译器
JRuby 程序 ---> Jruby 编译器
Groovy程序 ---> groovy编译器 字节码(.class) ---> Java虚拟机
...... ---> ......
Class文件: 8位字节为基础的二进制流,各数据项严格紧凑排列
- 1~4字节 魔数 oxCAFEBABE
- 5~6字节 次版本号
- 7~8字节 主版本号
- ... ... 常量池容量计数值 (Class文件中第一个出现表类型数据项目)
(常量池中两大常量:字面量、符号引用),常量池中每一项都是一个表
- 之后的两个字节 访问标志(access-flags),标志一些类或接口层次的访问信息
(类 / 接口,是否为public,是否为abstract)
- 类索引:类的全限定名
- 父类索引:类的父类全限定名
- 接口索引:描述这个类实现了哪些接口
- 字段表: 用于描述接口或类中声明的变量
- 方法表: 访问标志,名称索引,描述符索引,属性表索引
在Java语言中,要重载一个方法,除了方法具有相同简单的名称除外,还要求拥有与原方法不同的特征签名。(特征签名:一个方法中各个参数在常量池中字段符号引用的集合)
属性表集合(Class文件、字段表、方法表都可以携带自己的属性表集合)
- Code属性
- Exception属性
- LineNumberTable属性(源码行号,字节码行号关系)
- LocalVariableTable属性
- SourceFile属性(Class文件源码文件名称)
- ConstantValue属性(静态变量)
- InnerClasses属性(记录内部类与宿主类之间关联)
- Depreated 及 Synthentic属性
- StackMapTable属性(复杂的变长属性)
- Signature属性(泛型签名信息)
- BootstrapMethods(复杂变长属性)
大部分指令都不支持byte、char和short等类型,将转化为int操作
Class文件中方法,字段都需要引用CONSTANT_UTF8_INFO型常量,所以CONSTANT_UTF8_INFO型常量最大长度也就是Java中方法,字段的最大长度
Java程序中如果定义超过64KB英文字符的变量或方法名,将无法编译
类加载过程
Java可以在运行期动态加载和动态连接达到Java动态扩展的特性
类加载时机
加载---> 验证 ---> 准备 ---> 解析 ---> 初始化 ---> 使用 ---> 卸载
5种类进行“初始化”:
1. 遇到new , getstatic , putstatic , 或invokestatic 4个字节码指令时
常见场景:
(1)使用new 关键字实例化对象的时候
(2) 读取或设置一个类的静态字段
(3) 调用一个类的静态方法的时候
2. 使用java.lang.reflect包对类进行反射调用的时候
3. 初始化一个类,发现其父类没有进行初始化
4. 当虚拟机启动时,用户需要指定一个执行的主类,主类需要初始化
5. 当使用java动态语言时,REF_getStatic,REF_putStatic,REF_invokeStatic 句柄时,初始化
接口与类真正区别:
当一个类初始化时,要求其父类其内部初始化过,但一个接口初始化时,并不要求其父接口全部初始化过,真正使用时才初始化。
(1)加载过程
1. 通过类的全限定名获取此类的二进制字节流
2. 将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构
3. 在内存中生成一个代表这个类的java类的对象作为访问入口,作为方法区这个类的各种数据的访问入口。
(2)验证
确保Class文件的字节流中包含的信息符合虚拟机的要求,不会危害向虚拟机安全,防止系统崩溃
- 文件格式验证
- 元数据验证(字节码描述进行语义分析)
- 字节码验证(最复杂)通过数据流和控制流分析,确定语义合法(通过验证也不代表安全)
- 符号引用验证 符号引用转为直接引用,确保解析动作能正常执行
(3)准备
正式为类变量分配内存并设置类变量初始化(没赋值,只是初始化),这时候进行内存分配的仅包括类变量(static 修饰变量),不包括实例变量没实例变量将会在对象一起分配在Java堆中
(4)解析
虚拟机将常量池内的符号引用转换为直接引用
符号引用:以一组符号描述所引用的目标,符号可以是任何形式的字面量
直接引用:直接引用可以是直接指向目标的指针,相对偏移量或一个能简介定位到目标的句柄
解析动作主要针对(类或接口),(字段),(类方法),(接口方法),(方法类型),(方法句柄),(调用点限定符)7类符号引用进行。
(5)初始化
执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集器中所有类变量赋值动作和静态语句块语句合并产生的。
不需显示调用父类构造器,父类的<client>()方法共执行;只有一个线程执行这个类<client>()方法
对于任意一个类,需要由加载它的类加载器和这个类本身一同确立其在Java虚拟化中的唯一性,即使两个类来源同一个Class文件,被同一个虚拟机加载,只需要类加载不同,这两个类必然不相等。
类加载器:
BootStrap Classloader:加载\lib目录下的java核心类库,不可以直接使用
Extension Classloader:加载\ext目录下地java扩展类库
Application Classloader:系统类加载器,加载ClassPath里指定的类库
双亲委派:防止在内存中出现多份同样的字节码
过程:特定的类接收加载类请求时,首先将加载任务委托给父亲加载器。依次递归,如果父亲加载器完成,则成功返回。无法加载,才自己加载。
破坏双亲委派:
- 上下文加载器请求加载SPI代码,父类加载器请求子类加载器完成类加载动作
- 追求程序动态性导致
代码热替换,模块热部署
OSGi:模块化热部署,每个程序模块有自己的类加载器,当更换一个Bundle时,就把Bundle连同类加载器一起实现热部署。
栈桢:方法调用,方法执行的数据结构
存储了方法的局部变量、操作数栈、动态连接、方法返回地址等信息
栈桢结构:
(1) 局部变量表: 变量值存储空间
以变量槽(slot)为最小单位,虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的slot数量(slot可重用)
(2)操作数栈:
在方法执行过程中,开始时操作数栈是空的,随后会有各种字节码指令往其中写入和提取内容。两个相互独立的栈桢会出现一部分重叠。
(3)动态连接:指向运行时常量池中该栈桢所属方法的引用
- 静态解析:类加载阶段或第一次使用时转化为直接引用。
- 动态连接:每一次运行期间转化为直接引用。
(4)方法返回地址
方法退出时的操作:
- 恢复上层方法的局部变量表和操作数栈
- 把返回值压入调用者栈桢的操作数栈中
- 调整PC计数器的值以指向方法调用指令后面的一条指令
方法调用
目的:确定被调用方法的版本
静态方法、私有方法、实例构造器、父类方法在类加载时会把符号引用解析为该方法的直接引用。
(1)解析调用
(2)分派
Human man = new Man();
静态类型 实际类型
- 静态分派:方法重载,编译期可知重载版本(重载的本质)
- 动态分派:方法重写,运行期决定执行版本(重写的本质)
宗量:方法接受者,方法的参数
- 单分派:根据一个宗量对目标方法选择
- 多分派:根据多个宗量对目标方法选择
动态分派实现:需方法表,如果子方法未重写,入口地址和父类地址相同;如果重写,子类方法表地址指向子类版本入口地址。
早期(编译期)优化
Java编译过程
- 解析与填充符号表:(1)词法、语法分析(AST)(2)符号表中信息在不同阶段用到
- 插入式注解处理器的处理
- 分析与字节码生成
- 标注检查:检查内容是否已被声明,变量与赋值之间数据类型是否匹配
- 数据及控制流分析:对程序上下文逻辑进一步验证
- 解语法糖:泛型,变长参数等方便程序员使用的语法
- 字节码生成:生成class文件
Java语法糖:
泛型:Java中的泛型是违泛型,类型擦除后无类型膨胀。由于List<String>和List<Integer>擦除后是同一类型,引入Signature存储方法在字节码层面的特征签名来完成区别。
Java类型擦除的原因:
(1)避免JVM的重构。如果JVM将泛型延续到运行期,那么运行期时JVM就要进行大量的重构工作
(2)版本兼容,可以更高地支持原生类型(Raw Type)
自动装箱、拆箱与遍历循环
条件编译:把分支中不成立的代码块消除掉
晚期(运行期)优化 (字节码-->本地机器码)
JIT编译器:当某个方法或代码块运行特别频繁时,热点代码编译成本地平台相关的机器码
虚拟机包括:解释器 & 编译器
- 解释器:迅速启动和执行、省去编译时间
- 编译器:逐渐发挥作用,把越来越多的代码编译成本地代码
Hotspt内置C1编译器 和 C2编译器
Hotspt分层编译:
第0层,解释执行,不开启性能监控功能
第1层,C1编译,将字节码编译成本地代码,进行简单可靠的优化
第2层,C2编译,启动一些编译耗时较长的优化
热点代码:多次调用的方法;被多次执行的循环体
即时编译的出发条件(热点探测)
- 基于采样的热点探测
- 基于计数器的热点探测(阈值检测:方法调用计数器,回边计数器)
逃逸分析:分析对象动态作用域(方法逃逸、线程逃逸)
Java即时编译 与 c/c++静态编译
- 即时编译占用用户程序的运行时间
- Java是动态的类型安全语言,虚拟机需频繁动态检查,耗时
- Java多态选择大于c/c++,优化难度大于c/c++
- 动态扩展语言,会改变类的继承关系
- Java内存分配是在堆上进行,c/c++则有多种内存分配方式
Java语言的劣势是为了换取开发效率优势付出的代价
Java优势:别名分析,c/c++以运行期性能监控为基础的优化无法进行
JVM常见参数:
- 最小堆内存:-Xms
- 最大堆内存:-Xmx
- 堆的比例分配(设置eden空间与s0空间的比例大小):-xx:SuriviorRatio
- 老年代与新生代比例:-xx: NewRatio
- eden空间与surivior区比例:-xx:SurvivorRatio
- 新生代:-Xmn 2g
- 永久代:-xx: Permsize=512m -xx:MaxPersize=512m
- 大对象进入老年代的阈值:-xx:PretenureSizeThreadhold
- 本机直接内存:-xx:DirectMemorySize=100m
- 虚拟机栈:-Xss 256k
- 打印日志:-Verbose:gc ...
- OMM保留现场日志:-xx:HeapDumpOnOutOfMemoryError
- 打印GC信息:
-xx:+PrintGC
-xx:+PrintGCDetail
-xx:+PrintGCStamps
-xx:+PrintTenuringDistribution
-xx:+PrintHeapAtGC
- 类和对象跟踪:
-xx:+TraceClassLoading 类加载情况
-xx:+TraceClassUnloading 类卸载情况
- 堆快照:-xx:+HeapDumpOnOutOfMemoryError -xx:HeapDumpPath
- 垃圾回收停顿最大毫秒数:-xx:MaxGCPauseMills =10
- 垃圾收集器占比:-xx:GCTimeRatio = 49
- GC自适应调节策略:-xx:UseAdaptiveSerzPolicy
- -xx:UseParallelGC
- -xx:UseParallelOldGC
- 设置垃圾收集线程数:-xx:ParallelGCThreads=4
- 老年代占比收集:-xx:CMSIntiatingOccupanayFraction = 8