从HelloWord学习JVM虚拟机

一、为什么学习JVM

面试、找工作、OOM、内存调优?

二、什么是JVM,它做了什么

  • java虚拟机:执行java代码的平台,屏蔽了底层硬件指令的细节,一次编写到处执行
  • 代码执行过程:源代码->字节码文件class->-->jvm

jvm&jdk&jre 关系  jdk包括jre和jvm

  • jvm做了什么?
  1. 空间分配回收,而c++需要考虑内存分配和回收。java能让开发者100%精力投入业务开发
  2. 内存管理
  3. 屏蔽底层硬件区别

三、jvm运行时运行区

1,程序计数器:指向当前线程执行的字节码指令的地址,行号,如0x3d48

java最小的执行单位是线程,但线程只负责做,做什么需要指令告知;

程序计数器为什么要记录地址呢?因为cpu是有调度策略的,当cpu被其他线程抢到了,此时该线程被挂起,此时需要记录上一次执行到的地址方便线程恢复执行。

2,虚拟机栈(FILO先进后出):存储当前线程运行方法时所需要的数据,指令和返回地址。

如一个helloword()方法,一个方法一个栈帧,局部变量表存储局部变量,一个程序运行时就能确定的空间,一个栈层存储32位的数,即一个int,多个字节要拆开存储如64位拆为高位和地位。

操作数栈:比如算sum=i+j  从局部变量表加载i,j,然后做加法得到sum,再将sum存到局部变量表,即对应4条指令。

注意:成员变量不管是不是引用类型都存储在堆内存中的。

但对于引用类型obj。局部变量表只记录obj的地址应用,所以结论栈指向堆。

学会看反编译字节码文件,javap -v HelloWord.class > aa.txt 参考javap指令集查看https://www.cnblogs.com/JsonShare/p/8798735.html

如methodOne(int i){int j=0; } ,仅一个int j=0 对应两条指令;  j存在局部变量第二位置里,地址0:存this 地址一1:存 i  地址2:存j。

 

动态链接:java动态特性,如运行时多态,如@Autowired private Service service;

代码调用serevice.do()。需动态解析,到底是哪个实现类的do方法。为什么动态链接要存在栈帧里呢?

方法出口:即return,正常出口和异常出口如throw。

一个方法调用另一个方法,怎么入栈?先入栈methodOne,再入栈methodTwo

 

3,本地方法栈:native方法栈,

4,方法区:存储类信息即class文件,常量,静态变量,JIT,methodOne存储在方法区。

5,堆内存:分代模型 就分新生代和老年代,跟后面的永久代和元空间没关系。

注意:jdk1.8后 永久代变成了元空间meta space主要解决永久代溢出的问题,因为元空间会自扩容,跟arraylist差不多。

方法区就存在永久区。老年代默认是新生代的2倍。创建对象首先在eden区询问空间,有则放下,没有则触发yonggc ,把之前的对象移动到surivor区域的from区域,如果from也放不下,就会触发担保机制,直接放到老年代,每新生代gc一次,就触发from和to的交换一次,对象向下移动年龄是加1的。

Xms  s:starting堆初始的总大小

Xmn  n:new  堆初始时new空间的大小

Xmx   x:max堆最多的空间

 

垃圾回收算法:gc算法:复制回收算法(新生代用的),标记清除算法和标记内存整理算法(后面俩是老年代用的)。

垃圾回收器:不同的垃圾回收器是实现了不同的垃圾回收算法的,如parllen new等实现了

复制回收算法,eden区一定是空的。

 

内存溢出后排查是否是内存泄露。

对象的生命周期,什么对象能被回收,即是否可达,gc root概念,当一个对象被应用时计数器就加1,当为0时即无引用代表可回收。堆里有很多gc root只要有gcroot指向的都是可达的,都不能回收。

gc root有哪些:有4种;强引用、软引用、弱引用、虚引用

 

FullGc 5分钟一次怎么调优? 衡量的标准和维度?5分钟一次怎么啦?是要追求吞吐率还是最小执行时间?业务出现性能问题,90%以上是业务调优,并不是有问题就调jvm。

内存泄露,要找出gcroot,即泄露源,因为它指向了很多对象,或者死循环对象,被回收不了。

性能调优:发挥机器本来的性能。

1,如果追求吞吐率就要算吞吐率,看下回收器用的什么,如可用cms就是追求吞吐率的。

