JVM(Java Virtual Machine)(一)内存结构

 

目录

1.什么是JVM

定义: 

好处:

关系:

2.学习JVM的作用

3.常见的JVM

4.学习路线

​一、内存结构

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

1.1 定义 

1.2 作用

1.3 特点

2.虚拟机栈(JVM Stacks)

2.1 定义

2.2 作用

2.3 特点 

2.4 问题辨析

3.本地方法栈(Native Method Stacks)

3.1 定义 

3.2 特点

4.堆(Heap)

4.1 定义

4.2 作用及特点

4.3 堆内存溢出以及如何调整堆内存的大小

4.4 堆内存诊断

4.4.1 jps工具:(java process statistic Java进程统计)

4.4.2 jmap工具(相当于给进程拍照片)

4.4.3 jconsole工具(连续监测)

 4.4.4 jvisualvm可以查看对象个数,以及对象的详细信息

5.方法区(Method Area) 

5.1 定义

5.2 特点

5.3 组成

​ 5.4 方法区内存溢出

5.5 运行时常量池(区别于常量池)

5.5.1 两者区别

​5.6 StringTable


1.什么是JVM

定义: 

Java程序的运行环境(Java二进制字节码的运行环境也就是被编译过后的.class文件的运行环境)

好处:

1.一次编写到处运行(只要安装了jdk)

2.自动内存管理,垃圾回收机制

3.数组下标越界检查

4.多态

关系:

jvm、jre、jdk

 

2.学习JVM的作用

1.理解底层的实现原理

2.向更高级的程序员看齐

3.常见的JVM

由于JVM是一种规范,所以实现它的公司和团体有很多,我们较常用的还是(HotSpot,Oracle JDK edition )

4.学习路线

一、内存结构

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

1.1 定义 

JVM规范—程序计数器:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1 

当执行一条指令时,首先需要根据PC中存放的指令地址,将指令读取到指令寄存器中,称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

1.2 作用

 记住下一条jvm指令的执行地址。

过程:二进制字节码-->解释器-->机器码-->cpu

解释器从程序计数器中获取下一条jvm指令的执行地址。

1.3 特点

1.是线程私有的,每个线程都有自己的程序计数器。

2.不会存在内存的溢出问题,仅仅存储指令的地址,有一定的大小限制

2.虚拟机栈(JVM Stacks)

2.1 定义

 JVM规范—虚拟机栈:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

 每个jvm都有一个私有的jvm栈,与线程同时创建。一个Jvm栈存储栈帧。jvm栈类似于传统的C语言的栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。因为jvm栈从来不直接操作,除了入栈和出栈,栈帧可能被分配。jvm堆栈的内存不需要是连续的。

在jvm规范第一版中,jvm栈被称为Java栈。

该规范允许jvm栈具有固定的大小,或者根据计算需要动态地扩展和收缩。如果jvm栈是固定大小的,则在创建栈时可以独立的选择每个Jvm的大小。

一个Jvm实现可以为程序员或用户提供对 Jvm栈初始大小的控制,以及在动态扩展或收缩 Jvm栈的情况下,对最大和最小大小的控制。

以下异常情况和jvm虚拟机栈相关联:

·如果一个线程中的计算需要一个比允许的更大的jvm栈,则jvm抛出一个StackOverflow Error。

·如果可以动态扩展jvm栈,并且尝试了扩展,但可以提供的内存不足,无法实现扩展,或者,如果没有足够的内存可用于为新线程创建初始jvm栈,则jvm抛出Out of Memory Error。

2.2 作用

2.3 特点 

1.是线程私有的,每个线程都有自己的虚拟机栈。

2.每个栈由多个栈帧(Frame)组成,对应着每个方法调用时所占用的内存

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

4.会出现内存溢出的情况.

(1)栈内存过多导致内存溢出,比如递归调用

(2)栈帧过大导致栈内存溢出

2.4 问题辨析

1.垃圾回收是否涉及栈内存?

栈是由多个栈帧组成,栈帧按照先进后出的执行策略,每一个栈帧执行完成会自动弹出虚拟机栈,从而释放内存,所以栈内存不需要垃圾回收。

2.栈内存分配越大越好吗?

虚拟机栈设置越大,相应的线程数会变小

https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html

 

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

如果方法内的局部变量没有逃离方法的作用范围,它是线程安全的

如果局部变量引用了对象,或者逃离了方法的作用范围,需要考虑线程安全问题

3.本地方法栈(Native Method Stacks)

