Java语言特性:
跨平台:代码一次编写,到处运行。
本来在不同操作系统要开发不同的汇编代码,对java语言来说不需要,写一份Java代码在各平台都可以运行。
跨平台怎么实现:
字节码文件都会放到JVM中去运行,通过Java虚拟机帮我们最终生成基于不同操作系统平台的二进制机器码。
因为不同平台底层计算机可执行的指令集都不一样。
总结:Java虚拟机从软件层面屏蔽不同操作系统在底层硬件与指令上的区别。
不同平台下载不同的JDK,Java虚拟机是JDK的组成部分。
JVM组成部分
栈(线程栈/虚拟机栈VM Stack)
所有的都是内存区域,放数据。
栈(线程栈):程序运行时候的局部变量。
分配专属的运行空间(需要内存空间来存放局部变量)
内部有些组件:
栈帧
栈帧内存空间:只要有个方法开始运行,给它分配方法对应的栈帧内存空间。
为什么要用栈这种数据结构来存储我们内部的栈帧内存空间?
因为它跟我们程序方法嵌套调用的前后执行顺序是非常相匹配的。
先调用的方法先分配内存,后调用的方法后分配内存,后调用的先结束(先销毁),先调用的后结束。
栈FILO:先进后出
字节码文件探究:
javap
命令:
1 | javap -c Math.class > math.txt |
这两个字节码文件是一样的。但是右边的可读性更高。
都是JVM底层执行的代码。
越底层的语言代码量会越多,高级语言帮我们屏蔽了很多底层的细节。
Oracle官方网站有对这些class代码解释的命令手册。
程序计数器
程序计数器是每一块线程都独有的。(专属的内存空间)
当前线程执行字节码的行号指示器。
用来放当前线程正在运行或者马上要运行的代码的行号。
(实际上是这行代码在方法区中的内存地址)
为什么用它?
比如正在执行某一行代码时,有个更高优先级的线程出现了,把CPU的时间片抢过去了,当前线程被挂起,当那个线程执行完,CPU才会切回当前这个线程继续执行,可以根据这个值知道继续在哪行代码之后执行。
字节码文件实际上是加载到方法区(元空间)的。
程序计数器的值是怎么修改的?
随着java代码的运行不断的作修改,是由字节码执行引擎去修改的。
操作数栈:
程序在运行过程中,操作数在操作过程中临时存放的一块内存空间。
运算完就不需要了。
动态链接
把一些符号引用转变为直接引用。
main方法内调用computer方法,程序在运行过程中,调到computer方法,可以通过动态链接去到方法区里去找到这个方法的代码。
方法出口
调用computer方法完了会把结果存储在方法出口。
方法区(元空间)
存放常量+静态变量+类信息
对象放在堆
所以方法区会访问 堆
本地方法栈
native修饰的方法是本地方法。
比如main线程里,加了一行 new Thread().start();
里 start0
就是本地方法。
main方法在运行过程中,有本地方法就需要用本地方法栈来分配内存空间。
堆
Eden园区和survivor区,老年代。
8:1:1的比例 。
new出来的对象,优先放在eden园区。
minor GC(回收整个年轻代)
放不下的时候,字节码执行引擎在后台开启一个线程,垃圾收集线程,收集垃圾对象。
垃圾收集的时候会运用到一个算法(找垃圾):可达性分析算法
GC ROOT根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。
首先找出所有的GC ROOT变量。(user, math …)
比如找到math对象,假设math还有成员变量引用了其他对象,再往下找,直到找到最后一个对象没有成员变量引用其他对象了,结束。整个引用链上的所有对象打上”非垃圾“的标签,其余未标记的是垃圾对象。
把这些对象复制到survivor区,其他Eden园区中的对象把清空。
这些对象每经历了一次minor GC,如果没有被回收的话,会挪右边一次,每挪一次,分代年龄就会加1。当对象分带年龄达到了15的话,会被复制到老年代里去。(还有其他情况:对象太大,前面的放不下;前面满了)
如果老年代放满了之后: full gc
对整个堆的所有区域做垃圾收集。
如果没有什么垃圾可以收集,之后再有新对象进来(OOM内存溢出)
调优
调优最重要的减少full GC
为什么?——stop the World(STW)
full GC的时候会停掉用户线程。
对吞吐量和性能影响很大。
既然会影响很大,为什么要设计STW机制?
假设没有STW机制,产生了Full GC了,找垃圾找完所有的GC ROOT,都标记了“非垃圾”对象,GC线程没结束用户线程已经结束了,在所有的内存空间都会被释放掉,意味着又变成“垃圾了”。索性在full gc线程过程中先停掉,之后再恢复。不能让我们的对象一会是垃圾一会不是垃圾。
- 使用-XX:UseSerialGC, 新生代使用Serial GC,老年代自动使用Serial Old GC
对频繁fullGC的时候如何调优?
8个G:java虚拟机4-5G(堆3G、元空间1-2G),1-2G操作系统。
堆:老年代 2G,年轻代1G
假设每个订单对象假设1KB (所有成员变量占用的空间之和+对象头+对象补齐)
下单还涉及其他对象,如库存、优惠券、积分等我们放大20倍
(300KB*20)/秒对象生成
可能同时还有其他操作,如订单查询等 再放大10倍
(300KB2010)/秒
每秒生成60MB对象,1秒后都变成垃圾对象。
14s的时候,前面的12-13秒的在做minor gc的时候结束,被回收。
最后1-2s的部分线程还没结束,被挂起了。会复制到survivor区。
很有可能不一定会放到survivor区,会直接放到老年代。
什么对象会进入老年代?
- 大对象直接进入老年代
- 分代年龄到一定程度(15岁,CMS回收器默认6岁,不同垃圾回收器稍微不同)
对象动态年龄判断:
一批对象的总大小大于这块survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),(对象动态年龄机制)那么此时大于等于这批对象年龄最大值的对象,就直接进入老年代了。
所以在做minor gc的时候将“非垃圾”对象放到survivor区域的时候触发了对象动态年龄机制,直接放了一批到老年代。
4/5分钟老年代放满了,触发full GC。
能否对JVM调优,让其几乎不发生full GC?
Old:1G;eden:1.6G;S0/S1:200M
那对单机超高并发(几十万并发)的系统怎么调优呢?
kafka、Racket MQ
需要大内存CPU比较多的系统才扛得住。(单机32G/64G以上起步,才可能抗的住这么高的并发)
比如分配:Old:1G;Eden:30G;s0/s2:200M
但是对于这种,在minor gc上就要进行调优了,如果不调一次也回收太多可能要回收几秒钟,在这个过程中,用户线程会超时。
遍历30G的eden区,至少要2-3s。客户端发消息的过程中,做了minor GC,会停顿超时了。
可以让垃圾收集器边收集垃圾边回收,不一定需要等到30G空间放满了才触发mnior GC,比如说可以等到2G空间放满了就收集,收集一部分区域,也就是大概300ms的时间。可以使用一些比较合适的垃圾收集器。
CMS垃圾收集器:最普通的
G1垃圾收集器 -XX:+UseG1GC 如果对那种大内存的回收可以做到部分回收,可以让一次GC的时间尽可能的短(STW的时间,客户停顿时间不至于太长)。
通过设置 -XX:MaxGCPauseMills:目标停顿时间。
垃圾收集器
调优诊断优化工具
JVM调优工具:jvisualvm
阿里巴巴开源的工具:Arthas
命令:
- dashboard
- thread 8
- jad com.tuling.jvm.ArthasTest 反编译代码
异常关闭查看进程:jps
资料:
[美团技术博客]
https://tech.meituan.com/2020/11/12/java-9-cms-gc.html
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html