2,

 

 

总结:程序计数器,虚拟机栈,本地方法栈是线程独有的,每个线程都有,但堆和方法区是共有的。

 

四:类加载过程

源码-》字节码-》装载-》链接-》初始化

装载:怎么装载的,用类加载器

1,通过类的全限定名找到类的二进制流;

2,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3,在java堆中生成一个java.lang.calss对象,作为方法区中这些数据的入口;

链接:验证,保证类加载的正确性,如文件格式,符号引用等;准备,为类的静态变量分配内存,并将其初始化为默认值如private static int a=10,将a初始化为0而不是10:解析,把类中的符号引用转换为直接引用;

4,初始化:对类的静态变量或静态代码块执行初始化操作;这时a才初始化为10;

五:类加载机制&类加载器

bootstrap classloader 负责加载jre/lib/rt.jar

extension classloader 加载javahome中的jre/lib/*.jar

app classloader 负责加载clsspath下的类和jar包

custom classloader 自定义加载类,实现ClassLoadeer

双亲委派机制:所有类加载原则,都是回溯到父类去加载,一直到最顶层加载,没有再交给孩子节点加载,保证同一个类只有一个层级加载;

六、运行时数据区 共6块

两块:一个是跟随线程生命周期的如虚拟机栈,本地方法栈,程序计数器;另一个是跟随jvm生命周期的如堆方法区

方法区:

常量final修饰的,静态变量static修饰的;方法区是线程非安全的,因为共享;

堆:所有线程共享,对象和数组

虚拟机栈:

 

七、内存分配模型

1,非堆,就是方法区

2,堆分为old和young,young又分为eden和s0和s1块,eden:s0:s1一般等于8:1:1,为什么这个比例,因为浪费的最少,而且80% eden区域可以存下更多对象,且eden大部分都是朝生夕死的,所以不能太小了。

3,刚分配的对象,首先分配给eden区,大对象直接放old区,如果不够用则触发young gc,对象年龄就加一,一般到达15岁就会到old区

4,young gc过程:将死亡的对象垃圾回收,但这导致了空间不连续,有垃圾碎片,为了空间连续能够再次多盛对象,就把存活的对象复制到s0,再次young gc时把eden和s0对象放到s1去,这样s0和eden就可以清空,空间就连续了,所以s0和s1永远都有一个使用一个未使用。

5,当young gc=minor gc时,若s0放不下,则触发担保机制,向old区域借空间存放对象。

fullgc = old gc+young gc

注意:s0和s1总有一个空闲,所以浪费了10%的空间,但目的就是为了解决空间碎片的问题,浪费了空间

总体流程图如下:

jvisualvm内存查看工具,

八,jvm垃圾&回收

1,判断对象是不是垃圾,

a,引用计数法(有引用计数加1,但存在两个对象循环引用的问题导致永远无法回收)

b,可达性分析,gc root可达或间接可达都不能算做垃圾,gc root主要有:类加载器,Thead类、本地变量表、static成员,常用引用,本地方法栈中的变量

2,垃圾回收算法

a)标记清除:循环内存所有内存区域,找出不可达的对象,将其标记释放,但这个导致了空间不连续,有空间碎片产生

b)复制算法:内存再一分为二,循环内存所有区域,将所有存活的对象复制到空闲区域,复制时连续存放,解决了标记清除碎片问题,但基本空间浪费一半空间。

c)标记整理:综合了a和b两种,首先循环所有内存区域进行标记,标记完将存活对象整理成连续存放,最后释放垃圾对象

3,各个区域和回收机制的配合使用-分代收集算法:不同的代使用不同的回收算法机制

young区:使用复制算法,因为eden区当gc时大部分对象都死了,所以只需要复制少量存活对象到from区域即可。

old区域:标记清除或标记整理

4,垃圾回收器:垃圾回收算法的实现

主要有串行的,并行的,cms,g1的等,各个回收器使用的范围如图:

上述适用于young区域的都是实现了复制算法的税收器,连线关系是搭配使用的关系;下层都是实现了标记整理的回收器;

serial,串行回收,当需要gc时,停止所有用户进程,启动一个进程串行回收垃圾,

parnew,并行,当需要gc时,停止所有用户进程,启动多个进程并行回收垃圾,

paralleScavenge:相比parnew更加关注吞吐量,

cms:主要关注停顿时间。比较主流,先单线程初始标记,找到gcroot关联的对象,这个很快所以不需要花额外开销开多线程,第二步:再并行标记,防止初始标记不完成,但此刻并行标记线程可以和用户进程并发执行了,重新标记需要stop the world(STW),最后并发清理,不需要STW,具体图下图:

G1回收器;使用于新生代和老年代,主要关注停顿时间,比cms高端的是,用户可以设置停顿时间,供 4刷选回收使用

分为4个过过程,1,初始标记;2并行标记,3最终标记  4刷选回收。其中1,3,4步,需要stop the world

 

九、如何选择回收器,如何开启

  • 垃圾回收器分类,

1,串行的serial和serial old 只有一个线程回收,需要停用户线程,适用于内存较小的设备,

2,并行收集器(吞吐量优先),如parallel scanvenge 、parallel old多条垃圾收集器线程共同工作,用户线程处于等待状态,适用于后台运算而不需要太多交互的任务

3,并行收集器(停顿时间优先),如cms和g1,用户线程和垃圾回收器线程同时执行(但不一定并行,可能是交替执行),不会停用户线程,适用于对运行时间有要求的场景,如web对响应时间有要求

  • 评判垃圾回收器好坏的指标:吞吐量和停顿时间,调优也主要调这两个指标

停顿时间:垃圾回收器进行垃圾回收终端应用执行的响应时间,停时间越短越适合与用户交互

吞吐量:运行用户代码的时间/(运行用户代码时间+垃圾收集时间),吞吐量越高越有效的利用了cpu时间,cpu利用率越高。

如何选择收集器

如何开启

十、jvm参数,命令,工具

1,标准参数,不随着

2,-X参数,

3,-XX参数,有bool值和参数设置值 如-XX:+:UseG1GC  +/-代表开启和关闭    -XX:name=value如设置堆内存大小等

4,其他参数,如-Xms100M  <=> 等价于 -XX:InitialHeapSize=100M   再比如 -Xmx100M    -Xss100M等

设置方式:在开发工具中有vm options参数设置或者java命令后设置,或者tomcat启动.sh中设置

JVM命令:

1,jps:查看当前所有的java进程,主要用来获取pid

2,jinfo:查看某个java进程的参数设置, 如jinfo -flag UseG1GC  pid      查看参数UseG1GC设置情况   jinfo -flags pid查看该pid的所有参数设置

3,jstat:查看当前java进程统计信息,如:jstat -gc pid 1000 10 =>查看该pid gc运行情况,每隔1000ms打印一次,共打印10次

4,jstack,查看当前进程堆栈信息  如 jstack pid,查看当前进程有那些线程,比如代码中死锁,用这个一目了然

5,jmap,打印出堆转存储快照,如jmap -heap pid  会打印出当前老年代,新生代等存储信息。

或dump出堆内存相关信息到文件 如:jmap -dump:format=b,file=xxx.hprof PID

也可以设置当堆内存溢出时自动dump,,只要在jvm参数加上-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=heap.hprof  

 

常用工具

1,jconsole监听某一个具体的java进程

2,jvisualvm

3,arthas 阿里的火焰图工具,

4,mat/perfma查看dump出的内存文件

5,gceasy.io网址,直接在线选择gc日志文件分析查看即可、   gcviewer本地工具,查看gc的回收

 

 

类加载执行过程,调用本地方法时有本地方法接口可以调用。

十一,性能优化

1,OOM后dump出prof文件,用prof查看工具如mat/perfma,查看时有histogram,可以查看其中的类对象及个数, leak suspects 泄露疑点,打开可以看details详情。

2,GC优化:通过不断的调整,观察GC日志的吞吐量和停顿时间,寻找最佳值。

jvm参数使用 -Xloggc:/home/admin/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps  -XX:+PrintGCTimeStamps  -XX:+UseConcMarkSweepGC可以打印gc日志。通过gceasy.io网站,或者gcviewer本地查看,主要关两个指标,一个是avg pause gc time停顿时间和throughput吞吐量。

调优目标:高吞吐量和低停顿时间。假如我能忍受200ms的停顿时间,就可以调吞吐量了。主要手段就是调整堆内存大小,垃圾回收器选择,回收的线程数,堆占用比例是多少时触发gc,然后观察吞吐量。

GC优化指南,发现问题->排查问题->解决方案

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章