3.1 定义 

  JVM规范—本地方法栈: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

 jvm的实现可以使用常规的栈(俗称“C栈”)来支持方法(用Java编程语言以外的语言编写的方法)。本地方法栈也可以由jvm指令集的解释器的实现使用,例如C无法加载方法并且本身不依赖于常规栈的jvm实现不需要提供本地方法栈。如果提供,则在创建每个线程的时候,通常为每个线程分配本地方法栈。nativenative

此规范允许本地方法栈具有固定的大小,或者根据计算的需要动态扩展和收缩。如果本地方法栈的大小是固定的,则在创建该栈时,可以独立选择每个本地方法栈的大小。

jvm实现可以为程序员或者用户提供对本地方法栈初始大小的控制,以及在大小不同的本地方法栈的情况下,对最大和最小方法栈大小的控制。

以下异常情况和本地方法栈相关联:

·如果一个线程中的计算需要的本地方法栈大于允许的范围,则jvm抛出一个StackOverflow Error。

·如果本地方法栈可以动态扩展,并且尝试了扩展,但可以提供的内存不足,无法实现扩展,或者,如果没有足够的内存可用于为新线程创建初始本地方法栈,则jvm抛出Out of Memory Error。

3.2 特点

1.本地方法栈与虚拟机栈的区别是,虚拟机栈执行的的是Java方法,本地方法栈执行的是本地方法(Native Method),其他的基本上一致

2.线程私有的

3.会内存溢出

4.例如

Object中的clone()

FileInputStream中的read()

FileOutputStream中的write(int b)

4.堆(Heap)

4.1 定义

JVM规范—堆: 

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

 jvm具有一个在所有jvm线程之间共享的堆。堆是从中分配所有类实例和数组的内存的运行时数据区域。

堆创建于jvm启动时。对象的堆存储由自动存储管理系统(称为垃圾回收器)回收,从不显示解除分配对象。jvm假定没有特定类型的自动存储管理系统,存储管理技术可以根据实施者的系统要求进行选择。堆可以是固定的大小,也可以根据计算的需要进行扩展,如果不需要更大的堆。堆的内存不需要是连续的。

jvm实现可以为程序员或用户提供对堆的初始大小的控制,以及如果堆可以动态扩展或收缩,则可以控制最大和最小堆大小。

以下异常情况与堆相关联:

·如果计算需要的堆比自动存储管理系统可用的堆多,jvm将抛出一个OutOfMemoryError

4.2 作用及特点

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

2.堆是线程共享的,堆中对象都需要考虑线程安全的问题

3.堆有垃圾回收机制

4.3 堆内存溢出以及如何调整堆内存的大小

package Heap;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
 * -Xmx8m
 */
public class Heap_4_2_1 {
    public static void main(String[] args) {
        int i = 0;
        try{
            List<String> list = new ArrayList<>();
            String s = "hello";
            while(true){
                list.add(s);
                s += s; // hello  hellohello  hellohellohellohello
                i++;
            }
        }catch(Throwable e){
           e.printStackTrace();
            System.out.println(i);
        }
    }
}

 

-Xmx8m可以调整堆内存的大小为8M

 

 

 

 

4.4 堆内存诊断

package Heap;
public class Heap_4_3_1 {
    public static void main(String[] args) throws InterruptedException{
        // 先使用jps命令,然后再在1 2 3输出之后分别使用jmap命令查看堆内存使用情况
        System.out.println("1...................");
        Thread.sleep(30000);
        byte[] bs = new byte[1024*1024*10]; // 10MB
        System.out.println("2...................");
        Thread.sleep(40000);
        bs = null;
        System.gc();
        System.out.println("3...................");
        Thread.sleep(1000000L);
    }
}

4.4.1 jps工具:(java process statistic Java进程统计)

查看当前系统中有哪些java进程 ,右键进入Terminal可以直接进到路径下面

4.4.2 jmap工具(相当于给进程拍照片)

查看堆内存占用情况,jmap - heap 进程id 查看堆内存使用变化

ps:前面的这些都是基本的堆内存的配置情况 

接下来我们看堆内存的使用情况:

下面这张图片是上述程序刚运行的时候堆内存的使用情况:

new byte[]之后: 

将对象的引用置为null并且进行显示的gc之后: 

 

 可以明显地看到堆内存的变化情况:

4.4.3 jconsole工具(连续监测)

 图形界面的,多功能的监测工具,可以连续监测,堆内存变化。

自动弹出:

 

