JVM(1):JVM的介绍


一、 什么是JVM

定义
Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)

好处

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

比较
JVM JRE JDK的区别

学习 JVM 有什么用

面试必备
中高级程序员必备
想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。

常见的 JVM

我们主要学习的是 HotSpot 版本的虚拟机。

JVM整体架构

二、内存结构

1、程序计数器

Program Counter Register 程序计数器(寄存器)

1.1 作用

  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行

1.2 特点

  • 线程私有

    1、CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码。
    2、程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令。

  • 不会存在内存溢出

2、虚拟机栈

2.1 定义

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

代码演示

package com.ali.jvm;
public class Test {

    public static void main(String[] args) {
        method1();
    }
    private static void method1() {
        method2(1, 2);
    }
    private static int method2(int a, int b) {
        int c = a + b;
        return c;
    }
}


在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点,每一个方法都是一个栈帧,最先进入的方法是main方法,所以main方法对应的栈帧压在最下面,最后进的方法是method2,所以method2对应的栈帧在最上面
2.2 问题辨析

  • 垃圾回收是否涉及栈内存?

    不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。

  • 栈内存的分配越大越好吗?

    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

  • 方法内的局部变量是否是线程安全的?

    如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
    如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

2.3 栈内存溢出
Java.lang.stackOverflowError 栈内存溢出

发生原因

  • 虚拟机栈中,栈帧过多(无限递归)
  • 每个栈帧所占用过大

代码演示1:

package cn.itcast.jvm.t1.stack;

/**
 * 演示栈内存溢出 java.lang.StackOverflowError
 * -Xss256k
 */
public class Demo1_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }
    private static void method1() {
        count++;
        method1();
    }
}

图上所示就是无限递归,当调用了一万八千多次之后,出现栈内存溢出

代码演示二:

package cn.itcast.jvm.t1.stack;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
 * json 数据转换
 */
public class Demo1_19 {

    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");

        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2 = new Emp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));

        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}

class Emp {
    private String name;
//    @JsonIgnore
    private Dept dept;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dept getDept() {
        return dept;
    }

    public void setDept(Dept dept) {
        this.dept = dept;
    }
}
class Dept {
    private String name;
    private List emps;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List getEmps() {
        return emps;
    }

    public void setEmps(List emps) {
        this.emps = emps;
    }
}

2.4 线程运行诊断
案例一:CPU占用过高
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

1、top 命令,查看是哪个进程占用 CPU 过高
2、ps H -eo pid, tid(tid是线程id的意思), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
3、jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

代码演示:

package cn.itcast.jvm.t1.stack;

/**
 * 演示 cpu 占用过高
 */
public class Demo1_16 {

    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}

先用javac命令编译Demo1_16.java文件,然后在项目根目录下执行nohup java cn.itcast.jvm.t1.stack.Demo1_16 &命令,再用top命令查看


再用ps H -eo pid,tid,%cpu | grep 24634命令,得到是线程24662的cpu占用率为98.9%

再用命令jstack 24634 得到如下图:

3、本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
例如:Object类中由很多这样的方法

4、堆

定义:通过new关键字创建的对象都会被放在堆内存
特点:

  • 所有线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

堆内存溢出
代码演示:

package cn.itcast.jvm.t1.heap;

import java.util.ArrayList;
import java.util.List;

/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}


堆内存诊断的几种工具

  • jps工具:查看当前系统中由哪些java进程
  • jmap工具:查看堆内存占用情况
  • jconsole工具:图形界面的,多功能的检测工具,可以连续监测
JVM