JVM 是 Java 程序员中高级开发的必备技能之一,也是未来长久发展的核心!!!~

什么是JVM?

JVM 是Java虚拟机。他它是实现Java代码,一处编写,处处运行的基础。也是Java二进制代码的运行环境

JVM优势:

  • 提供了自动的内存管理机制:垃圾回收机制!
  • 一处编写,处处运行的基础
  • 数组下标越界的检查

内存泄漏

说明:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

JVM、JRE、JDK 关系

JVM 是一套规范,很多公司都实现过JVM 我们常用的就是Open JDK、Oracle JDK,不同规范应用范围不同,同时受到的限制也不同!我们常用就是 OpenJDK。使用的虚拟机就是HotSpot 可以通过java- version实现。

JVM 结构

按顺序学习 由浅入深的学习 顺序是: JVM 内存结构 、 垃圾回收 、 Java Class编译优化 、 类加载器

JVM的内存划分

程序计数器 Program Counter Register

  • 作用:是记录下一条 JVM 指令的执行地址行号。(由物理的寄存器实现的)

特点:

  • 是线程私有的(属于自己的线程的)
  • 不会存在内存溢出
  • 每个线程,都有自己的程序计数器

内存溢:Out of Memory 是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。

下面是一个.class字节码信息

A列: B列                C列      // D列

0 : getstatic 		#20	// PrintStream out = System.out; 
3 : astore_1			// -- 
4 : aload_1 			// out.println(1); 
5 : iconst_1 		  	// -- 
6 : invokevirtual 	#26  	// -- 
9 : aload_1 			// out.println(2); 
10: iconst_2          		// -- 
11: invokevirtual 	#26 	// -- 
14: aload_1 		  	// out.println(3); 
15: iconst_3          		// -- 
16: invokevirtual 	#26 	// -- 
19: aload_1           		// out.println(4); 
20: iconst_4          		// -- 
21: invokevirtual 	#26 	// -- 
24: aload_1           		// out.println(5); 
25: iconst_5          		// -- 
26: invokevirtual 	#26 	// -- 
29: return

说明 每一列是一串二进制字节码 由 A B C D 四部分组成

A 列:就是 程序计数器 记录的数字

虚拟机栈 (栈帧:先进后出)

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

栈帧包含的内容:

明文就是:一个线程一个栈,一个方法就是一个栈帧。方法里创建的对象,就会创建在堆中。然后对象在栈帧中记录了方法地址去操作。当栈帧弹出,指向的对象不会消失,等待GC垃圾回收。

栈问题辨析:

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

  • 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。立即释放。

2)栈内存分配越大越好吗?(就是启动命令加上参数 -Xss就是设置的栈大小)例如 -Xss1m、-Xss1024k 、-Xss1048576

  • 不一定。因为计算机物理内存是一定的,线程占用物理内存的 ,线程中栈内存越大,虽然可以支持更多的递归调用,但是受到物理限制,(可执行的)线程数就会越少。 比如物理内存200M 每个栈占用 5M 那么最多创建40个线程执行。栈小一点 就可以创建更多的线程!

3)方法呢的局部变量是否线程安全

  • 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
  • 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

总结一句:栈帧内部自己的内存不用别人的,也不让别人用。就是线程安全的

4)栈内存溢出 (推荐使用 -Xss256k 指定栈内存大小!)

递归调用导致栈帧过多,栈帧过大、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,推荐使用 -Xss256k 指定栈内存大小!

更深层次的理解:

一个线程运行内存有限。每个方法进入栈队列变成栈帧。栈帧是有大小的。栈帧过多就会导致线程的内存有限。过多方法进入就会stackOverflowError 。内存溢出了。

线程运行诊断 CPU 占用过高

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

步骤:

用top命令得到进程(process ID)pid,再用ps命令得到进程pid中有问题的线程(thread ID) tid 最后用jstack 拿到tid 转为16进制 对比 nid

  1. top 命令,查看是哪个进程占用 CPU 过高
  2. ps H -eo pid, tid(线程id), %cpu | grep :刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高 得到tid
  3. jstack 线程id :通过对比线程的 tid ,定位出错代码的行数,通过 ps 命令看到的 nid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要我们拿到pid 转换为16进制 对比 nid。

本地方法栈

一些带有 native 的关键字修饰方法就是。需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。

