一、JVM构成

jvm:java virtual machine java文件经过编译后生成字节码文件,然后装载到java虚拟机上运行,屏蔽了底层操作系统细节,一次编译,多个环境运行

jre:java runtime environment 由JVM+基础类库组成,包括java.lang.*,java.util.*,java.io.*等等

jdk:java development kit 由JVM+基础类库+编译工具组成,包括javac、jstack、jstat、jmap,jconsole等常用工具

 二、JVM内存结构

  Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存将会包括以上几个运行时数据区域:堆区、方法区、程序计数器、虚拟机栈、本地方法栈

其中方法区和堆是所有线程共享的数据区,程序计数器,虚拟机栈,本地方法栈是线程隔离的数据区,画一个逻辑图

 三、程序计数器

  程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。为什么要记录当前线程所执行的字节码的行号?直接执行完不就可以了吗?因为代码是在线程中运行的,线程有可能被挂起。即CPU一会执行线程A,线程A还没有执行完被挂起了,接着执行线程B,最后又来执行线程A了,CPU得知道执行线程A的哪一部分指令,线程计数器会告诉CPU。特点:

  • 是线程私有的
  • 不会存在内存溢出

四、虚拟机栈

  虚拟机栈存储当前线程运行方法所需要的数据,指令,返回地址。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  局部变量表存储存储局部变量,是一个定长为32位的局部变量空间。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用以个。引⽤类型(new出来的对象)如何存储?看下图

public int methodOne(int a, int b) {
    Object obj = new Object();
    return a + b;
}

  如果局部变量是Java的8种基本基本数据类型,则存在局部变量表中,如果是引用类型。如String,局部变量表中存的是引用,而实例在堆中。

假如methodOne方法调用methodTwo方法时, 虚拟机栈的情况如下

 当虚拟机栈无法再放下栈帧的时候,就会出现StackOverflowError。-Xss 参数可以设置jvm每个线程的堆栈⼤⼩。

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

问题辨析
1. 垃圾回收是否涉及栈内存?不存在,垃圾回收只针对堆内存
2. 栈内存分配越大越好吗?不是,栈内存方法一出站就会被回收,分配的越大,可用的线程数就越少
3. 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

线程运行诊断:cpu占用百分比过高

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
  • jstack 进程id|grep 线程id(十六进制)

 五、本地方法栈

  本地方法栈(Native Method Stack)与虚拟机栈锁发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为n虚拟机使用到的Native方法服务

 六、Heap堆

通过new关键字,创建对象都会使用堆内存。堆内存溢出会抛出OutOfMemoryError:java heap space,可通过-Xmx:设置堆的最大内存,-Xms:设置堆的初始内存

特点:

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

 堆内存诊断工具:

1. jps 工具
查看当前系统中有哪些 java 进程
2. jmap 工具(监控某一时刻)
查看堆内存占用情况 jmap - heap 进程id
3. jconsole 工具(连续性的监控,还可监控cpu、线程等)
图形界面的,多功能的监测工具,可以连续监测
4. jvisualvm(同jconsole,可查看堆dump,查找最大内存的n个对象)

 七、方法区

  方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。jdk1.8之前是堆上开辟一段叫永久代的内存空间,1.8以后使用操作系统的内存空间,称为元空间,字符串常量也移到堆内存了。

      1.8之后会导致元空间内存溢出异常java.lang.OutOfMemoryError: Metaspace,可通过参数配置元空间内存大小 -XX:MaxMetaspaceSize=8m

      1.8以前会导致永久代内存溢出异常java.lang.OutOfMemoryError: PermGen space,使用-XX:MaxPermSize=8m配置永久代大小

     场景:spring.mybatis cglib代理使用类加载器加载大量类到方法区

八、直接内存

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

分配和回收原理

  使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存