JVM学习笔记(一)——内存结构

目录

一.引言

1.什么是JVM?

2.JVM、JRE、JDK的关系

 二.JVM内存结构

1.程序计数器

1.1定义

1.2特点

 2.虚拟机栈 

2.1定义

2.2特点

 3.本地方法栈 

3.1定义

4.堆

4.1定义

4.2特点

5.方法区

5.1定义

 5.2运行时常量池

 5.3StringTable

6.直接内存

6.1定义

6.2分配和回收原理


一.引言

1.什么是JVM?

        JVM全称Java Virtual Machine(Java虚拟机),其是一种用于计算机设备的规范,它是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现。其是Java程序的运行环境(Java二进制字节码的运行环境)

        JVM也是Java能够跨平台的关键工具。Java是一门抽象程度特别高的语言,提供了自动内存管理(GC)等一系列的特性。但是这些特性在操作系统上实现非常困难,所以需要在Java程序与操作系统函数之间,加入一个JVM进行转换。

如上图,JVM只需要正确执行.class文件,就可以运行在诸如Linux、Windows等平台。而Java跨平台的重要意义在于:一次编译,处处运行,这与JVM也密不可分。JVM上承开发语言,下接操作系统,它的中间接口就是字节码。

2.JVM、JRE、JDK的关系

        相信很多小伙伴都会很头疼这三者的关系,其实很简单,用如下图来阐述他们之间的关系:

  • JDK(Java Development Kit Java开发工具包):其包括JRE以及JAVA的开发工具
  • JRE(Java Runtime Environment Java运行环境):包括JVM和JAVA的核心类库
  • JVM(Java Virtual Machine Java虚拟机)

 二.JVM内存结构

        JVM的内存结构如下:其可以分为方法区、堆、虚拟机栈、程序计数器、本地方法栈。接下来将分别介绍这五个区域。

1.程序计数器

1.1定义

        程序计数器(Program Counter Register)作用是记住下一条JVM指令的执行地址。在JVM里,字节码解释器Interpreter工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OOM情况的区域(OutOfMemory Error)。

1.2特点

  • 线程私有
  • 不会存在OOM

 2.虚拟机栈 

2.1定义

        每个线程运行时,所需要的内存成为虚拟机栈(Java Virtual Machine Stacks),每个栈由多个栈帧(Frame)组成,栈帧中对应着每次方法调用时所占用的内存(每个栈帧意味进入了一个方法)。每个线程中只能有一个活动栈帧,对应着当前正在执行的方法。每个栈帧中存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 每个方法被调用直至执行完成的过程,对应一个栈帧在虚拟机栈中从入栈到出栈过程。
  • 局部变量表存放了编译器可知的各种Java虚拟机基本数据类型、对象引用(Reference类型),这些数据类型在局部变量表中的存储空间以Slot来表示。其中64位长度的long和double会占用两个Slot。

2.2特点

  •  线程私有
  •  会造成OOM现象,主要有两点:
    • 栈帧过多导致占内存溢出(典型的如递归不设置终止条件)
    • 栈帧过大导致占内存溢出(实际比较难出现)

 3.本地方法栈 

3.1定义

        本地方法栈与虚拟机栈所发挥的作用非常类似,其区别只是在于虚拟机栈作为虚拟机执行Java方法(字节码)服务,而本地方法栈则视为虚拟机使用本地(Native)方法服务。

4.堆

4.1定义

        Java中通过new关键字,创建对象都会使用堆内存。堆是JVM所管理的内存中最大的一块。其是被所有线程共享的一块内存区,在虚拟机启动时创建。在堆中因为GC机制,通常会出现新生代、老年代、永久代等明此,其实这些都是在划分堆的区域。

        Java的堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。

4.2特点

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

5.方法区

5.1定义

        Java内存结构中的方法区也是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型(Class)信息、常量、静态变量、即时编译器JIT编译后的代码缓存等数据。在JDK8以前,方法区由“永久代”实现;而到了JDK8之后,将方法区移到了“元空间”(Metaspace)中,不再位于内存区域,而是在本地内存中。如下图:

 5.2运行时常量池

        从方法区的结构中可以看出,有常量池结构。常量池就是一张表,用于存放编译器生成的各种字面量和符号引用,虚拟机指令根据这张常量池表找到要执行的类名、方法名、参数类型、字面量等信息。

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

 5.3StringTable

        StringTable又称为串池,在JDK1.8以前是存在与方法区的常量池中,而在JDK1.8后,留在了Heap中。其作用是为了避免重复创建字符串对象。看以下代码:

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();

// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);

如果不了解Stringtable的概念对上述问题其实是挺困难的,现在让我们来分析一下:

        在JVM编译期间,会提前将一些基本数据类型,包括字符串类型放入到常量池中,当这个类被加载时,就会将这些数据加载到运行时常量池。而StringTable其本质是一个hashtable结构,不能扩容。虽然字符串类型的"a", "b","ab"已经在运行时常量池了,但是只有到真正用到它们时,才会创建一个字符串类型的对象,也就是执行到String s1 = "a"。

        当执行String s1 = "a",会判断Stringtable中是否存在字符串"a",但刚开始肯定时没有的,所以会往Stringtable中加入一个字符串类型对象“a”,下面两个s2、s3同理。所以此时Stringtable中含有三个字符串类型对象["a", "b", "ab"];

        当执行到String s4 = s1 + s2;在底层实际上是创建了一个StringBuilder()。进行了这样的操作:new StringBuilder().append("a").append("b").toString(),等价为:new String("ab“)

因此s4是相当于新建了一个字符串类型的对象"ab",其值虽然和串池中的"ab"相同,但不是同一对象。因此s3中的”ab“和s4的"ab"不是同一个字符串对象,故第一问是fasle

再看第二问,String s5 = "a" + "b",这里Javc会在编译期间做一个优化,会自动在编译期间就自动做了一个拼接,所以实际上这里应该就是String s5 = "ab",而之前执行s3时已经将”ab“存入了Stringtable中,所以s5会直接引用到Stringtable中的字符串对象"ab",因此不会重复创建一个字符串类型的对象,故第二问为true;

再看第三问,首先介绍一些intern方法。在JDK8中,可以使用intern方法,主动将串池中还没有的字符串对象放入串池中。如果串池中已经存在,则不会放入,并且会返回串池中的字符串类型的对象。如果串池中不存在,则会将方法的字符串对象放入到串池中,并且返回该对象。

因此第三问中的s6=s4.intern(),会尝试将s4引用的字符串对象放入到串池中,但是串池中已经存在了"ab"了,所以不会放入成功,但同时他会返回给s6串池中的"ab"对象。而串池中的"ab"对象是执行s3 = "ab"时放进去的,所以和s3指向的是同一个对象(串池中的"ab"和s4不是同一个对象,看第一问),因此第三问的答案是true。

STringtable特性

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

6.直接内存

6.1定义

        直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频繁地使用,而且可能导致OOM异常。

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

6.2分配和回收原理

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值