架构师训练营:第九周总结

用户头像
zcj
关注
发布于: 2020 年 08 月 02 日

JVM虚拟

JVM 组成架构

Java 是一种跨平台的语言,JVM 屏蔽了底层系统的不同,java源文件可以由JVM解析为 Java 字节码文件,JVM为Java字节码文件构造了一个统一的运行环境。

java字节码文件编译过程:



Java 字节码执行流程:

Java字节码文件内容都是字节码。Java 所有的指令有200个左右,一个字节码(8位)可以存储256中不同的指令信息,一个这样的字节称为字节码(Bytecode)。在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态的编译为与操作系统相关的机器码,提高执行效率。字节码内容如图:

其中开头 cafa baba是java字节码文件特定的魔法值,表示文件是一个java字节码文件,0000 0037表示jdk的版本号。

java字节码文件的执行流程:



类加载的双亲委托模型

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,需要上级加载器类确认,只有当上级记载器类没有加载过这个类,也允许加载的时候,才让当前类加载器加载这个未知类。目的就是为了防止一个类重复加载,一个类多个版本同时被加载。



类加载器的种类:

  1. 启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

  2. 扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

  3. 应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

  4. 自定义加载器:用户自己定义的类加载器。

自定义类加载器的场景:

1、隔离加载类:同一个JVM中不同组件加载统一个类的不同版本。

2、扩展加载源:从网络、数据库等处加载字节码。

3、字节码加密:加载自定义的加密字节码,在ClassLoader中解密

JVM的内存模型:



JVM 堆 & 栈 &方法区 & 程序技术器

堆:每个JVM实例唯一对应一个堆。应用程序在运行中所创建的类的实例或数组都放在这个堆中,并有应用所有的线程共享



堆栈:JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。



方法区&程序计数器:方法区主要存放从磁盘加载进来的类字节码,二在程序运行过程中创建的类实例则存放在堆里。程序运行的时候,实际上是以线程为单位运行的,当JVM进入启动类的main方法的时候,就会为应用程序创建一个主程序类,main 方法里的代码就会被这个主线程执行,每个线程有自己的 Java栈,栈里存放着方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序技术寄存器。



jvm的对象存储实意图



Java(线程栈)线程工作内存 & volatile

所有在方法内定义的基本类型变量,都在每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所以这些变量一定是线程安全的。



在多线程的情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存的变量副本来进行。

所以在多线程情况下,线程操作主内存中的数据就会涉及到数据原子性,一致性,可见性的问题。

volatile关键字可以保证数据的更改对其他线程总是可见的,其原理就是,让线程每次读取某个数据的时候,强制工作内存从主内存获取最新的数据的值。保证了数据的可见性。volatile关键在适合用在只有一个线程修改数据,同时多个线程读取数据情况。



Java 运行环境



JVM 的垃圾回收

JVM的垃圾回收就是讲JVM堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。

JVM 通过一种可达性分析算法进行垃圾对象的识别,具体过程是:从线程栈桢中的局部变量,或者方法区的静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记的对象都是被使用的对象,而那些没有被标记的对象就是可回收的对象。



垃圾对象标记的方法:

  1. 引用计数法: 基本想法就是 一个对象,如果有地方在引用它,那么它就不是垃圾,因此呢,我给这个对象创建一个引用计数器,对象 被引用一次,计数器就+1,引用失效,计数器-1,垃圾回收的时候一看这个对象计数器值为0,说明这个对象没被引用了,就被当作垃圾回收了。这个算法非常高效,但是存在的问题就是两个对象互相引用,这样这两个对象就永远无法被回收。

  2. 根搜索法: 这个方法的基本思路是,从一些根节点,称为“GC Roots”,为起始点往下搜索,形成一条引用链,在引用链之外的对象被视作垃圾。java语言中的“GC Roots”有如下几种:帧栈中本地变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。

