jvm相关知识详解

Posted 野生java研究僧

tags:

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

jvm相关知识详解

1.hello world!?

我相信大多数人学一门语言都是先从 hello world 开始的,如果成功运行hello world 那么恭喜你,成功进入编程世界的大门。

public static void main(String[] args) 
        System.out.println("hello world!");
    

当我们学了一门语言后,学习了API?而且会调用API,那么想要更进一步写出好的代码,那就得学习一下jvm了,就比如说你遇到的 StackOverflowError是如何引起的? 我们所定义的变量是存在什么位置的? 对象什么时候被垃圾回收器回收?这一系列问题学完jvm就有了一个新的理解,也能写出比较高效的代码,遇到问题也能快速定位。

我们的cpu只认识机器码:也就是由0和1组成的指令,那么我们编写的hello world 是如何交给cpu进行运算的呢?

为什么说java是跨平台语言?(一次编译到处运行)

这个夸平台是中间语言(JVM)实现的夸平台
java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统,我们自己写的java源代码不用更改,只需要更换不同的jdk即可。

难道 C 和 C++ 不能夸平台吗 其实也可以,C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样。

Jdk和Jre和JVM的区别

看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM

Jvm在倒数第二层 由他可以在(最后一层的)各种平台上运行

Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库

Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

常见的jvm,注意不同版本的的jdk,jvm实现是不一样的。该文章基于HotSpot实现来描述的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aCHxiblr-1666016850022)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211212230847531.png)]

2.JVM数据区域划分

首先我们来看下VM的一个结构图:接下来会根据这个结构图进行详细的一个说明

2.1 程序计数器

**程序计数器(Program Counter Register):**了解程序计数器的概念之前,我们先来了解下栈数据结构,便于我们理解程序计数器。

程序计数器是一块比较小的内存空间,是线程私有的,每个线程都有自己的一个程序计数器,可以看成是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过这个程序计数器来取下一条需要执行的字节码指令,程序计数器是程序控制的指示器,分支,循环,跳转,异常处理,线程恢复等基础的功能都需要这个程序计数器来完成。由于java多线程是通过线程的切换,分配处理器的执行时间来实现的,一个处理器(对于多核的处理器来说是一个内核)都只会执行一条线程中的指令,因此为了线程之间相互切换执行后能恢复到正确的执行位置,每一个线程都需要一个自己独立的程序计数器,各条线程之间程序计数器互不影响,数据存储独立,我称这类内存区域为:线程私有的内存,程序计数器在物理上是通过寄存器来实现的,寄存器是读取速度非常快的。

  • 程序计数器的作用:记住下一条指令的执行地址
  • 程序计数器的特点:线程私有,每个线程都有自己的程序计数器,不会出现内存溢出(OutOfMemoryError)的数据区域

首先我们来了解一下jvm字节码指令,这就是通过 javap -c Test.class这个指令来进行返编译的

 public com.compass.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_2
       1: invokestatic  #2                  // Method methodOne:(I)Z
       4: pop
       5: return

  public static boolean methodOne(int);
    Code:
       0: iload_0
       1: invokestatic  #3                  // Method methodTwo:(I)V
       4: iload_0
       5: iconst_3
       6: if_icmpne     13
       9: iconst_1
      10: goto          14
      13: iconst_0
      14: ireturn

  public static void methodTwo(int);
    Code:
       0: return


2.2 Java虚拟机栈

**Java虚拟机栈(Java Virtual Machine Stack) : **

与程序计数器一样,java虚拟机栈也是线程私有的,他的生命周期与线程相同,线程结束java虚拟机栈也就随之消失。虚拟机栈是描述java方法执行的线程内存模型,每个方法被执行的时候java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈(对数据进行计算的一个区域),动态链接,方法返回地址,每个方法被调用直至执行完毕的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表存放了编译器可知等待各种Java虚拟机基本数据类型(int,byte,char,long,double,boolean,short,float),对象引用(Refernce,他并不等同于对象本身,可能是一个指向对象起始地址的一个引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置和ReturnAddress[指向了一条字节码指令地址])

这些数据类型在局部变量表的储存空间以局部变量槽(Slot)来表示,其中64位长度的long和double占用两个变量槽,其余的数据类型只占用一个局部变量表所需空间在编译器完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是确定的,方法在运行期间不会改变局部变量表的大小