此图就是Object 的 clone方法 他是native修饰的 说明实现是有底层调用本地C 或者C++来实现的

堆 Heap

  • 通过new关键字创建的对象都会被放在堆内存

特点

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

2)堆内存溢出

java.lang.OutofMemoryError :Java heap space. 堆内存溢出
可以使用 -Xmx8m 来指定堆内存大小

案例演示 项目启动是,指定VM参数:-Xmx8m

public class Jvm1 {
    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        ArrayList<byte[]> arrayList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            byte[] bytes = new byte[1024 * 1024]; // 数组占用空间1Mb。1 kb = 1024 byte
            arrayList.add(bytes);
            System.out.println("代码使用了:" + (++count) + "M");
        }
    }
}

控制台打印的结果:
代码使用了:1M
代码使用了:2M
代码使用了:3M
代码使用了:4M
代码使用了:5M
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.xxx.xxx.Jvm.main(Jvm.java:16)

堆内存诊断分析

代码

public class Jvm {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("in 1");
        Thread.sleep(30000);

        byte[] bytes = new byte[1024 * 1024 * 10];
        System.out.println("in 2");
        Thread.sleep(30000);

        bytes = null;
        System.gc();
        System.out.println("in 3");
        Thread.sleep(1000000l);
    }
}

方式一:

1、jps 查看当前系统中有哪些 java 进程 可参考:https://www.zanglikun.com/1010.html#jps

jps

当然知道我们Java项目的运行端口也可以 lsof -i:端口


2、查看堆内存占用情况 jmap - heap 进程id

jmap -heap 进程id
[root@iZuf6XXXXXXXvkjjn20Z ~]# lsof -i:18081
COMMAND   PID USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
java    10140 root  166u  IPv6 308674937      0t0  TCP *:18081 (LISTEN)


[root@iZuf6XXXXXXXvkjjn20Z ~]# jmap -heap 10140
Attaching to process ID 10140, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 536870912 (512.0MB)
   NewSize                  = 268435456 (256.0MB)
   MaxNewSize               = 268435456 (256.0MB)
   OldSize                  = 268435456 (256.0MB)
   NewRatio                 = 1
   SurvivorRatio            = 30
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 251658240 (240.0MB)
   used     = 207160352 (197.56350708007812MB)
   free     = 44497888 (42.436492919921875MB)
   82.31812795003255% used
From Space:
   capacity = 8388608 (8.0MB)
   used     = 2884576 (2.750946044921875MB)
   free     = 5504032 (5.249053955078125MB)
   34.38682556152344% used
To Space:
   capacity = 8388608 (8.0MB)
   used     = 0 (0.0MB)
   free     = 8388608 (8.0MB)
   0.0% used
PS Old Generation
   capacity = 268435456 (256.0MB)
   used     = 66860552 (63.76319122314453MB)
   free     = 201574904 (192.23680877685547MB)
   24.907496571540833% used

40435 interned Strings occupying 4567600 bytes.

Eden区 就是我们每次new出来 在堆中使用的内存区域

方式二:JDK 提供的 jconsole.exe

方式三 jps + jstat

方法区(他只是个概念,需要具体实现)

所有Java虚拟机线程共享区域,他存储了类结构相关的信息(成员变量、方法数据、构造方法、成员方法)也会存储常量池的内容。

当虚拟机启动时,就会创建方法区!他在逻辑上属于堆的内容。具体实现就根据不同虚拟机有关了。

永久代、元空间都是方法区的实现。元空间是JDK1.8出现的。它直接使用操作系统的内存。

方法区会出现内存溢出的情况

常量池(constant pool)

指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。

运行时常量池:常量池的组成部分,放的就是常量池信息

串池:也是常量池组成部分。比如运行常量池运行到String a = "A"。串池就会生成"A"。

注意String a = new String("a")。此时a指向的内容就是堆内存,String b = "a"。则指向串池的内存地址。a==b就是false!

串池的字符串,也会被垃圾回收!

a.intern() 在JDK1.8会先放入串池,返回的地址是串池地址。(没放成功,说明串池有,直接用串池地址,如果串池没有,放进去,返回新的串池地址!)

垃圾回收

1.6永久代只能Full GC。但是1.8的元空间可以miorGC

特殊说明:
上述文章均是作者实际操作后产出。烦请各位,请勿直接盗用!转载记得标注原文链接:www.zanglikun.com
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取全部资料 ❤