【JVM学习】内存结构

JVM内存结构

一 程序计数器

在这里插入图片描述

1.1 定义

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

作用:记住下一条jvm指令的执行地址

特点:

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

1.2 作用

在这里插入图片描述

  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
  • 多线程的环境下,如果两个线程发生了上下文切换,由于程序计数器是线程私有的,不同的线程有不同的程序计数器记录各自的执行指令地址,所以程序计数器会取线程下一行指令的地址行号,以便于接着往下执行。

二 虚拟机栈

2.1 定义

Java Virtual Machine Stacks(Java 虚拟机栈)

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

注:
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

问题辨析:
1 垃圾回收是否涉及栈内存?
不会,当方法调用时,会同步创建栈帧,当方法执行完毕后,就会自动出栈。
2 栈内存分配越大越好吗?
不是,物理内存是一定的,栈内存分配多了,可以支持方法更多的递归调用,但是由于单个线程占用内存的提高,线程数会减少,运行会变慢。
3 方法内的局部变量是否线程安全?

  • 首先,变量的线程安全,可以简单理解为并发情况下,该变量不会受多个线程的影响。
  • 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

2.2 栈内存溢出

栈帧过多导致栈内存溢出(较常见,比如递归过多)

栈帧过大导致栈内存溢出(较少见)

注:栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!(-Xss256k为VM options,虚拟机参数的一种,可以设置栈内存的大小)

2.3 线程运行诊断

案例一:cpu 占用过多

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

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

三 本地方法栈

  • 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的。
  • 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
  • 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemoryError异常。

什么是本地(Native)方法?

  • 一些带有 native 关键字的方法就是本地方法,这类方法很多,比如Object中的hashCodeclone方法。

在这里插入图片描述

在这里插入图片描述

  • 因为 Java 有时候没法直接和操作系统底层交互,只能调用本地的C或者C++方法(也就是native方法),间接和底层交互。

四 堆

4.1 定义

Heap 堆

  • 通过new关键字,创建对象都会使用堆内存

特点

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

4.2 堆内存溢出

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

4.3 堆内存诊断工具

  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id,查看的是当前堆内存占用情况,非连续
  3. jconsole 工具
    图形界面的,多功能的监测工具,可以连续监测
  4. jvisualvm 工具
    jconsole一样是图形界面的,可以通过堆转储 dump 功能抓取内存快照,分析占用内存最多的那些对象

五 方法区

5.1 定义

Method Area 方法区

  • 方法区是线程共享的。
  • 方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中的“文本”段。
  • 它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化和接口初始化中使用的特殊方法

特点

  • 尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。
  • 虚拟机规范不强制指定方法区的位置或用于管理已编译代码的策略。
  • 方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。
  • 方法区域的内存不需要是连续的!

5.2 组成

  • HotSpot虚拟机在JDK1.6时方法区的实现是永久代(PermGen)
  • HotSpot虚拟机在JDK 1.8时改用与JRockit、J9一样在本地内存中实现的元空间(MetaSpace)来代替
    在这里插入图片描述

5.3 方法区溢出

  • JDK1.8 之前会导致永久代内存溢出,抛出java.lang.OutOfMemoryError: PermGen space错误
    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小
  • JDK1.8 之后会导致元空间内存溢出,抛出java.lang.OutOfMemoryError: Metaspace错误
    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

5.4 运行时常量池

*.class 文件(二进制字节码文件)的信息

先给出一段简单的代码,运行该代码

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

然后使用 javap -v HelloWorld.class 命令反编译查看结果。
可以发现,二进制字节码主要包括类基本信息,常量池,类方法定义(包含了虚拟机指令)。
其中常量池就是一张表,包含地址和符号的对应关系。
在这里插入图片描述
在看到类方法定义中的主函数,其中每条虚拟机指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
在这里插入图片描述

常量池和运行时常量池

经过上述过程的反编译,我们已经知道了二级制字节码包括的信息,特别是理解了常量池是什么,下面就可以知道运行时常量池和常量池之间的关系了。

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