垃圾回收使用的算法

  1. 标记清除算法:在这种回收机制中,分为两个过程,一是标记,再就是清除,标记阶段就是用根搜索法来判断对象是否需要回收,清除就是回收内存空间。这个算法是垃圾回收中最基本的算法,许多高级的垃圾回收算法都是根据它为基础。这个算法的缺点是效率低,回收后的内存空间过于碎片化。

  2. 复制算法:针对标记清除算法的效率问题,复制算法给出的解决方法是,将内存空间一分为二,每次只用其中一块空间,当进行垃圾回收时,把活着的对象复制到另一块空间中,清除时将原空间全部清除,这样效率既高也解决了内存碎片,缺点就是太废内存了。

  3. 标记整理算法:过程和标记清除算法类似,区别就是它不直接把垃圾空间清除掉,而是将活着的对象都移动到一起,剩下的空间就是垃圾空间了,这时候直接清除掉就行。

  4. 分代回收算法:分代回收法就是根据对象的生存周期,分为老年代和新生代两个内存空间,对于新生代这种对象生成和死亡都比较快的区域使用复制算法进行回收,对于老年代则使用标记清除或标记整理算法。



垃圾回收器分类:

串行回收器:一个垃圾回收线程,stw(stop-the-world)停止所有的用户线程回收。

并行回收器:多个垃圾回收线程,stw停止所有线程回收垃圾。

并发回收期CMS:分为以下4个阶段:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,会“Stop The World”。

  2. 并发标记:GC Roots Tracing,可以和用户线程并发执行。

  3. 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。

  4. 并发清除:清除对象,可以和用户线程并发执行。

G1回收器:其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)器垃圾回收过程如下:

  1. 初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

  2. 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

  3. 最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set  Logs里面,把Remembered Set  Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)



JVM 的性能诊断工具

基本工具:

  • JPS:用来查看运行的所有java进程的pid

  • JSTAT:JVM统计监测工具

  • JSTACK:用来查看某个Java进程内的线程堆栈信息。

  • JMAP:是一个可以输出所有内存中对象的工具

集成工具:

  • JConsole:一个内置Java性能分析器,可以从命令行或在GUI shell中运行。

  • JVisualVM:JDK提供的一个JVM运行监控工具,可用于查看dump文件,CPU及内存的占用情况,GC垃圾回收情况等信息



Java代码优化方面

1、多线程:合理并谨慎使用

  • 使用场景:IO/阻塞,需要多cpu并发

  • 带来的问题 :资源争用于同步问题

  • 合理使用:使用juc并发包,启动线程数=[任务执行时间/(任务执行时间-IO等待时间)*cup内核数]。 cup密集型任务 线程数最多不超过CPU内核数,因为启动再多线程,CPU也来不及调度。对于IO密集型任务,多启动线程有助于提高任务并发度,提升高系统吞吐能力,改善系统性能。

2、竞态条件与临界区

  • 两个线程竞争统一资源时,如果对资源的访问顺序敏感,就存在竞态条件。倒置竞态条件发生的代码去称作临界区。

  • 避免竞态条件:在临界区中适当使用同步。

3、内存泄漏优化

  • 原因:程序保留了不会在使用的对象的引用,垃圾回收器无法回收,这些对象会慢慢耗尽内存。

  • 容易内存泄漏的点 : 长生命周期对象,竞态容器,缓存

  • 避免方式:

  • 1、复用线程对象资源,避免程序生命周期中创建和删除大量对象

  • 2、池管理算法(记录哪些对象是空闲的,哪些对象正在使用)

  • 3、对象内容清除(ThreadLocal 清空)

4、使用合适的JDK容器类提高程序效率和保证线程安全性

5、缩短对象生命周期,加速垃圾回收

  • 减少对象驻留内存的时间

  • 在使用时创建对象,用完释放

6、IO方面

  • 使用I/O buffer 及 NIO提升IO操作的效率

  • 延迟写与提前读策略

  • 使用异步无阻塞IO通信

