JDK8

JVM

在这里插入图片描述
《Java类的加载》中学习了类加载器子系统的相关知识,这篇文章学习运行时数据区相关知识。
上图运行时数据区中,方法区和堆是所有线程共享的数据区,虚拟机栈、本地方法栈、程序计数器是线程私有的数据区。即假设一个进程有五个线程,则有五组虚拟机栈、程序计数器和本地方法栈,共享一个方法区和堆。

Java运行时数据区(JVM Runtime Area)是JVM在运行期间,其对计算机内存空间的划分和分配。

程度计数器(又称PC寄存器,Program Counter Register)

  • 这里并非广义上所指的物理寄存器,JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
  • 程序计数器是一块很小的内存空间,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。程序执行过程中的循环、跳转、异常处理、线程恢复等都需要依赖程序计数器。
  • JVM多线程是通过线程轮流切换、分配处理器执行时间的方式实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
  • 如果线程在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,这个计数器值为空(Undefined)。
  • 不会发生异常,不会内存溢出

虚拟机栈(Java Virtual Machine Stack)

栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
堆解决的是数据存储的问题,即数据怎么放,放在哪。

什么是虚拟机栈

  • Java虚拟机栈也叫Java栈 ,每个线程在创建时,JVM会为这个线程创建一个私有的虚拟机栈,当线程调用某个方法时,JVM会在虚拟机栈中为这个方法创建一个栈帧,用来存储局部变量表、操作数、方法出口等信息。当被调用的方法执行完毕,栈帧就会出栈。线程在运行的过程中,只有一个栈帧处于活跃状态,即栈顶栈帧
  • 虚拟机栈的生命周期和线程相同。
  • 虚拟机栈是线程私有的

栈内存常见异常

栈内存不需要垃圾回收机制,因为栈内存中都是些入栈出栈操作,但是会发生异常,常见异常有

  • 空指针异常
  • 栈溢出异常(StackOverflowError):线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,可以设置VM options参数-Xss:数值 单位设置虚拟机栈的大小。Linux 64位系统,HotSpot VM分配的默认栈大小是1MB
    比如递归调用没有退出条件时
  • OutOfMemoryError:栈内存申请失败

栈的存储单位

  • 每个线程都有自己的栈,栈中的数据都是以栈帧的形式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧,方法执行结束,对应的栈帧就会出栈
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

栈的运行原理

  • JVM直接对Java栈的操作只有两个:对栈的压栈和出栈,遵循“后进先出”原则
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧对应的方法就是当前方法,定义这个方法的类就是当前类
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧

【扩展小知识:】阿里实现了一个新技术GCH,使不同的JVM虚拟机之间共享数据。我们知道,一个虚拟机对应一个进程,进程间可以共享数据

  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
  • Java方法有两种返回函数的方法,一种是正常的函数返回,使用return执行;另一种是抛出异常,不管使用哪种方法,都会导致栈帧被弹出

本地方法栈(Native Method Stacks)

本地方法栈和虚拟机栈作用类似,只是虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈则是为虚拟机使用本地方法服务。在HotSpot虚拟机中,本地方法栈和虚拟机栈合二为一。

本地方法(Native Method)

Java中有两种方法:Java方法和本地方法

  • Java方法是由Java编写,编译成字节码,存储在.class文件中
  • 本地方法是由其他语言编写的,编译成和处理器相关的机器代码,本地方法是和平台有关的

一个本地方法可以理解为一个Java方法调用非Java代码的接口。

在定义一个本地方法时,使用native关键字,不提供方法体,因为其实现是由非Java语言实现的。

本地库接口(JNI)的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
Object类中的clone()方法就是个本地方法,具体实现在C/C++中

public class Object {
@HotSpotIntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;
}
//等等。。。

为什么要使用Native Method

  • 与Java环境交互
    有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因
  • 与操作系统的交互
    通过使用本地方法,我们可以用Java实现了jre的底层系统的交互,甚至JVM的一些部分就是用C写的。
  • Sun’s Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互

Java堆(Java Heap)

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都存储在堆上。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。

堆可能会发生内存溢出:
在这里插入图片描述

堆内存诊断

  • jsp工具(jdk带的工具):查看当前系统中有哪些java进程
  • jmap工具(jdk带的工具):查看堆内存占用的情况,命令是heap 进程id,进程id可以通过jsp查看。这是获取某个时刻的内存快照
  • jconsole工具:运行程序后,终端输入jconsole命令,就能弹出一个图形化窗口,检测堆内存使用等

使用VM参数-Xmx可以改变堆内存的最大值:-Xmx50m表示将堆内存最大是50兆。

方法区(Method Area)

方法区被所用线程共享,一个进程对应一个方法区,用于存储已经被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码缓存等数据。

直接内存

直接内存不由JVM分配和释放,当时这块内存和NIO的缓冲区分配密切相关。传统的IO是先将文件从磁盘读到操作系统缓冲区,再读到JVM堆内存的一块缓冲区上,这样Java程序才能操作这些数据,这里的两次写缓冲区就比较耗时。有了直接内存后,文件先从磁盘读到直接内存,Java程序能操作直接内存,通过这种方式提高文件IO。
传统方式:
在这里插入图片描述
使用直接内存(物理内存映射文件,下图中的红色路线):
在这里插入图片描述
直接内存也会产生内存溢出:

public class Main{
    static int _100Mb = 1024 * 1024 * 100;
    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while(true) {
                // 分配直接内存
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
            }
        }finally {
            System.out.println(i);
        }
    }
}

在这里插入图片描述

直接内存的分配和释放

Java中的Unsafe类可以用来分配和释放直接内存,通过下面这个例子说明allocateDirect()如何和Unsafe结合管理直接内存分配和释放:

public class Main{
    static int _1Gb = 1024 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc();
        System.in.read();   
    }
}

allocateDirect()中会调用DirectByteBuffer():

// 源码
DirectByteBuffer(int cap) {                   // package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
    	// 通过Unsafe的allocateMemory方法分配直接内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
	
	// 在回调任务Deallocator对象中的run()方法
	// 中会调用unsafe.freeMemory(address);释放直接内存
	
	// Cleaner是一个虚引用类型
	// Cleaner中有一个clean()方法,clean()方法不是由主线程调用执行,
	// 而是由后台的ReferenceHandler线程去监测虚引用,一旦虚引用被回收,
	// 就会触发Deallocator对象中的run()方法执行,然后释放直接内存
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

直接内存分配和回收:

  • 使用Unsafe对象的allocateMemory()方法和freeMemory()方法分配和释放直接内存,当调用ByteBuffer的allocateDirect()方法时,会调用allocateMemory()方法分配直接内存;
  • 在ByteBuffer的实现类内部,使用了虚引用Cleaner来检测ByteBuffer对象,一旦ByteBuffer对象被JVM的垃圾回收器回收,ReferenceHandler线程就会通过Cleaner的clean()方法去执行回调任务中的run()方法,在run()方法中执行freeMemory()方法。

本文转载:CSDN博客