引言

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

优点

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

jvm, jre, jdk之间的区别

在这里插入图片描述

JVM整体结构:

在这里插入图片描述

JVM 内存结构

JVM 内存结构主要包括:

  • 程序计数器(PC Register)
  • 虚拟机栈(JVM Stacks)
  • 本地方法栈(Native Method Stacks)
  • 堆(Heap)
  • 方法区(Method Area)

一、程序计数器(PC Register)

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

作用:记住下一条指令的执行地址。现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。

特点:

  • 程序计数器具有线程隔离性
  • 不会存在内存溢出的问题,是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
  • 程序计数器占用的内存空间非常小,可以忽略不计
  • 执行native本地方法时,程序计数器的值为空。原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计

二、虚拟机栈(JVM Stacks)

定义

含义:Java Virtual Machine Stacks(Java虚拟机栈)

  • Java线程运行时所需要的内存成为虚拟机栈
  • 每个栈中可以放入多个栈帧(Frame),栈帧用于存储局部变量表,操作数栈,动态链接方法出口等信息。
  • 每一个方法被调用到执行完毕的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程
  • 每个线程在一个时刻只能有一个活动的栈桢,对应着栈顶那个正在执行的方法

问题:

1、垃圾回收是否涉及虚拟机栈?

答:不涉及,虚拟机栈是每个线程自己使用的内存空间,不需要GC管理。

2、栈内存是否越大越好?

答:不是。虽然栈内存越大调用深度就可以越深,但是由于每个线程对应一个栈内存,则栈内存越大可分配的线程就越少。

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

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

异常:

  • 如果线程请求栈的深度大于虚拟机所允许的深度,则抛出StackOverflowError异常
  • 如果Java虚拟机栈的容量可以动态扩展,当栈扩展时无法申请到足够的内存时会抛出OutOfMemory异常
栈内存溢出(java.lang.StackOverflowError)

出现的原因:

  • 调用深度过多导致的栈桢过多超出栈内存溢出

  • 方法中过多的本地变量导致的栈桢多大而引发的栈内存溢出

线程运行诊断

案例一:CPU占用过多

定位方法

  • 在linux系统下使用top命令定位哪个进程对cpu的占用高
  • 用ps命令进一步定位是哪个线程引起的cpu占用过高
ps H -eo pid,tid,%cpu | grep 进程id
  • jstack 进程id
    • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

案例二:程序运行很长时间没有结果

使用 jstack 进程id 查看运行的相关信息:
在这里插入图片描述

三、本地方法栈

  • 本地方法栈是指为虚拟机使用到本地(Native)方法(比如调用C语言的方法)服务。
  • 本地方法栈也会在栈溢出或者栈扩展失败时分别抛出:StackOverflowError异常和OutOfMemory异常

四、Java堆

定义:

Java堆是被所有线程共享的内存区域,所有对象实例以及数组(通过new关键字创建的)都应该在堆上分配。

特点:
  • 所有线程共享的,堆中所有对象都应该考虑线程安全问题
  • 有垃圾回收机制
  • 如果从内存分配的角度来看,所有线程贡献的Java堆可以划分为多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
堆内存溢出

如果在Java堆中没有内存完成实力分配,并且堆无法再扩展时,Java虚拟机会抛出OutOfMemory异常

其中可以使用-Xmx和-Xms设置堆的最大内存和最小内存。

堆内存诊断

内存诊断工具:

  • jps工具:查看当前系统中有哪些java进程

  • jmap工具:查看堆内存的占用情况

  • jconsole工具:具有图形界面,多功能的检测工具,可以连续监测
    在这里插入图片描述

  • jvirsualvm:可以查看堆中各个对象占用内存的大小

在这里插入图片描述

五、方法区

定义:

方法区是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

  • 方法区是线程共享的

  • 如果方法区无法满足新的内存分配需求时,将抛出OutOfMemory异常

  • JDK8之前,hotspot使用永久代来实现方法区,使得垃圾收集器可以像管理堆一样管理方法区,省区了专门编写内存管理代码的工作

  • JDK8及之后在本地内存中实现的元空间(Metaspace)来代替了永久代

在这里插入图片描述

方法区内存溢出问题

方法区内存溢出的主要原因是加载的类过多导致超出内存空间

  • jdk8 之前会导致永久代内存溢出
// 报错:java.lang.OutOfMemory: PremGen space
// 参数:-XX:MaxPremSize=8M
  • jdk8 之后会导致元空间内存溢出
// 报错:java.lang.OutOfMemory: Metaspace space
// 参数:-XX:MaxMetaspaceSize=8M