选择你要监测的程序:链接的时候可能会提示不安全的链接,直接连接就行

 

 可以清晰的看到内存的动态变化过程。

 4.4.4 jvisualvm可以查看对象个数,以及对象的详细信息

package Heap;
import java.util.ArrayList;
import java.util.List;
/**
 * 演示查看对象个数
 */
public class Heap_4_3_2 {
    public static void main(String[] args) throws InterruptedException{
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
        }
        Thread.sleep(1000000L);
    }
}
class Student{
    private byte[] big = new byte[1024*1024];
}

我们首先依然使用jconsole查看一下内存使用情况,我们中间进行一下gc(右上角)发现内存使用减少的不明显,我就想知道到底是谁占用内存这么多,并且gc回收不掉。

 

 也是会自动弹出来:选择要查看的程序,双击

点击抽样器,点击内存。

 我们可以看到字节数组占用内存很大,我们点击右上角的堆Dump:

 

可以查找前多少个最大的对象:

 

双击还可以更加详细的查看细节:

 

5.方法区(Method Area) 

5.1 定义

jvm规范-方法区定义:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

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

方法区是在jvm启动时创建的。尽管方法区在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾回收或压缩它。此规范不要求方法区的位置或用于管理已编译代码的策略。方法区可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。

jvm实现可以为程序员或用户提供对方法区的初始大小的控制,以及在方法区大小不同的情况下,对最大和最小方法区大小的控制。

以下异常情况与方法区相关联:

·如果方法区中的内存无法用于满足分配请求,jvm将抛出OutOfMemoryError。

5.2 特点

1.是线程共享的

2.存在内存溢出的情况

5.3 组成

1.jvm1.6

2.jvm1.8 (方法区被分配到本地内存)

 5.4 方法区内存溢出

·1.8以前会导致永久代内存溢出(ps:这个就不做演示了)

java.lang.OutOfMemoryError:PermGen space

调整大小:-XX:MaxPermSize=?? 

·1.8之后会导致元空间内存溢出

package Heap;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
 * 演示元空间内存溢出  java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=10m  最大元空间大小
 * -XX:-UseCompressedOops  关闭压缩指针
 */
public class Method_5_3_1_8 extends ClassLoader{
    public static void main(String[] args) {
        int count = 0;
        try{
            Method_5_3_1_8 clz = new Method_5_3_1_8();
            for (int i = 0; i < 30000; i++,count++) {
                // 每一次for循环,做了一个类加载过程
                // ClassWriter 作用是生成类的二进制字节码 byte[] --> Class对象
                ClassWriter cw = new ClassWriter(0);
                // 参数:版本号 访问权限 类名 包名 父类 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "class" + i, null, "java/lang/Object", null);
                // 生成二进制字节码
                byte[] code = cw.toByteArray();
                // 执行类的加载
                clz.defineClass("class" + i, code, 0, code.length); // Class对象
            }
        }finally {
            System.out.println(count);
        }

    }
}

 首先:最大元空间大小是很大的,所以先进行一下限制。其次,如果你不关闭压缩指针的话它会报:压缩类空间不足,这是因为在metaspace里面有一片空间专门存放压缩类文件。(网上说默认大小是1G)压缩指针默认是开启的。

 关闭之后就可以看到元空间内存不足:

5.5 运行时常量池(区别于常量池)

5.5.1 两者区别

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

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

查看一下常量池:

package method;
public class Method_5_4_1 {
    // 二进制字节码(类基本信息、常量池、类方法定义(包含了虚拟机指令))
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

找到编译后的代码: 

 

右键包名(是包名,不是类名):

执行javap  -v命令:  

 看一下常量池:

还可以看一下main函数的底层执行过程:(具体的指令可以查看jvm指令手册)

5.6 StringTable

package method;
public class Method_5_5_1 {
    // StringTable 串池,Hashtable结构,不能扩容
    // ["a","b","ab"]
    // 常量池中的信息,都会被加载到运行时常量池中
    // 这时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";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()
        String s5 = "a" + "b"; // javac在编译期间的优化,自动拼接字符串常量
        // intern()方法会尝试将字符串字面量放入串池,如果存在,不放入,返回串池中的字面量
        // 如果没有,则将该字面量放入串池,返回串池中的字面量
        String s6 = s4.intern();//由于串池里面本身存在ab的地址,所以放不进去,返回串池的字面量,s6指向串池
        System.out.println(s3==s4);//s3的指向在串池,s4的指向是堆里面的一块空间
        System.out.println(s3==s5);
        System.out.println(s4==s6);
    }
}

运行结果:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值