5.5 StringTable

StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

StringTable 面试题

根据上述StringTable(串池)的特性,分析下面代码的输出。

// jdk 1.8环境
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中
    // 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    public static void main(String[] args) {
    	// 下面三行代码都是用到时才去创建字符串对象
    	// 也就说它们是懒惰的
        String s1 = "a"; 
        String s2 = "b";
        String s3 = "ab";
        // 上述三行代码执行完后,串池的情况如下
        // StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
        
        // 字符串变量拼接,相当于执行了下面的代码
        // new StringBuilder().append("a").append("b").toString()
        // toString()内部其实调用了new String("ab")  
        String s4 = s1 + s2; 
        // 字符串常量拼接,javac在编译期间的优化,结果已经在编译期确定为ab
        String s5 = "a" + "b";  
        // 已有,不放入,返回串池中的ab
        String s6 = s4.intern();
		// 问
		// false,s3串池中,s4堆中
		System.out.println(s3 == s4); 
		// true,s3和s5都是串池中的,同一个
		System.out.println(s3 == s5); 
		// true
		System.out.println(s3 == s6); 
		String x2 = new String("c") + new String("d");
		String x1 = "cd";
		// 已有,不放入
		x2.intern();
		// false
		System.out.println(x1 == x2);
    }
}

internjdk1.6jdk1.8的区别

例子1——jdk1.8环境

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {

        String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        String s2 = s.intern(); 
	
        System.out.println( s2 == x); // true
        System.out.println( s == x ); // false
    }

}

例子2——jdk1.6环境

public class Demo1_23 {

    // ["a", "b", "ab"]
    public static void main(String[] args) {


        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b")  new String("ab")
        // 将这个字符串对象尝试放入串池,如果有则并不会放入;
        // 如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
        String s2 = s.intern(); 
        // s 拷贝一份,放入串池

        String x = "ab";
        System.out.println( s2 == x); // false
        System.out.println( s == x ); // true
    }

}

5.6 StringTable的位置

  • jdk1.6 StringTable 位置是在永久代中。
  • jdk1.8 StringTable 位置是在堆中。

5.7 StringTable垃圾回收

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

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo1_7 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }
}

5.8 StringTable调优

  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
  • 可以通过设置虚拟机参数-XX:StringTableSize=桶个数来调整HashTable桶的个数(最少设置为 1009 以上)
  • 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池

六 直接内存

6.1 定义

Direct Memory 直接内存

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

6.2 分配和回收原理

代码演示

public class Code_06_DirectMemoryTest {

    public static int _1GB = 1024 * 1024 * 1024;

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

	  // 演示 直接内存的分配和释放
	    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();
	    }

	    // 演示 直接内存 是被 unsafe 创建与回收(底层原理)
	    private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException {
			// 通过反射获取Unsafe
	        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();
	    }

}

直接内存_释放原理

直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。

① 查看 allocateDirect 的实现
在这里插入图片描述
可以发现其内部创建了一个DirectByteBuffer 对象。

② 查看 DirectByteBuffer
在这里插入图片描述

  • 可以发现内部确实是通过unsafeallocateMemorysetMemory分配直接内存
  • 接着调用了一个 Cleanercreate 方法,传入两个参数,第一个是关联的DirectByteBuffer对象,第二个是回调任务对象Deallocator(在内部实现了run方法,通过unsafe.freeMemory 来手动释放内存)

在这里插入图片描述

③ 查看Cleaner

  • Cleaner是一个虚引用类型,当它所关联的对象被回收时,Cleaner会触发clean方法
  • 后台线程会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleanerclean方法,来清除直接内存中占用的内存。

在这里插入图片描述

 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()thunkRunnable 对象。run 方法就是回调 Deallocator 中的 run 方法去释放内存。

直接内存的回收机制总结

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

6.3 禁止显式回收

/**
     * -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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值