JAVA虚拟机
绿色部分是所有线程共享的运行期数据区
白色部分是每个线程独享的运行期数据区
执行引擎负责在不同的操作系统中将代码转换为本地代码执行
方法区主要存放从硬盘加载进来的类字节码,静态变量是放在方法区的。
堆存放在程序执行过程中创建的类的实例。
程序运行时是以线程为单位运行的
当JVM进入启动类的main()时,就会创建一个主线程,main()里面的代码会被这个主线程执行。
每个线程都有自己的栈,栈里面存放方法运行期的局部变量。
当前线程执行到哪一行字节码指令,则存储在程序计数寄存器中
Java字节码文件
class字节码文件的开头8位:cafe babe
Java所有的指令有200个左右,一个字节(8位)就可以存储256种不同的指令信息,一个这样的字节就被称为字节码(ByteCode)
在代码执行过程中,JVM会把字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态地编译成机器码,提高执行效率
字节码执行流程
字节码文件编译过程
语法树
类加载器的双亲委派模型
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。
如果低层次的类加载器想要加载一个未知类,需要向上级类加载器确认,如果上级没有加载过这个类,且也同意加载的时候,才会让当前类加载器加载这个类。
自定义类加载器
自定义类加载器使用的场景:
- 隔离加载类,同一个JVM中不同组件加载同一个类的不同版本。不同加载器生成的类是不同的。
- 扩展加载园,实现从网络,数据库等加载字节码
- 字节码加密:实现按照自定义的加密算法将字节码加密解密
如何实现:
堆:
- 每个JVM实例唯一对应一个堆。
- 应用程序在运行中创建的所有类实例或数组都存放在堆中
- 应用的所有线程共享这个堆
堆栈:
- JVM为每个新创建的线程分配一个堆栈
- Java中所有对象的存储空间都是在堆中分配的,但对象的引用却是在堆栈中分配。也就是说在建立一个对象的时候,要做两个操作。一是在堆中实际建立这个对象,二是在堆栈中分配内存,存放指向这个堆对象的引用。
- 程序运行时,实际上是以线程为单位运行的。所以说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。
方法区和程序计数器
java线程栈
所有在方法内定义的基本类型变量,都会被每个运行这个方法的线程放入到自己的栈中,线程之间彼此隔离,所以这些变量一定是线程安全的。
线程工作内存和volatile
volatile 解决什么问题?
正常情况下,Java在多线程情况下,线程操作主内存变量需要通过线程独有的工作内存拷贝主内存变量副本来进行。所以,不同线程中对变量修改后会存在不一致的情况。
volatile 使得当一个线程修改了某个变量的值以后,这新值对其他线程来说是立即可见的。
Java整体的运行环境
JVM的垃圾回收
JVM垃圾回收通过一种 可达性分析算法 进行垃圾对象的标识,最终是为了 将JVM堆中已经不再被使用的对象清理掉以释放内存
要解决的四个问题:
- 怎么知道哪些对象要回收
- 从线程栈帧中的局部变量或者方法区中的静态变量出发
- 将引用了这些变量的对象进行标记
- 看这些被标记的对象是否引用了其他对象,如果有继续标记
- 这样所有被标记过的对象都是被使用的对象,而没有被标记的就是可回收的垃圾对象了
- 如何回收;三种回收的手段
- 清理
- 将垃圾对象占据的内存空间标记为空闲,记录在一个空闲列表中
- 并不是真正地将这些内存清理掉
- 当应用程序需要创建新对象时,就从空闲列表中找一段空间的内存分配出去
- 压缩
- 从堆内存的头部开始,将存活的对象拷贝到一段连续的内存空间中,那么剩下的就是连续的内存空间了
- 复制
- 将堆空间分为两部分,只在其中一个中创建对象
- 当这部分空间用完,将标记过的对象复制到另一个空间中
- 回收中的内存如何管理
- 用什么的过程进行回收
分代垃圾回收
大部分的对象生命周期较短,所以把老年代区设置比Eden区要大很多,Eden区小更方便垃圾回收的遍历。
- 在Eden区中申请空间创建对象
- Eden满了以后,遍历Eden区判断哪些对象可回收,启动young GC
- 将标记的对象拷贝到from区,Eden区变空
- 一段时间后Eden区满,将Eden区和From区的标记对象拷贝到To区,From区和Eden区变空
- 再一段时间后Eden区满,将Eden去和To区的标记对象拷贝到From区,To区和Eden去变空
- 多次拷贝后仍被标记的对象,就会被拷贝到老年代
如果Eden区和To区往From区拷贝时出现空间不不足的情况,会对触发Full GC。
JVM垃圾回收器算法
- 串行回收器:单独启动一个线程专门用于垃圾回收,当GC时所有线程都停止工作『stop the world』,等待这个线程的GC完成
- 并行回收器:并行是指可以启动多个线程进行垃圾回收;当GC发生时,仍然会『stop the world』,但可以根据cpu核心数,启动多个线程进行GC,效率更高。
- 并发垃圾标记清理(CMS):并发是指GC线程和业务线程可以同时工作,同时也就意味着可能会出现重标记,重标记期间还是会发生『stop the world』,比并行回收器,对业务线程影响较小,但更好资源。
- G1回收器:jdk1.7高版本开始引入G1
G1垃圾回收机制
-XX:MaxGCPauseMillis 最大允许的stopTheWorld毫秒时间
Java启动参数
标准参数:所有JVM都需要实现的参数,而且向后兼容
非标准参数:默认jvm实现这些参数,但不保证所有jvm实现,且不保证向后兼容
服务器中Xms和Xms设置相同
JVM性能诊断工具
- JPS
- 可以找出运行的jvm进程id,即lvmid,然后进一步使用其他工具来监控和分析JVM
- JSTAT
- 对java应用程序的资源和性能进行实时的命令行的监控,包括对Heap size和垃圾回收状况的监控。
- JMAP
- 可以输出内存中对象的工具,可以将VM中的heap以二进制的形式输出文本
- JSTACK
- 查询jvm中的线程堆栈信息
- 其他可视化的工具
- JConsole
- JVisualVM
Java代码优化
合理并谨慎使用多线程
启动线程数=【(任务执行时间/(任务执行时间 - IO等待时间)】* CPU内核数
考虑阻塞时间,如果阻塞时间为0,启动线程数 = CPU内核数
竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,出现问题是因为多个线程同时访问了相同的资源。
当多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称为竞态条件。
导致竞态条件发生的代码区,叫临界区。
在临界区中使用适当的同步就可以避免竞态条件。
Java线程安全
允许被多个线程安全执行的代码称为线程安全的代码。
- 方法局部变量
- 局部变量存储在线程自己的栈中
- 每个线程的栈是独立的,所以方法内的基础局部变量是线程安全的
- 方法局部的对象引用
- 如果方法内的对象不会逃逸出该方法,也就是说不会传递出去,就是线程安全的
- 对象成员变量
- 对象成员存储在堆内存中
- 当两个线程更新同一个对象的同一成员,这个代码就不是线程安全的。
ThreadLocal
ThreadLocal能保证每个线程获取到的值与之前set的值是相同的。
实现原理:
- Thread.currentThread()
- map是线程自己的变量
Java内存泄漏
Java内存泄漏是由开发人员测错误引起的。
如果程序保留对永远不会再被使用的对象的引用,这些对象将占用并耗尽内存
- 长生命周期的对象
- 静态容器,Map、list
- 静态变量不会被回收
- 静态变量引用的对象不会被回收
- 解决方法:弱引用对象
- 缓存
合理使用线程池和对象池
- 复用线程或对象资源,避免在程序中创建或删除大量对象
- 池管理算法(记录哪些对象是空闲的,哪些是在使用的)
- 对象内容清除(ThreadLocal的清空)
使用合适的容器
- linkedList和ArrayList的区别及适用场景
- HashMap的算法实现及应用场景
- 使用concurrent包
缩短生命周期,加速垃圾回收
- 减少对象驻留内存的时间
- 使用完对象后释放,将变量置为null
高性能秒杀案例
秒杀一种营销活动,可以以有限的投入(少量的商品)吸引大量的用户。
秒杀活动最早起源于并不太知名的淘宝,对现今已有海量用户的系统,并不太适用。
如果在现有系统的基础上改造,需要协调各个项目组的资源和配合,最高效的方式就是重新写一个新的系统
设计原则
- 页面静态化
- 商品是固定的,但商品信息可能是变化的
- 使用js自动更新技术将动态页面转换为静态页面
- 并发控制,防秒杀器
- 控制并发阀门,只放最前面一部分进入秒杀系统
- 简化流程
- 去掉不必要的业务流程。
- 下单页面的所有数据库查询
- 收货地址填写去掉,秒杀成功后再填写
- 去掉是否开通支付接口的判断,做前置判断·
- 以下单成功作为秒杀成功标识。支付流程在 1天内完成即可。
- 下单后的流程复用现有系统的逻辑
- 去掉不必要的业务流程。
- 前端优化
- 采用YSLOW原则提升页面响应速度
实现
静态化如何实现
秒杀前10分钟 才能看到商品信息
如何判断秒杀是否开始
valid-offer.js 空文件,每次刷新返回商品id
三道阀门的设计
大部分的请求会被挡在第一道阀门外,最终只可能最高有56个并发进入业务系统
秒杀器的预防
扩展
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩
计算机领域的任何问题都可以通过增加一个中间层(虚拟层)来解决
linkedList和ArrayList的区别及适用场景
HashMap的算法实现及应用场景
ConcurrentHashMap和HashMap的线程安全特性有什么不同
应该考虑作为一个架构师,面对一个新的技术或框架,考虑为什么要怎么设计,这么设计是为了解决什么问题,是怎么解决的。应该和作者站在同一高度去思考而不是只单纯的为了去学习。
架构师要有自己完整的体系
不应该是学会了什么知识去传授的,而是实现了什么去分享的。知识人人都可以去学习,但具体工作不是人人可以实现的。
新建一个系统是低投入高回报的,维护一个旧的系统则是相反的。
学习知识只是开始,重点是要在重大机会前适当的应用。