写点什么

JVM 架构 && 秒杀总结

用户头像
Lane
关注
发布于: 2020 年 08 月 05 日

一、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.设计一个阀门用于点击"秒杀商品"按钮后的计数器,计数器的逻辑如下图,计数器的目的是为了放过允许的流量请求。



用户头像

Lane

关注

还有梦想 2018.07.05 加入

还未添加个人简介

评论

发布
暂无评论
JVM架构&&秒杀总结