7、优先使用组合代替集成

  • 减少对象耦合

  • 避免太深的集成层次带来的对象创建性能损失 (对象创建的步骤:静态代码段-静态变量-父类构造函数-子类构造函数)

8、合理单例模式

  • 使用无状态的单例对象

  • 保证线程安全

JVM虚拟机跨平台的思想的启示:

计算机领域的任务问题都可以通过增加中间层(虚拟层)来解决问题

如:JVM虚拟机,相当于操作与java代码的的中间层,解决java跨平台问题

代码提倡面向接口编程,接口相当于一个中间层

秒杀系统设计

秒杀问题:互联网商城在网站上做促销活动,定时发售,限量抢购的模式,在抢购前后时间段会吸引众多活跃用户,在活跃用户众多,发售商品数量很少的情况下。定时开抢那一刻开始,很短的时间内商品被抢完,现象看起来就像是秒杀,一秒钟抢完的样子。所以叫秒杀问题。



秒杀活动对系统带来的挑战:

系统负载方面:瞬间高并发,需要考虑的方面

  • 网络带宽时候能承受这么大流量

  • 服务器和数据库能否承受如此大的并发请求

  • 秒杀的页面响应时间要求是否满足

  • 如何在需求的时间内快速达到秒杀性能要求

技术实现方面:

  • 秒杀定时器的设计 :如何控制某个时刻开放商品才能下单

  • 订单分配的设计:如何保证商品不会超卖,不会在秒杀前下单。



解决手段:

1、秒杀主页查询,下订单动作单独一套系统,单独分配资源。降低与原有系统的影响。

2、商品图片限制大小,节省网路带宽。

3、页面静态化,减少不必要的动态加载。使用YSLOW 原则提升页面响应速度。

4、设置三道流量阀门,商品列表-阀门-秒杀页面-阀门-填写订单页面-阀门-提交订单。三道阀门流量逐渐减少,到下订单正好控制到商品数量。



5、秒杀页面 url:随机,秒杀前2秒放出,脚本生成。

6、下单页面 订单Id ,随机,不能直接跳过秒杀页面进入,每个秒杀商品预先生成随机Token 做 URL 参数。

7、图片合并,css偏移展示,减少少http请求数

8、html压缩,css,js精简 直接页面中,减少http请求次数

9、砍掉不必要的数据库操作,必要的操作数据采用缓存预热。

10、秒杀流程精简,砍掉填写或选择收货地址,秒杀成功后填写。

11、砍掉调用是否开通支付接口,在首页问题提示必须开通。

12、禁用模糊查询功能

13、采用更轻量/快速的服务器

14、使用CDN ,与方向代理,加速核心页面



最后做好应急预案:

  • 域名分离,独立域名,不影响原有业务

  • 准备备用服务器 若干台

  • 拆东墙补西墙:来不及采购,下掉非核心集群冗余服务器,加入秒杀集群。

  • 壁虎断尾策略:所有办法均失效情况下,例如流量耗尽。关闭非核心应用如咨询,论坛等社区系统。保住核心应用的可用性。

  • 万能出错页面:秒杀活动已结束,位于另外集群。



最后自我总结:

身为仅工作3年的程序员,报名架构师训练营这个课程,当初的想法仅仅是想了解架构师必须要知道的知识层面,必须会的技能等。从这方面说我的目的已经达到了。老师多次提到,有时候有技术不一定能解决问题,关键是要发现问题的本质,一直追求技术的路上也许忽略了这一点。 有时候空有技术,还觉得自己没有机会表现,无法活用,生搬硬套不得要领。学习这个课程,只需把老师说的当做一个指路方向,真正要达到架构师境界,还需要自己领悟。加油!



参考文献

https://blog.csdn.net/iva_brother/article/details/87886525



用户头像

zcj

关注

还未添加个人签名 2019.10.12 加入

精神小伙

评论

发布
暂无评论
架构师训练营:第九周总结