2、JVM虚拟机,也就那么回事!
2020-06-22 23:53:49 0 举报
JVM虚拟机,也就那么回事
作者其他创作
大纲/内容
this
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar gdos.jar
线程-ThreadB
线程-ThreadA
JVM虚拟机
寄存器
eden伊甸(800M)8/10
java -Xms6144M -Xmx6144M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar gdos.jar
60M
Mark Word锁信息gc分代年龄
c = 1
如果像618、双11啊,这种带活动的,可能峰值,即活动开始前,每秒可能就有1000单
注意:1.7之前,存在永生代,方法区是一种规范,永生代是方法区这种规范的实现,1.7之后,取消了永生代(因为只有HotSpot虚拟机才有永生代,其他如IBM的J9等没有),取而代之的是元空间
Java虚拟机栈Java Virtual Machine Stacks
full gc
s1(200M)1/10
重新分配后,启动jar程序
Metaspace元空间(类的元信息)
iconst_1
服务2(Tomcat)4核8G每秒处理:300单
JMMJava内存模型
服务1(Tomcat)4核8G每秒处理:300单
每秒产生60M对象
线程A
old占堆的 2/3 = 2G
0 iconst_1
old老年代 (1G)1/3
os-线程调度算法
sum=?
类的字节码
怎么做优化呢?
Instance Data实例数据
栈(线程)默认:1M
工作内存(线程私有)
对象每躲过一次gc,分代年龄就+1
压栈
不是每个用户浏览网站,看完后,都要下单购买的,买东西的人毕竟占少数!
Padding对齐填充不够8整除,补齐
对象头
程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。线程私有
JMMDemo.java
二进制0101
超大对象也会直接放到老年代
core1
所有对象实例和数组都在堆区上分配,堆区是GC主要管理的区域。堆区还可以细分为新生代、老年代;新生代还分为一个Eden区和两个Survivor区。此块内存为所有线程共享区域,当堆中没有足够内存完成实例分配时会抛出OOM异常。JVM只有一个堆,被线程共享。
本地方法栈Native Method Stacks
public class JMMDemo { private static int a = 1; public int add(){ int c = 1; int d = 2; int sum = (c+d-a)*10; return sum; } public static void main(String[] args) { JMMDemo demo1 = new JMMDemo(); new Thread(()->{ demo1.add(); System.out.println(\"sub1 = \"+demo1.add()); }).start(); new Thread(()->{ demo1.add(); System.out.println(\"sub1 = \"+demo1.add()); }).start(); }}
栈帧add()
汇编指令码
本地方法栈与虚拟机栈发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。线程私有
0 iconst_1 1 istore_1 2 iconst_2 3 istore_2 4 iload_1 5 iload_2 6 iadd 7 getstatic #2 <com/appleyk/jmm/JMMDemo.a>10 isub11 bipush 1013 imul14 istore_315 iload_316 ireturn
对于Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它和永久代有什么不同的? (1) 存储位置不同,永久代物理上是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存; (2)存储内容不同,元空间存储类的元信息,而静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
记录正在执行的虚拟机字节码的地址
cpu缓存
容量越大,性能越低由快到慢,依次是:寄存器>L1>L2>L3>内存
为什么说volatile关键字,不保证i++操作的原子性?static volatile int i = 0;i++;
小知识点:以前Java SE的主流JVM中还有JRockit,跟HotSpot与J9一起并称三大主流JVM。这三家的性能水平基本都在一个水平上,竞争很激烈。自从Oracle把BEA和Sun都收购了之后,Java SE JVM只能二选一,JRockit就成炮灰了。
堆
public class JMMDemo { private static int a = 1; public int add(){ int c = 1; int d = 2; int sum = (c+d-a)*10; return sum; } public static void main(String[] args) { JMMDemo demo1 = new JMMDemo(); new Thread(()->{ demo1.add(); System.out.println(\"sub1 = \"+demo1.add()); }).start(); new Thread(()->{ demo1.add(); System.out.println(\"sub1 = \"+demo1.add()); }).start(); }}
栈帧main()
Metaspace元空间(512M)
二、在不增加内存,保持原有服务器8G内存的条件下,我们可以增加新生代的堆空间大小,将新生代的堆大小设置为2G(老年代自动缩减为1G)
假设在14s的时候,线程执行完new后,还在执行其他业务并没有完全退出,这时候由于eden区满了,会触发一次MinorGC,由于触发GC,会产生STW效应(Stop the World,即程序会停止,直到GC结束),STW会造成在14s的时候,线程局部变量GCRoot还存在引用,因此,在MinorGC后,前13s的所有在内存中的订单**对象会全部被清除,第14s产生的订单**对象由于存在引用,所以不会被GC当做垃圾回收,这时候这些非垃圾对象会被放入到s0区,这时候就有意思了;因为对象60M超过了s0区空间的50%,这时候这些对象会被放到old区,然而,这些对象却是无意义的,但却占用着老年代的空间,这种情况就很容易发生full gc
正常跑,耗时:3205ms
动态链接
L1缓存
L2缓存
14s一次MinorGC,会往老年代放60M,算一下,大概1分钟放257M,则2G放满需要大概8分钟,也就是系统,每8分钟,会产生一次full gc,意味着,网站每8分钟,会卡顿一次,原本几个小时full gc或者几天full gc一次的,却在网站用户量暴涨时,每8分钟一次,这个频率还是挺高的,对用户来说,体验是非常不好的!
old老年代 (2G)2/3
OS操作系统
分代年龄到15了会被放到老年代
也就是通常所说的栈区,它描述的是Java方法执行的内存模型,每个方法被执行的时候都创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法从被调用到完成,相当于一个栈帧在虚拟机栈中从入栈到出栈的过程。此区域也是线程私有的内存,可能抛出两种异常:如果线程请求的栈深度大于虚拟机允许的深度将抛出StackOverflowError;如果虚拟机栈可以动态的扩展,扩展到无法动态的申请到足够的内存时会抛出OOM异常。线程私有
用户不光是买,还要查询,可能会产生其他对象,所以空间大小我们再扩大20倍
class Order{ private Long id; private String name; private Double price; private Date cTime; ...... ...... ..... ...... ......}
CPU缓存(集成在CPU封装内,独立于CPU的器件)
增大survivor区的堆空间大小后,在minorGC时,60M对象会被清理,而不是往old区放了!
指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
Survivor(存活区)1/3,占1G这个区发生的GC叫minorGC
线程B
在1.8之后,方法区与堆共享内存,逻辑上可以认为方法区被并入堆中。大致结论为( 堆 [ 方法区 { class对象 ( 静态变量 ) } ] )。
core0
局部变量表
core3
假设8个G的内存,操作系统占2个G,其他服务占2个G,剩下4个G分给JVM
线程
假设后台创建一个订单对象,需要占1KB的内存空间
main()-栈帧
JVM运行时数据区
工作内存Work Memory其实就是对CPU寄存器和高速缓存的抽象,或者说每个线程的工作内存也可以简单理解为CPU寄存器和高速缓存。那么当写两条线程ThreadA与ThreabB同时操作主存中的一个volatile变量i时,ThreadA写了变量i,那么:ThreadA发出LOCK#指令发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效ThreadA向主存回写最新修改的iThreadB读取变量i,那么:ThreadB发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值.由此可以看出,volatile关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上.
cpu同一时间,只能执行一条指令,所以线程需要进行调度,cpu通过线程调度算法,决定哪一个线程会被cpu进行优先执行
一、增加机器内存,给JVM堆分配更多的空间,比如由原先的3G,改为6G,这时候,survivor区就会有2G的空间,s0和s1各200M,则60M对象放入s0后,由于不超过s0总空间的50%,所以会在下一次minorGC后,s0中的对象会被清除掉!这样就能避免60M对象每次minorGC后放入老年代,导致JVM频繁full gc造成系统卡顿了!
订单转换率10%
JMMDemo.class
L3缓存
JVM类加载系统
缓存行对齐,避免伪共享的两种方式(J8 注解 or 前后各7Byte占位)
二进制指令
Java内存模型,简称JMM,是一种抽象的设计概念,是Java内存设计的一种指导思想。JVM是JMM的一种实现。在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从Java 5开始的JSR-133发布后,已经成熟和完善起来。
其实,CPU执行的时候,是要将内存中的数据,加载到CPU缓存(寄存器等),多线程的时候,数据首先在寄存器中修改,改完后重新刷入内存中。解决多线程数据可见性的问题,Java提供了Volatile关键字,它的实现原理,对象操作后面加入Lock汇编指令(lock chmxsg)。A线程在修改操作后,强制刷入主存,然后通知执行B线程的CPU,你CPU缓存中的值是失效,不能用了,要用请从主存中重新获取。
方法出口
堆(heap)
加注解后,避免伪共享,耗时:765ms
内存条
FILO(先进后出)Java虚拟机栈
解释执行器/JIT优化
-Xms:初始堆大小,3G-Xmx:堆最大值,3G-Xss:设置每个线程栈的大小,默认1M-Xmn:新生代大小,增加新生代,会缩减老年代
3M*20/s意味着每秒产生60M的对象1s后会成为垃圾(局部变量)
操作数栈
300*1Kb =300KB
每个方法从调用到完成,相当于一个栈帧从虚拟机栈中从入栈到出栈
运行大概约14s占满eden区
程序计数器Program Conuter Register
cpu
add()-栈帧
core2
d=2
更新
栈帧:存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元 ; 栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。
istore_1
运行结果
假如每个人浏览网站,平均每天点击20次
对象static int a = 1
由于字节码只有JVM字节码引擎能识别,CPU与底层硬件无法识别执行,字节码需要被编译器编译成计算机硬件汇编指令
s0(100M)1/10
jvisualvm工具(Visual GC是插件,需安装)
用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的
线程C
java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar gdos.jar
nginx负载均衡
线程记录表
JDK1.7之前有永生代permGen之后就是Metaspace,元空间,替代之前的permGen
主内存(线程共享)
平均网站每天的用户访问量在500w
s0(200M)1/10
cpu读取到指令后,将指令对应的内容,比如c=1写入到内存中,其实就是线程对应的工作内存中
线程结束完后,局部变量GCRoot在内存中就没什么用了
300KB*10 = 3M
亿级流量(电商网站)假设每日访问量1亿次
服务3(Tomcat)4核8G每秒处理:300单
s1(100M)1/10
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址
运行时数据区(内存模型)
正常情况下,假设每秒几十单(这个压力可以忽略)
对象内存结构
新生代占堆的 1/3 = 1G
订单可能还会牵涉交定金啊、优惠券啊、减库存、加会员积分等操作所以,产生的内存空间我们假设再乘以10倍
程序计数器=0
缓存行(cache line,通常为64Byte,64字节)
... ... ...
c=1
Klass Pointer类的指针指向类在内存中的地方
字节码执行引擎
eden伊甸(1.6G)8/10
main线程
每天成交50w订单(平均每秒6单)
因为i++并不是一个原子操作,它包含了三步(实际上对应的机器码步骤更多,但是这里分解为三步已经足够说明问题):1、获取i2、i自增3、回写iA、B两个线程同时自增i由于volatile可见性,因此步骤1两条线程一定拿到的是最新的i,也就是相同的i;但是从第2步开始就有问题了,有可能出现的场景是线程A自增了i并回写,但是线程B此时已经拿到了i,不会再去拿线程A回写的i,因此对原值进行了一次自增并回写;这就导致了线程非安全,也就是我们常说的多线程执行的结果不对;总而言之,volatile只能保证拿到的变量一定最新的,至于拿到的变量做了其他操作,volatile是无法也没有办法保证它们的线程安全性的;也许你会问,如果线程A对i进行自增了以后cpu缓存不是应该通知其他缓存,并且重新load i么?拿的前提是读,问题是,线程A对i进行了自增,线程B已经拿到了i并不存在需要再次读取i的场景,当然是不会重新load i这个值的。补充:也就是线程B的缓存行内容的确会失效。但是此时线程B中i的值已经运行在加法指令中了,所以不存在需要再次从缓存行读取i的场景。翻译:是,没错,我知道缓存的i的值是失效的,但是,我已经执行完操作了,你让我重新再来(重新load i)一遍?开玩笑,你觉得我会吗?解答:指令就是i自增,执行完一次,就作废了,不可能再回过头来重新执行一遍了!!!!!!!!!!!!
0 条评论
下一页