JVM 架构 && 秒杀总结
一、JVM虚拟机架构
组成架构
Java是一种跨平台的语音,JVM屏蔽了底层系统的不同,为Java字节码文件构造了一个统一的运行环境。
绿色区为所有线程共享的运行期数据区
白色区域为每个线程独享的运行期数据区
举个例子:
1.类加载器将.class文件加载到JVM虚拟机内部,把它放到方法区中。
2.加载到方法区之后就可以在当前线程或者是新启动一个线程。每进入一个新方法,就为新方法创建一个栈帧,这个栈帧放到堆栈的最顶部。当前的方法就一定在最顶部的栈帧上执行。所以Java栈是线程独有的。
3.在方法区的某个方法,如果是new了一个classA,那这个classA是放到了堆里面的。方法一行一行的向下执行,下一行代码执行哪一行代码是直接放到了程序技术寄存器里面的。
4.方法区中放的是Java自己的指令,这种指令操作系统是不认识的,因此和系统交互的时候就需要一个执行引擎,执行引擎会把Java字节码翻译成机器语言来执行。
字节码文件
执行流程
类加载器的双亲委托模型
低层次的当前类加载器,不能覆盖更高层次的加载器已经加载的类。如果低层次的类加载起想加载一个未知类,需要上级类加载器的确认,只有当上级类加载器没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类。
自定义加载器
隔离加载类:同一个JVM中不同组件加载同一个类的不同版本。
扩展加载源:从网络、数据库等处加载字节码。
字节码加密:加载自定义的加密字节码,在ClassLoader中解密。
通常用来隔离版本
堆 & 栈
方法区 & 程序计数器
Java(线程)栈
所有在方法内定义的基本类型变量,都会被每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所有这些变量一定是线程安全的。
线程工作内存&volatile
Java内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。
一个共享变量(类的成员变量,类的静态成员变量) 被volatile修饰之后,保证了不同线程堆这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其它线程来说是立即可见的。
Java的整体运行环境
JVM的垃圾回收
可以想象垃圾回收要解决4个方面的问题
1.怎么知道哪些对象要回收 2.如何回收 3.回收过程中的内存空间如何管理 4.用什么样的过程进行回收
JVM的垃圾回收就是将JVM堆中的已经不再使用的对象清理掉,释放宝贵的内存资源。
JVM通过一种可达性的分析算法进行垃圾对象的识别,
具体过程是:从线程栈帧中的局部变量,或者是方法区的静态变化出发,将这些变量引用的对象进行标记,然后看这些被标记的对象释放引用了其它对象,继续进行标记,所有被标记过的对象都是被使用的对象,而那些没有被标记的对象就是可回收的垃圾对象了。
标记完之后,JVM就会对垃圾对象占用的内存进行回收,回收主要有三种方法:
1.清理, 将垃圾对象占据的内存清理掉,其实JVM并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间
标记为空闲,记录在一个空闲列表中,当应用程序需要创建新对象的时候,就从空闲列表中找一段空闲的内存分配给这个新对象.
2.压缩:从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,那么其余的空间就是连续的空闲空间。
3.复制:将堆空间分成两部分,只在其中一部分创建对象,当这个部分空间用完的时候,将标记过的可用对象复制到另一个空间中
JVM分代垃圾回收
在堆中分成两块儿
新生代
分成3个区 Eden区,From区和To区
1.当要创建一个对象的时候先在Eden区创建,当Eden区被占满了以后就会启动一次新生代的垃圾回收
叫YoungGC,会去遍历Eden里的对象,看哪些是有引用的,把还有引用的对象拷贝到from区里去。
拷贝到From区之后,Eden区就空下来了,之后就可以继续new对象了。再new满了后,会对Eden区和
From区里的对象再做一次是否还有引用的标记,把还在使用的对象一起拷贝到To区去。
拷贝过去之后Eden和From区就都空了,之后就可以继续new对象了。
2.经过多次的Eden+From或Eden+To区的转移之后,如果对象引用还在,这些对象就会被拷贝到老年代。
3.在运行一段时间之后在Eden和From区To区都无法再转移的时候,就会进行一次全量的垃圾回收。
老年代
绝大部分对象的生存周期都是很短暂的,new出来之后,用这个对象做一些处理,处理完了之后,这个对象就失去引用了。
举个例子: 用户并发的请求进来之后,每个并发的请求都要去创建一个对象,处理完了之后返回了,刚刚创建的这些对象就已经失去引用了,
也就成了可以被回收的对象了。
垃圾回收器算法
垃圾回收器是一个或多个线程承担的清理,压缩复制这些动作的
1.串行垃圾回收器
应用程序是在多线程执行的,在执行过程中,内存空间不足了。就会先暂停这些线程(stop-the-world),然后启动一个
垃圾回收线程,这个线程就会去标记还在使用的对象,把还在被引用的对象拷贝,把没有引用的对象清理掉。
在进行垃圾回收的时候,所有的应用线程都不能够执行。
在Java早期的时候,那个时候都是单cpu的,本来一个机器就有一个线程,stop-the-world后整个世界都给你了,启动
多个线程也没有意义了
2.并行回收器
多核时代只有一个核心去进行垃圾回收,效率显然就太低下了。
这个时候还是stop-the-world应用线程全部暂停,然后启动和cpu核心数相符的若干个线程执行垃圾回收。
在早起的大数据计算领域,因为并不在乎执行时间,所以才有这种垃圾回收机制的比较多。
3.并发回收器CMS(并发标记清理)
分成了4个阶段,一开始的时候启动一个线程进行初始化标记。这时从一些根,比如静态变量,线程,线程栈,从这些对象
根开始标记这些根包含的对象,所以很快就可以标记完了。
并发标记的时候,应用程序的线程和垃圾回收的线程会并发执行。这时是有可能标记不全。
因为上一步有可能标记不全,这时还是需要一个stop-the-world,这个时候世界会非常短的,暂停后再进行重标记。
第4步会进行并发的清理
在早期的时候web应用使用的都是这种垃圾回收机制,这种垃圾回收机制其实比并行回收器需要的计算资源要多很多的。
4.G1回收器
在JDK 1.7开始逐渐的转移到G1回收器算法,这种事目前的主流
它的原理是把内存空间,分割成若干个region区域
分别是 Eden, Survivos(也就是之前的From和To区),old老年区和Humongous(大对象区)
缺醒情况下会分成2000个小的区域
Java启动参数
Java性能诊断工具
基本工具: JPS , JSTAT, JMAP,JSTACK
集成工具: JConsole, JVisualVM
JPS
JSP用来查看host上运行的所有java进程的pid(jvmid),一般情况下使用这个工具的目的只是为了找出运行的 jvm进程ID,即lvmid,然后可以进一步的使用其它的工具来监控和分析JVM
常见的几个参数 :
1.-I 输出java应用程序的main class的完整包
2.-q 仅显示pid,步显示其它任何相关信息
3.-m 输出传递给main方法的参数
4.-v 输出传递给JVM的参数,在诊断JVM相关问题的时候,这个参数可以查看JVM有关参数的设置
JSTAT
JSTAT(“Java Visual Machine statistics monitoring tool”)是JDK自带的一个轻量级小工具。主要对Java应用程序的资源和性能进行实时的命令行
监控,包括了对Heap size和垃圾回收状况的监控。
语法结构如下: jstat [Options] vmid [interval] [count]
Options — 选项,我们一般使用 - gcutil查看gc情况
vmid — VM的进程号,即当前运行的java进程号
interval — 间隔时间,单位为毫秒
count — 打印此时,如果缺省则打印无数次
JMAP
JMAP是一个可以输出所有内存中对象的工具,甚至可以将VM中的heap,以二进制输出成文本。
使用方法:
jmap -histo pid > a.log 可以将其保存到文本中去,在一段时间后,使用文本对比工具,可以对比出GC回收了哪些对象。
jmap -dump:format=b,file=f1 PID可以将该PID进程的内存heap输出出来到f1文件里
JSTACK
Jstack可以查看jvm内的线程堆栈信息
JConsole
JVisualVM
合理并谨慎使用多线程
使用场景(I/O阻塞,多CPU并发)
资源争用与同步问题 java.util.concurrent
启动线程数 = [ 任务执行时间/(任务执行时间 - IO等待时间) ] * CPU内核数
最佳启动线程数和CPU内核数量成正比,和IO阻塞时间成反比。如果任务都是CPU计算型任务,那么线程数最多步超过CPU内核数,
因为启动再多线程,CPU也来不及调度。
相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,并提高系统吞吐能力,改善系统性能。
竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。
当两个线程竞争同一个资源时,如果对资源的访问顺序敏感,就称为存在竞态条件, 导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件。
Java线程安全
允许被多个线程安全执行的代码称作线程安全的代码
方法局部变量
局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。
方法局部的对象引用
如果在某个方法中创建的对象不会逃逸出该方法,那么它就是线程安全的。
对象成员变量
对象成员存储在堆上,如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
ThreadLocal
Java内存泄露
Java内存泄漏是由于开发人员的错误引起的。
如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存。
1.长生命周期对象
2.静态容器
3.缓存
合理使用线程池和对象池
复用线程或对象资源,避免在程序的生命期中创建和删除大量对象
池管理算法(记录哪些对象是空闲的,哪些对象正在使用)
对象内容清除(ThreadLocal的清空)
使用合适的JDK容器类(顺序表,链表,Hash)
LinkList和ArrayLIst的区别及适用场景
HashMap的算法实现及应用场景
使用concurrent包,ConcurrentHashMap和HasmMap的线程安全特性有什么不同
缩短对象生命周期,加速垃圾回收
在使用时创建对象,用完释放
创建对象的步骤(静态代码段 - 静态成员变量 - 父类构造函数 - 子类构造函数)
合理使用单例模式
线程安全
二、秒杀系统总结
1.面临的挑战及解决
1.对现有网站业务造成冲击
秒杀活动只是一个附加活动,这个活动具有时间短,并发访问压力大的特点。
因此不能和现有的网站部署在一起,因为部署在一起了会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
2.高并发下的应用,数据库负载
用户在开始前会进行不断的刷新页面以保证不错过秒杀,这些请求不能按照动态的网站来进行设计,要尽量的静态化,降低应用和数据库的负载
3.突然增加的网络及服务器带宽
假设商品页面200K(主要是图片的大小),那么需要的网络和服务器带宽是2G(200k*并发数),这些网络带宽是因为秒杀活动新增的,要超过网站的平时的带宽。
因此要考虑增加网络带宽,同时动静分离(将图片放到CDN)来减轻服务器的压力
4.直接下单
下单页面也是一个普通的 URL,如果得到这个 URL,不用等到秒杀开始就直接下单了。这样是不行的,因此要将这个URL变成携带随机变量的形式。
2.核心的架构方案
1.页面动静分离(将图片放到CDN)来减轻服务器的压力
2.增加临时带宽
3.下单页面URL动态化,在开始时才输出给客户端
4.设计一个阀门用于点击"秒杀商品"按钮后的计数器,计数器的逻辑如下图,计数器的目的是为了放过允许的流量请求。
评论