补充一点:基本数据类型存放的位置不是完全在java虚拟机栈中的,具体的话要看这个变量声明在说明位置

  • 第一种:类变量,由static关键字修饰的成员变量,随之类的加载而加载,存放在方法区中,可以通过类名.变量名直接调用
  • 第二种:类成员变量,随对象的创建而加载,对象存放在堆中(对象的引用存放在栈中)
  • 第三种:在方法内部声明,存放在栈中,随方法的调用而入栈,方法调用完出栈该变量也就声明周期结束

在<<java虚拟机规范>>中规定java虚拟机栈中出现的两类异常:

  • 一:如果线程请求的栈深度大于java虚拟机锁规定的大小,将抛出StackOverflow异常(最常见的就是递归方法调用太深)

  • 二:如果java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常

java虚拟机栈精简概括:

  1. 每个线程运行时所需要的内存,称为虚拟机栈

  2. 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  3. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

  4. 虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,方法出口

问题辨析:

  1. 垃圾回收是否涉及到虚拟机栈内存?答案:不会涉及,因为进入一个方法时创建一个栈帧,在该方法执行完毕后,出栈后,该栈帧的的局部变量都会被随之释放。
  2. 栈内存空间分配的越大越好吗?答案:不是的,栈空间分配的越大随之线程数也会随之减少,但是也不宜分配的太小,太小会导致无法创建Java虚拟机。
  3. 方法内的局部变量是否线程安全?
    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

我们来看下一个递归调用出现的StackOverflow异常

  public static void main(String[] args) 
         methodOne();
    
    public static void methodOne()
        // 自己调用自己形成递归,没有递归结束条件
        methodOne();
    

java虚拟机栈的大小是可以进行改变的,设置参数如下:

linux64位的操作系统默认是:1024kb

windows:根据windows的虚拟内存进行分配

-Xss1m
-Xss1024kb    

开发过程中的一些栈内存溢出:如java对象转JSON字符串,Lombok的toString (循环相互依赖,导致递归过深)

使用Gson将java bean转Json字符串(对象直接相互引用)就会出现 StackOverflowError

public class Employee 


   int  id;
   int age;
   String name;
    Department dept;

    public Employee(int id, int age, String name, Department dept) 
        this.id = id;
        this.age = age;
        this.name = name;
        this.dept = dept;
    

    public Employee() 
    

class Department 

    int depId;
    String depName;
    List<Employee> employees;

    public Department(int depId, String depName, List<Employee> employees) 
        this.depId = depId;
        this.depName = depName;
        this.employees = employees;
    
    public Department() 

    

class Test
    public static void main(String[] args) 

        List<Employee> list = new ArrayList<>();
        Department department = new Department(2, "开发", list);

        Employee employee= new Employee(2, 21, "杰克", department);

        list.add(employee);

        Gson gson = new Gson();

          System.out.println( gson.toJson(employee));
          System.out.println( gson.toJson(department));

    

解决方案:不让他们产生循环依赖关系:

public class Employee 

    @Expose
    int  id;
    @Expose
    int age;
    @Expose
    String name;
    @Expose
    Department dept;

    public Employee(int id, int age, String name, Department dept) 
        this.id = id;
        this.age = age;
        this.name = name;
        this.dept = dept;
    



class Department 
    @Expose
    int depId;
    @Expose
    String depName;
    // 在进行转换时,忽略掉该字段
    List<Employee> employees;

    public Department(int depId, String depName, List<Employee> employees) 
        this.depId = depId;
        this.depName = depName;
        this.employees = employees;
    


class Test
    public static void main(String[] args) 

        List<Employee> list = new ArrayList<>();
        Department department = new Department(2, "开发", list);

        Employee employee= new Employee(2, 21, "杰克", department);

        list.add(employee);

        Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

          System.out.println( gson.toJson(employee));
          System.out.println( gson.toJson(department));

    

成功进行转换:

CPU占用过高分析定位:

在linux环境下后台运行以下代码:

public class Test 
    public static void main(String[] args) 
      methodOne();
    
    public static void methodOne()
       new Thread(()->
           while (true);
       ).start();
    





  1. javac Test.java
  2. nohup java Test & (以后台启动的方式运行该java程序)
  3. 启动后使用top命令查看系统cpu的一个使用情况,看到java程序 cpu的使用率,直接高达100%

4.我们已经知道进程id,通过 ps H -eo pid,tid,%cpu | grep 5487来查看该进程下有哪些线程 ,可以看到是 5506这个线程

  1. jstack 5487(进程id) 将线程编号转化为十六进制,找到该线程
                                                                                     // 十六进制的线程id