问题产生的场景:当前许多主流的框架,如Spring、MyBatis对类进行增强时,都会使用CGLib这类字节码技术,当增强的类越多,就需要越大的方法区空间来保证动态生成的新类型可以载入内存。

运行时常量池

常量池:就是一张表,虚拟机执行根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里边的符号地址变为真实地址。

StringTable串池★

StringTable是运行时一个用于存储String常量的HashTable结构区域,不能够扩容。
在这里插入图片描述

StringTable串池特性
  • 常量池中的字符串仅仅是符号,第一次用到时才会变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量的拼接原理是StringBuilder(如上图所示),toString()方法实际上new String(‘a’)新对象
  • 字符串常量的拼接原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8版本中,intern方法尝试将字符串对象放入串池中,如果有则不会放入,如果没有则将其放入串池中,并将在串池中的该对象返回
    • 1.6版本中,intern方法尝试将字符串对象放入串池中,如果有则不会放入,如果没有则将它的拷贝放入串池中,并将在串池中的拷贝对象

试题:

在这里插入图片描述

  • 1.6版本下:位于永久代

  • 1.8版本下:位于堆中

  • 调整的原因:因为永久代触发GC的条件比较严格,而由于一般程序中都存在大量的过期无用字符串常量导致StringTable十分臃肿并且不能及时进行垃圾回收,所以1.8版本之后将其放在堆中

在这里插入图片描述

验证其存放位置从永久代(PermGen)到堆(Heap)的转变方法:不断创建具有引用的字符串常量,看哪个区域报OutOfMemory错误。代码如下:

/**
 * 测试StringTable所在的位置
 * //当使用UseGCOverheadLimit时,如果GC占用了98%的时间,但是只释放了2%的空间,则报此错误。
 * //这里没有使用(-号表示不使用,+号表示使用)
 * 1.8:-Xmx10m -XX:-UseGCOverheadLimit
 * 1.6: -XX:MaxPermSize=10m
 */
public class StringTableLocationTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int j = 0;
        try {
            for (int i = 0; i < 300000; i++) {
                //将j转化为字符串并放入StringTable中,再将返回的对象引用放入list
                list.add(String.valueOf(j).intern());
                j++;
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}

1.8版本的结果:

在这里插入图片描述

1.6版本结果

在这里插入图片描述

StringTable垃圾回收

测试代码

/**
 * 串池GC测试
 * 最大堆内存为10m     打印串池统计信息             打印垃圾回收的详细信息:次数、时间等
 * -Xmx10m       -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class StringTableGCTest {

    public static void main(String[] args) {
        int j = 0;
        try {
            for (int i = 0; i < 10000; i++) {
                String.valueOf(j).intern();
                j++;
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }

}

当没有在串池中加入字符串常量时,串池中总的字符串个数为1754。并且没有发生GC。
在这里插入图片描述

当试图在串池中加入10000字符串常量时,串池中总的字符串个数为7226,只增加了5000多个,因为在这个过程中发生了GC。
在这里插入图片描述

StringTable调优★
  • 如果应用中存在大量的字符串,而且这些字符串中存在大量的重复,那么就可以使用String::intern方法将字符串放入串池,减小字符串对象的个数,进而减少字符串对内存的消耗。

  • 调整 -XX:StringTableSize=桶的个数。StringTable内部采用HashTable的形式进行存储,当HashTable桶的个数非常小时,容易产生哈希冲突,减慢StringTable的存取速度。通过StringTableSize参数可以调整HashTable桶的个数,默认为60013个。

六、直接内存

定义

本机系统中的内存

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但是读写性能高
  • 由于不是JVM运行时数据区的一部分,所以不受JVM垃圾回收机制的影响
直接内存的底层原理

java要读取大文件,首先CPU要切换到内核态,然后调用Native方法将磁盘文件先读入到系统缓存区,然后从系统缓冲区复制到java缓冲区,过程中需要两次复制。
在这里插入图片描述

而使用直接内存之后,可以在系统内存中分配一块区域用于存储磁盘文件,并且Java中也可使用DirectByteBuffer作为这块内存的引用进行操作,这样就能在一些场景中显出提升性能。

[外链图片转存中...(img-uY3mHgU9-1588303260794)]

直接内存溢出

如果直接内存中存储的数据过大也会导致OutOfMemory异常:

[外链图片转存中...(img-Nyzu4NsQ-1588303260807)]

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

当直接内存使用较多,而且使用-XX:DisableExplicitGC禁用掉代码中System.gc()时,建议直接使用Unsafe对象对直接内存进行管理,示例代码如下:
在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