第一部分:java的发展史,jvm的发展史
java的出现:Green Project,开发在电子产品上运行的程序架构
jvm迭代:Classic Vm -> Exact Vm ,HotSpot Vm (解释器跟编译器的混合执行)
jdk跟jre:java开发工具(java语言,java api,jvm),java运行环境(java se api,jvm)
java的优势:结构严谨,面向对象,内存管理与指针越界管理,热点代码检测,运行时编译及优化,一次编写,到处运行,庞大而完整的生态
联想:
- java9,10,11,12,13新特性:模块化,集合工程,各种增强,局部类型推断var关键字,加入json解析api,httpclient等
问题:
- 为啥解释器跟编译器混合执行就牛逼了?
第二部分:jvm的内存管理
主要内容:
jvm的内存划分
程序计数器:记录指令执行
虚拟机栈:
-Xss
java方法执行时局部变量与栈帧的分配,请求的栈深度过大-StackOverFlowError,无法再申请内存分配栈帧-OutOfMemoryError
本地方法栈:
-Xos
本地方法执行时的内存分配,再hotsopt中,不区分本地方法栈与虚拟机栈
java堆:
-Xms 最小大小,-Xmn最大大小
常规对象分配的地方
OutOfMemoryError java heap space
方法区:
-XX:MaxPermSize
类元数据+运行时常量池
OutOfMemoryError perm space
直接内存区:
- 在虚拟机外分配使用的内存,常见于NIO中使用,避免内存复制
对象的创建
对象内存的分配
指针碰撞,计算对象大小,划分内存,要考虑同步
TLAB,在线程自己的内部空间里面分配
需要结合GC方式
对象的内存布局
对象头:hash code,分代年龄,锁状态,类型指针(确定对象是哪个类的实例)
对象实例数据
对齐空间
垃圾回收
对象回收:
先判断对象是否已死:
引用计数法,无法识别两个死亡对象互相引用的情况
可达性分析,通过枚举Gc Roots,如:常量引用,静态变量引用,栈上的引用
回收时机:安全点
回收算法
复制算法:内存消耗大
分代整理:基于复制算法的思路
标记整理:
标记清除:内存碎片
回收器
新生代
serial:stop the world
ParNew:stop the world ,回收多线程并行
Parallel Scavenge:类似于ParNew 更注重于控制吞吐量
老年代
serial old:stop the world
parallel old:stop the world ,回收多线程并行
cms:
标记:stop the world
并发标记:并发
重标记:stop the world
并发回收:并发,回收时,用户进程也在进行,所以会产生浮动垃圾。所以内存上需要留有余地
G1 能适用整堆上的垃圾回收
故障排查,性能分析工具
bin目录下的自带工具
jconsole,visualvm
调优案例总结
高性能硬件
采用逻辑集群+负载均衡的方式来发挥高性能硬件的优点
回收过大的内存区域,会导致GC时间过长
集群通讯导致大对象累积在内存中
使用NIO。DirectMemory的回收直到full gc才会顺便回收,当堆外内存无法分配也会导致OutOfMemory
Runtime.exec()会创建系统进程,要慎用
依赖其他的远程服务太过耗时,导致线程,Socket挂起,直到jvm崩溃
数据结构导致内存翻倍,比如long 占用8B,Long 占用24B
桌面程序最小化之后,工作内存转移到磁盘,恢复的时候可能导致不正常的GC
联想:
jdk8新增MetaSpace,将类元数据从方法区中移动到meta space
问题:
第三部分:
主要内容:
平台无关性:java通过虚拟机+字节码文件实现了平台无关性,jvm不仅能识别java编译器生成的字节码文件,还有很多语言在字节码文件的基础上,实现了自己的编译器,把自己的语言规范编译成字节码文件交给jvm执行。字节码文件有自己一系列的定义,比如,文件开头1-4字节为魔数,接下来的5-8字节为版本号,9-10字节代表常量池,同时定义了很多结构体,跟固定的协议,来解析class文件,其中方法体的代码是通过编译成jvm 指令存放到code属性里面
类加载的时机
new,类变量的访问与操作,静态方法调用
对类反射调用
初始化一个类的时候,若父类没有初始化,要先触发父类的初始化
虚拟机启动时,main方法所在的主类
动态语言支持如MethodHandler实例解析结果的方法句柄对应的类没有初始化(类似于反射)
类的加载过程
加载:将字节码的码流加载入jvm
验证:
格式验证(魔数,版本等),通过后码流便按结构进入了方法去
元数据验证
字节码验证(程序语义验证)
符号引用验证
准备
类变量的分配内存空间
设置类变量初值
static 变量 0,false,null等
static finnal 常量 显式的初始值
解析
- 符号引用替换成直接饮用
初始化
- 执行cinit(编译器自动收集所有类变量的赋值动作跟static静态代码块),并发问题由jvm控制,多线程并发也保证只执行一次,与init不同,init方法上实例构造器
使用
卸载
类加载器
Bootstrap ClassLoader 加载java_home下lib目录的jar
Extension ClassLoader 加载java_home下lib/ext目录的jar
类+类加载器唯一确定一个类
双亲委派模型,越是基础的类,越由上层加载器加载
双亲委派模型的破坏
jndi,jdbc等,需要上层类加载器主动要求下层的类加载器帮忙加载
osgi:模块化
字节码执行引擎
栈帧
局部变量表
操作数栈
动态连接
方法返回地址(恢复调用者的栈帧)
附加信息
方法调用
解析(方法的调用版本在运行期是不可以改变的,这类方法的调用叫解析,如:静态方法,私有方法,这些方法在类加载的解析过程中,会把符号引用替换为直接引用)
分派
静态分派:依赖静态类型定位方法执行版本如:方法重载,在编译期就能确定
动态分派:在运行期根据实际类型确定方法执行版本如:子类重写父类方法,invokevirtual指令会在实际类型中找方法
java的动态语言特性支持
动态类型语言:在运行期才会去做类型检查,所以c,c++,java本身是一门静态类型语言
jdk7 提供了invokedaynamic指令,invoke包 执行方法句柄参数
基于栈的执行引擎
物理机大多基于寄存器做执行引擎,速度快,但与硬件耦合太紧
虚拟机基于栈的架构,速度稍慢,可移植性强
联想:
问题:
第四部分:程序编译与代码优化
主要内容:
编译期优化
语法糖
- 范型,增强for循环,自动拆装箱,
运行期优化
解释器:把字节码解释机器吗
编译器:把整个方法,或者某段循环的代码,进行优化编译成机器码
编译优化:
公共子表达式消除
数组边界检查消除(如果99%的情况不回越界,可以不判断越界情况直接使用,并处理越界异常,同样适用于NPE)
方法内联:如果在运行时不回出现多个方法版本,可以将代码内联,减少栈分配与复原
逃逸分析:如果能确定对象不回逃逸出某个范围,则可以使用栈上分配,同步消除,标量替换
联想:
问题
第五部分:高效并发
主要内容:
java内存模型
内存操作
lock
unlock
read:从主内存read到工作内存
load:从工作内存load到变量副本
use:工作内存到执行引擎
assign:赋值给工作内存的变量
store:工作内存的变量值store到主内存
write:把主内存的值放入到主内存的变量中
volatile的使用
语义:防止指令重排,变量的可见性
使用的约束:不依赖当前值或者只有单一的线程修改,不与其他的状态变量参与到不变约束
变量规则(1-4 实现可见,5,6实现防止指令重排)
use 之前 必须 load
load 之后 必须 use
store 之前 必须 assign
assign 之后 必须 store
A->V :use + assign,P->V:read+write,B ->W: load+store,Q -> W: read+write
lock 空操作的内存屏障
先行发生原则:
程序次序规则:一个线程内,书写在前面的先行发生于书写在后面的
管程锁定:unlock 先行发生于后面对同一个锁的lock
volatile:写操作先行发生于后面的读操作
线程终止:线程中的所有操作先行发生于对此线程的终止检测
线程中断:对线程interrupt调用先行发生于对中断的检测
对象终结:对象初始化限行发生于对象finalize方法
传递性:A先行发生于B,B 。。。 C 则 A。。。C
java线程
jdk1.2之前使用用户线程实现,1.2之后使用操作系统的的原生的线程模型实现
线程调度
New
Ruing
Waiting/Timed Waiting
Blocked
Terminated
线程安全与锁优化
自旋锁:在锁能很快释放掉的情况下,可以通过自旋在不放弃cpu的情况下,减少线程调度的开销
自适应自旋:通过统计上一次的自旋情况来决定是否要自旋,还是挂起等待
锁消除:局部对象的引用安全,没有逃逸等,可以使用锁消除
锁粗化:多次重复对同一个锁操作,可以扩大锁范围
轻量级锁:使用cas方式加锁
偏向锁:在持有锁之后,没有竞争
联想:
解决java中的并发问题,其实变成是捋清楚内存操作的顺序
问题: