JVM,你都会了吗

Posted 笨兮兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM,你都会了吗相关的知识,希望对你有一定的参考价值。

1.概述

JVM(全称Java Virtual Machine)也叫Java虚拟机,它是一种抽象化的计算机。有句话叫 java语言是跨平台的,一次编译,多处运行。也就是说java代码只需要一次编译即可放到不同操作系统上进行运行,这完全依赖JVM,它将编译的class文件转化为对应操作系统所能运行的二进制文件。

2.JVM内存结构图

jvm共包含5个部分:堆、Java栈、本地方法栈、方法区(元空间,JDK1.7前叫方法区,JDK1.8后叫元空间)及程序计数器。

堆:实例化的对象都会放在堆中,以及数组也在堆中分配内存。

Java栈:全称是Java虚拟机栈,也叫线程栈,每个方法在执行时都会创建一个帧栈,用于存储局部变量表、操作数、动态链接和方法返回等信息。(线程私有)

本地方法栈:保存native方法信息,当一个jvm创建的线程调用native方法后,不会在Java栈为该线程创建帧栈,而是简单的动态链接并直接调用该方法。(线程私有)

方法区:存放已被加载的类信息,常量、静态变量、即时编译器编译后的代码数据。

程序计数器:当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。(线程私有,不会内存溢出)

一个java代码要想运行,就必须先编译成.class文件,然后交给JVM去执行。JVM会通过类装载子系统把字节码文件加载到内存区中,然后字节码执行引擎会运行内存区中的代码。

那么具有它们之间是什么关系?

1)字节码执行引擎会去执行方法区中的代码,当当前线程被其他线程抢占CPU时,字节码执行引擎会运行完当前一行的代码,然后把下一行将要运行的代码地址记录下来,并去修改程序计数器中当前线程的指令指定。

2)由于堆中存储的是对象,故当方法区中的静态变量包含对象时,则这些变量的值实际上是对象在堆中的首地址。同理,在Java栈中也会有变量是对象,其指向的也是对象在堆中的地址。

2.1Java栈

为了弄清楚Java栈的作用,这里以下面的代码进行说明

package com.zxh.demo;

import java.util.HashMap;
import java.util.Map;

public class JvmTest1 

    //静态变量
    public static final int sum = 15;
    //静态对象
    public static Map<String, String> map = new HashMap<>();

    public int add() 
        int a = 3;
        int b = 8;
        int c = (a + b) * 10;
        return c;
    

    public static void main(String[] args) 
        JvmTest1 jvmTest1 = new JvmTest1();
        int add = jvmTest1.add();
        System.out.println(add);
    

将上面的代码进行编译后会生成class文件,如果打开文件,显示是字节码信息是完全看不懂的,因为这些代码是虚拟机运行的代码

当然也有另一种表现形式的代码可以查看,在class文件目录打开cmd,执行命令

javap -c JvmTest1.class > t1.txt

对该文件进行反汇编

命令执行后,会生成指定名称的txt文件,内容如下:

 

要看懂这些代码,需要查看参考文档,其中部分指令说明如下:

iload 将指定的int型本地变量推送至栈顶
iload_0 将第一个int型本地变量推送至栈顶
iload_1 将第二个int型本地变量推送至栈顶
iload_2 将第三个int型本地变量推送至栈顶
iconst_0 将int类型常量0压入操作数栈
iconst_1 将int类型常量1压入操作数栈
iconst_2 将int类型常量2压入操作数栈
istore_0 将栈顶int型数值存入局部变量0(通常可视为this)
istore_1 将栈顶int型数值存入局部变量1(局部变量x表中元素的唯一标识,相当于索引)
istroe_2 将栈顶int型数值存入局部变量2
iadd  将栈顶两int型数值相加并将结果压入栈顶
imul  将栈顶两int型数值相乘并将结果压入栈顶
ireturn 从当前方法返回int
bipush 将一个8位带符号整数压入栈

lload 将指定的long型本地变量推送至栈顶
fload 将指定的float型本地变量推送至栈顶
dload 将指定的double型本地变量推送至栈顶
aload 将指定的引用类型本地变量推送至栈顶
lload_0 将第一个long型本地变量推送至栈顶
fload_0 将第一个float型本地变量推送至栈顶
dload_0 将第一个double型本地变量推送至栈顶
aload_0 将第一个引用类型本地变量推送至栈顶

根据上述的命令,对反汇编的结果进行分析,以add方法为例,其帧栈信息如下

操作数栈:是供操作数在进行加减乘除操作过程中临时存放的内存空间。

动态链接:在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,如描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

1)首先将int类型的常量3压入操作数栈

2)将栈顶int类型数组存入局部变量1,也就是定义了一个局部变量a,为其开辟了一块存储空间,给其赋值为3

 3)将int类型的常量8压入操作数栈

4)将栈顶int类型数组存入局部变量2,也就是定义了一个局部变量b,为其开辟了一块存储空间,给其赋值为8(这里遵循的是先进后出的原则)

5)将int类型局部变量1(a)推送到操作数栈

6)将int类型局部变量2(b)推送到操作数栈

7)将两个int类型的值相加,重新放入操作数栈

8)将int类型的常量10压入操作数栈

9)将两个int类型的值相乘,重新放入操作数栈

10)将栈顶int类型数组存入局部变量3,也就是定义了一个局部变量c,为其开辟了一块存储空间,给其赋值为121

 11)将int类型局部变量3(c)推送到操作数栈

12)将操作数栈的数返回

2.2堆

堆的内存结构图如下

 

 

Eden、S0、S1归为Young区(Young Gen),即新生代,执行new时大部分对象在此分配内存,经过一定GC次数(默认15次)后进入Old区。大部分对象在Eden区中生成,当Eden区满时,会进行minor gc操作,还存活的对象将被复制到Survivor区中的一个,当此 Survivor区满时,此区的存活对象将被复制到另一个Survivor区,当此Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。

 

3.JVM调优

JVM调优的目的是为了减少GC和STW(stop the word),提高服务器的性能。STW是指JVM在做垃圾收集时,会暂停用户线程,待垃圾收集完成后再让用户线程继续执行。若GC时间过长,就会出现用户使用时常卡顿甚至无响应的现象。

3.1调优的常用的诊断工具

3.1.1 jvisualvm

jdk自带诊断工具,在cmd直接输入 "jvisualvm" 即可打开

3.1.2 arthas

阿里巴巴开发的诊断工具,它可以快速解决以下一些问题

这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到JVM的实时运行状态?
怎么快速定位应用的热点,生成火焰图?

其使用方式也很简单,下载jar后运行即可。参考地址

github:
https://github.com/alibaba/arthas

gitee:
http://arthas.gitee.io/

下载地址:https://github.com/alibaba/arthas/releases,选择需要的版本下载jar(),这里直接在Windows上进行演示,Linux也是一样的

解压后在文件夹里有arthas-boot.jar,直接用java -jar的方式启动

java -jar arthas-boot.jar

启动时会寻找本地所有的JVM进程,,需要选择应用 java 进程,这里先启动了一个main方法

package com.zxh.demo;

public class JvmTest2 


    public static void main(String[] args) 
        //模拟CPU过高运行
        cpuHigh();
    

    public static void cpuHigh() 
        new Thread(() -> 
            while (true) 
            
        ).start();
    

输入要监控的进程的编号,启动成功会有提示

 

可查看仪表盘信息

dashboard

里面显示的信息都是动态的,包含线程使用CPU情况,栈和堆区使用情况

  

可以看到,名为"Thread-0"的线程运行状态正常,但其CPU的占用率总是高达99%以上,说明此线程是有问题的,需要查询问题并优化。

查看出现问题的线程的信息

thread 线程id

这里线程id是14

可以看到指出了问题代码的行数,查看代码发现果然是有问题的,里面是一个死循环,自此已经找到问题代码,就可以优化代码了。

当然,也可以通过命令查看死锁的线程

thread -b

也可以通过反编译来查看代码是否已更新

jad 类的全路径

比如这里对JvmTest2进行反编译

jad com.zxh.demo.JvmTest2

可以看到编译后的源码

 

以上是关于JVM,你都会了吗的主要内容,如果未能解决你的问题,请参考以下文章

前端100问,这些问题你都会了吗?

接口测试面试题目,你都会了吗?

最全的Spring依赖注入方式,你都会了吗?

数据库数据库绪论,你都会了吗

Android 这 13 道 ContentProvider 面试题,你都会了吗?

亿级用户分布式存储,这些方案你都会了吗?