"Thread-0" #11 prio=5 os_prio=0 cpu=33241.74ms elapsed=33.24s tid=0x00007fc0ec1e8000 nid=0x1582 runnable  [0x00007fc0bd4fa000]
   java.lang.Thread.State: RUNNABLE
	at Test.lambda$methodOne$0(Test.java:8)
     // 在我们的第8行代码出现了问题 
	at Test$$Lambda$1/0x0000000100060840.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)

我这里测试完我就直接将其杀死【实际开发中不要直接杀死该进程】:skill -9 5487(进程id)

检测线程死锁:

以下代码就会出现死锁问题

public class Test 

  final static  Object lockA = new Object();
  final static Object lockB = new Object();

    public static void main(String[] args) 

        new Thread(()->

            synchronized (lockA)
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                synchronized (lockB)
                    System.out.println("Thread-A");
                
            
        ,"Thread-A").start();

        new Thread(()->
            synchronized (lockB)
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                synchronized (lockA)
                    System.out.println("Thread-B");
                
            
        ,"Thread-B").start();
    


  1. 使用 jps -l 指令查看正在运行的java程序,找到我们的Test类
  2. 使用 jstack 进程id 即可查看关系该进程的一个堆栈信息
  3. 关键信息如下:
ava stack information for the threads listed above:
===================================================
"Thread-A":
	at Test.lambda$main$0(Test.java:17)
	- waiting to lock <0x00000000c8a06c48> (a java.lang.Object)
	- locked <0x00000000c8a06c38> (a java.lang.Object)
	at Test$$Lambda$1/0x0000000100060840.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)
"Thread-B":
	at Test.lambda$main$1(Test.java:30)
	- waiting to lock <0x00000000c8a06c38> (a java.lang.Object)
	- locked <0x00000000c8a06c48> (a java.lang.Object)
	at Test$$Lambda$2/0x0000000100061040.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)
// 发现一个死锁
Found 1 deadlock.

2.3 本地方法栈

**本地方法栈(Native Method):**本地方法栈和java虚拟机栈很相似,只不过本地方法栈是为java源代码中带Native关键字修饰的方法所服务的,而虚拟机栈是为那些没有带Native关键字修饰的方法(也就是字节码)所服务的,本地方法栈也会在栈深度溢出或栈扩展失败时抛出StackOverflowError和OutOfMemoryError,Native关键字的方法是看不到的,必须要去oracle官网去下载才源码才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。

就比如说Object类中就有很多Native关键字修饰的方法,不只是Object类,其余的别的类都有Native关键字修饰的方法。

    public final native Class<?> getClass();
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;

2.4 java堆

java堆(heap):

java堆是虚拟机所管理的内存最大的一块区域,java堆被所有的线程所共享的一块内存区域,所以堆中的共享对象需要考虑线程安全问题,java堆在jvm创建的时候分配内存,java堆内存的唯一目的就是存放对象的实例,无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存,《java虚拟机规范》中对堆的描述是:所有的对象以及数组都应该在java堆中分配,java堆是垃圾回收器管理的主要区域,因此也称为GC(Collected Heap)堆。如果在java堆中没有完成实例分配,并且堆无法在进行扩展时,java虚拟机将会抛出 OutOfMemoryError

指定对堆内存分配大小的指令:

-Xmx83886080
-Xmx81920k
-Xmx80m

我们来看下 堆内存溢出的一个代码:

出现异常:Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

public class Test 

    static class OOMObject

    
    // 该案例来自:《深入理解java虚拟机3》
    public static void main(String[] args) 
        //  -Xms20m :堆内存的最小值  Xmx20m :堆内存的最大值 (最大值和最小值都为20m避免堆内存自动扩展)
        // -XX:+HeapDumpOnOutOfMemoryError :出现内存溢出时dump出当前内存堆转存储快照便于事后分析
        //添加java虚拟机参数运行以下代码: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
        List<OOMObject> list = new ArrayList<>();
        while (以上是关于jvm相关知识详解的主要内容,如果未能解决你的问题,请参考以下文章

《深入理解JAVA虚拟机》垃圾回收时为什么会停顿

还没搞懂JVM吗?95%的技术面试必问知识点都在这,还怕面不过?

jvm调优

JVM参数设置分析

[转]JVM系列三:JVM参数设置分析

JVM系列三:JVM参数设置分析