Jvm内存笔记(一)

一、前言
1、什么是 JVM ?
1)定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
2)好处

一次编译,处处执行
自动的内存管理,垃圾回收机制
数组下标越界检查
3)比较
JVM、JRE、JDK 的关系如下图所示


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

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

4、学习路线

ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
Method Area:类是放在方法区中。
Heap:类的实例对象。
当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。
二、内存结构

1.程序计数器

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

作用:是记录下一条jvm指令的执行地址行号。

特点:

   ●是线程私有的

   ●不会存在内存溢出

2)作用

解释器会解释指令为机器码交给cpu执行,程序计算器会记录下一条指令的地址行号,这样下一次解释器会从程序计算器中拿到指令进行解释执行。

多线程的环境下,如果两个线程发生了上下文切换,那么程序计算器会记录线程下一行执行的指令地址行号,以便于接着往下执行。

1)定义

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存(参数,局部变量,返回值等)
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
  • 问题辨析:

    垃圾回收是否涉及栈内存?
    不会,栈内存是方法调用产生的,方法结束后会弹出栈。
    栈内存分配越大越好吗?
    不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
    方法呢的局部变量是否线程安全
    如果方法内部的变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

  • 2)栈内存溢出

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

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

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

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

  1. 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 它甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存
  2. 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
  3. 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

4、堆
1)定义
Heap 堆

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

它是线程共享,堆内存中的对象都需要考虑线程安全问题
有垃圾回收机制
2)堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
可以使用 -Xmx8m 来指定堆内存大小。

3)堆内存诊断
jps 工具
查看当前系统中有哪些 java 进程
jmap 工具
查看堆内存占用情况 jmap - heap 进程id
jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
jvisualvm 工具

5、方法区
1)定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

2)组成
Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图

3)方法区内存溢出
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小
1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
4)运行时常量池
二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)
首先看看常量池是什么,编译如下代码:

然后使用 javap -v Test.class 命令反编译查看结果。

每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实的地址。

5)StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

    intern方法 1.8
    调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

    如果串池中没有该字符串对象,则放入成功
    如果有该字符串对象,则放入失败
    无论放入是否成功,都会返回串池中的字符串对象
    注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则返回的是串池中的对象。

  • 6)StringTable 的位置
    jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

    7)StringTable 垃圾回收
    -Xmx10m 指定堆内存大小
    -XX:+PrintStringTableStatistics 打印字符串常量池信息
    -XX:+PrintGCDetails
    -verbose:gc 打印 gc 的次数,耗费时间等信息
     

6、直接内存

1)定义

Direct Memory

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

2)使用直接内存的好处

         文件读写流程:

因为在java不能直接操作文件管理,需要切换到内核态,使用本地方法操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,讲数据读到系统缓冲区,然后在系统缓冲区数据,复制到java堆内存中,缺点是数据存储了两份,在系统内存中有一份,java堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程

直接内存说操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到java堆内存,从而提高了效率。

3)直接内存回收原理

public class Code_06_DirectMemoryTest {

    public static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
//        method();
        method1();
    }

    // 演示 直接内存 是被 unsafe 创建与回收
    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {

        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);

        long base = unsafe.allocateMemory(_1GB);
        unsafe.setMemory(base,_1GB, (byte)0);
        System.in.read();

        unsafe.freeMemory(base);
        System.in.read();
    }

    // 演示 直接内存被 释放
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc
        System.in.read();
    }

}

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
第一步:allocateDirect 的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

底层是创建了一个 DirectByteBuffer 对象。
第二步: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 {
        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;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 通过虚引用,来实现直接内存的释放,this为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
    att = null;
}

这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。

public void clean() {
        if (remove(this)) {
            try {
            // 都用函数的 run 方法, 释放内存
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }

可以看到关键的一行代码, this.thunk.run(),thunk 是 Runnable 对象。run 方法就是回调 Deallocator 中的 run 方法,

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 释放内存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
 

直接内存的回收机制总结

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

/**
     * -XX:+DisableExplicitGC 显示的
     */
    private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕");
        System.in.read();
        System.out.println("开始释放");
        byteBuffer = null;
        System.gc(); // 手动 gc 失效
        System.in.read();
    }

一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC  // 静止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值