初识JVM及JVM的内存结构(一)

初识JVM

首先,在讲JVM之前,我们先来了解一下 ,
电脑是二进制的系统,他只认识 01010101(机器码,由0和1组成)
那么,我们经常要编写xxx.java这种java文件,电脑是怎么识别java文件的呢?

一段java代码是如何运行的?

1、【编译】程序员编写的java文件(编译成)class文件
2、【加载】JVM通过类加载器进行加载该class文件
3、【解释】class文件(通过JVM的执行引擎翻译成)机器码
4、【执行指令】机器码(通过特定平台的操作系统)运行

说明:
1、为什么编译成class文件?因为JVM只认识.class文件(二进制字节码文件)
备注:二进制字节码文件包含三部分组成:类的基本信息、常量池和方法定义(包含了虚拟机指令)虚拟机指令在执行时,会根据符号地址,在常量池中找到类名、方法名、参数类型、字面量等信息进行执行

对class文件感兴趣可以看下这篇文章:详解.class文件

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题并且保留了解释型语言可移植的特点,而通过即时编译器(jit)又有编译型语言执行效率高的特点。所以 Java 程序运行时比较高效。Java通过字节码文件和虚拟机jvm之间的关系,做到了平台无关性,一次编译,各平台都可运行

2、class文件为什么要翻译成机器码?因为电脑/CPU不认识class文件
3、这里也可以看出java是半编译半解释型的语言

JVM是什么?

JVM(Java Virtual Machine,java虚拟机)是Java 程序的运行环境(或者说是Java二进制字节码的运行环境)

JVM好处:

  • 一次编译,处处执行(通过上面的java代码运行流程可以看出,java文件先通过一次编译成class文件(二进制字节码)再通过JVM翻译成所有的操作系统都可以执行的机器码)
  • 自动的内存管理,垃圾回收机制
    c语言没有垃圾回收导致内存泄漏(内存泄露:被占用的内存无法释放)(野指针)
  • 数组下标越界检查

JVM在java体系中的位置

在这里插入图片描述
1、jre(java运行环境)=JVM+基础类库的class文件【即Java API】(都在 lib 目录下打包成了 jar)(包括:接口、抽象类、具体类)

2、jdk(java开发环境)=jre+编译工具(增加了编译 java 源码的编译器及 java 程序调试和分析的工具)

3、javase(jdk+IDE工具(集成开发环境 如Eclipse、IntelliJ IDEA))

4、javaee(javase+服务器如tomcat)

在这里插入图片描述

JVM三大子系统之内存结构

通过上面的介绍,我们大概知道了JVM在java中的作用,好处和位置,那么,JVM的基本结构包括了哪些,他们又分别有什么作用呢?
JVM由三个主要的子系统构成:

  • 类加载子系统
  • 运行时数据区(也叫内存结构)
  • 执行引擎
    在这里插入图片描述
    那么,我们先来了解下java的内存结构
    从上面的图中我们可以看出,java的内存结构包括方法区、堆、虚拟机栈、程序计数器、本地方法栈;

1、程序计数器

程序计数器(Program Counter Register ):是一块较小的内存空间,包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器指向顺序中的下一个指令。当计算机重启或复位时,程序计数器通常恢复到零

(备注:程序计数器只是对物理寄存器的抽象模拟,相当于物理硬件上的计算机处理器中的寄存器)

程序计数器的作用:

在线程上下文切换的过程中需要记录到下一条要执行的指令的地址,等到线程再次被调度到执行的时候,还是根据该线程的程序计数器,来找到下一条要执行的指令的地址

为什么需要程序计数器?

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此, 为了线程切换后能恢复到正确的执行位置 ,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

举例:线程1切换到线程2时,线程1的程序计数器记录线程1的下一条jvm指令,然后就可以切换到线程2了,当切换回来线程1时就继续执行(记录的)下一条jvm指令。(如果没有程序计数器,线程切换回来时,就不知道执行到哪条指令了)

特点:

  • 是线程私有的(即每个线程都有自己的程序计数器)
  • 程序计数器不存在内存溢出(堆、栈、方法区都会内存溢出)

2、虚拟机栈

什么是虚拟机栈?

栈,可以看成是方法的运行模型,所有方法的调用都是通过栈帧来进行的,JVM会为每个线程都分配一个栈区,JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。当线程进入一个Java方法函数的时候,就会在当前线程的栈里压入一个栈帧,用于保存当前线程的状态(参数、局部变量、中间计算过程和其他数据),当退出函数方法时,修改栈指针就可以把栈中的内容销毁。

1、每个线程运行需要的内存空间,称为虚拟机栈,【栈中存放该线程的所有的方法】(栈是每个线程私有的,它的生命周期和线程相同)

2、栈帧Frame【每个方法运行时需要的内存】,包含:存储局部变量表、操作数栈、动态链接、方法出口等信息
(帧是栈中的单位)(多个栈帧对应该线程调用的多个方法需要的内存空间)

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

4、解析栈帧:

局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落

问题辨析:

1、一个方法调用另一个方法,会创建很多栈帧吗?
答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面【栈顶为活动栈帧】

2、栈指向堆是什么意思?
栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址指向堆;

3、垃圾回收是否涉及栈内存?
不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

4、栈内存分配越大越好吗?(-Xss 设置栈大小)
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少
(举例:【每一个线程私有一个栈】物理内存一定(500M),如果一个线程占用1M,则理论上可以同时存在500个线程同时执行,如果给每个线程的栈帧内存设置为2M,则最多有250个线程)

栈内存溢出:

附:什么是内存溢出?
内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出。

什么情况下会出现栈内存溢出?
栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError
解决方案:使用 -Xss256k 指定栈内存大小
(本人使用的IDE是IDEA)如果不知道如何设置虚拟机参数,可以看下这两篇文章
IDEA配置虚拟机参数信息
虚拟机参数配置

说明:
1、栈帧过多可能是调用的方法出现了无限递归

private static void method1( ) {
count++;
method1();
}

2、与json转换的相关问题,也可能导致栈内存溢出
在这里插入图片描述
如何解决?添加@JsonIgnore注解(即不转换该属性,通过单方面来关联)
在这里插入图片描述

3、本地方法栈

本地方法栈和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。

备注:
1、虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
2、与虚拟机栈一样,本地方法栈区域也会内存溢出,抛出StackOverflowError和OutOfMemoryError异常

举例:clone、wait、notify等方法都是native方法

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

附:什么是Native Method?

一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数
源自:【JVM系列2】运行时数据区域

4、堆Heap

堆,唯一的目的就是用于存放对象实例,每个Java应用都唯一对应一个JVM实例,每个JVM实例都唯一对应一个堆,并由堆内存被应用所有的线程共享

说明:

  • 通过new关键字创建的对象都会被放在堆内存
  • 它是 线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制(java堆是垃圾收集器管理的主要区域,因此也被称为“GC堆)
  • 从内存回收角度来看java堆可分为:新生代和老生代。
  • 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区
  • 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存
  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

堆内存溢出:

java.lang.OutofMemoryError :java heap space.

为什么有垃圾回收机制也会内存溢出?
new的对象过多,因为如果对象一直使用,则不会进行垃圾回收,也可能发生内存溢出

5、方法区(method area)

定义:《深入理解java虚拟机》

方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

方法区负责存储java代码中的哪些数据?

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
【方法区存储每个类的结构信息】:例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口,
对方法区的具体存放感兴趣的话可以看这篇文章
【面向对象-04】方法区是什么、方法区存放什么

附:方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类(加载大量第三方jar包、timcat部署的工程过多、大量动态生成反射类),导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError: PermGen space(jdk7以前)或者java.lang.OutOfMemoryError: Metaspace(jdk8以后)

说明:
1、方法区有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来(堆主要放的是对象,是GC的主要区域,而在方法区区域GC行为比较少出现)

2、方法区逻辑上是由堆组成(在堆中),但是不强制实现该规范,开发厂商不一定遵守该JVM的逻辑定义,方法区只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方(永久代在堆中,元空间使用本地内存)

3、初始化方法区域是在虚拟机启动时创建的,关闭JVM就会释放这个区域的内存

4、永久代和元空间是HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式:
jdk7以前,习惯上把方法区称为永久代。
jdk8开始,使用元空间取代了永久代
(元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存)

为什么永久代被元空间替代?

类的元数据信息这些数据被移到元空间(Metaspace)由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
(1)为永久代设置空间大小是很难确定的
在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现错误:Exception in thread ‘dubbo client x.x connector’ java.lang.OutOfMemoryError:PermGen space。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
2)对永久代进行调优是很困难的
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再用的类型,方法区的调优主要是为了降低Full GC发生的概率。《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在。一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。
原文链接:https://blog.csdn.net/qq_36756682/article/details/112526114

设置方法区的大小和OOM问题

为什么方法区会内存溢出?(或者说为什么类信息很多)
实际场景中:动态代理会加载很多的类信息(spring、mybatis的cglib技术生成代理类)

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整

jdk7及以前:
通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M

jdk8及以后:
元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的两个参数。
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,
-XX:MaxMetaspaceSize的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
-XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值

常量池、运行时常量池、字符串常量池

具体解析可以看这篇文章,详解JVM常量池、Class常量池、运行时常量池、字符串常量池

什么是常量池?(也叫Class常量池)

(常量池是方法区的一部分)Java文件被编译成Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池是当Class文件被Java虚拟机加载进来后存放在方法区各种字面量 (Literal)和 符号引用

字面量:简单地理解为等号右边的值,比如类中定义了int a = 1; string str = “hello”,那么1和hello 都是字面量。
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型

什么是运行时常量池?
java运行时不会获取常量池所有的数据,只加载有用的常量池信息到运行时常量池。
常量池是 .class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

方法区的Class文件信息,Class常量池和运行时常量池的三者关系
在这里插入图片描述

字符串常量池

方法区中的字符串常量池是什么?
(JDK8以后)字符串常量池位于堆空间中,存放的是字符串的引用,这些引用指向方法区的元空间(在系统内存中)的常量池。

  • 常量池设计就是一种缓存池,为字符串开辟一个字符串常量池,类似于缓存区,为了提高程序性能
  • 创建字符串常量时,首先查询字符串常量池是否存在该字符串
  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

注:字符串常量池和串池/StringTable/stringpool都是同一个东西

字符串常量池有什么作用?
字符串常量池是JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

jdk1.6(位于常量池中)jdk1.8后在堆中
在这里插入图片描述
附:JDK 1.7 为什么要将字符串常量池移动到堆中?

主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存

三种操作字符串:

1、直接赋值字符串 String s1 = “a”; 类型

创建的字符串对象,只会在常量池中,字符串常量池中存放的只是指向常量池的引用

"a"属于字面量,创建s1对象的时候,JVM会先去常量池中通过equals(key)方法,判断是否有相同的对象。
如果有,则直接返回该对象在常量池中的引用;
如果没有,则会在常量池中创建一个新对象,再返回引用

2、String s5 =“a”+“b”;(字符串常量常量)

因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,会直接在串池/字符串常量池中找“ab”

因此String s5 =“a”+“b”;等价于 String s5 =“ab”;(和类型1一样)

判断常量池中是否有相同的对象
如果有,则直接返回该对象在常量池中的引用;
如果没有,则会在常量池中创建一个新对象,再返回引用

3、String s4=s1+s2;(字符串变量拼接)
上面提到,字符串池存放的是字符串的引用(堆中),因此这里s1和s2其实是两个引用/地址,因此不会进行优化。

那么这里发生了什么事情呢?如果s3 =“ab”;,此时s3==s4吗? false
在这里插入图片描述
String s4=s1+s2; 本质:new StringBuilder ( ) .append(“a”). append(“b” ).toString( ) ;

tostring本质:【new String】(" ab")

因此String s4=s1+s2; s4为new出来的对象“ab”(堆内存)
而s3为串池的对象(串池虽然也是在堆内存中,但是存放的是指向常量池的引用/地址)
因此 s3==s4//false
(再次说明:串池是字符串常量池)

intern方法

intern方法(jdk1.8):将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
intern方法(jdk1.6):将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

jdk1.8

		String s1 = new String("1")+new String("1");
        String s2 = s1.intern(); //是将s1对象存入串池(s1对象是一个堆对象),上传后,s1和返回值s2都是是该字符串在串池中的引用
        String s3 ="11";//此时的s3和s2是等价的
        System.out.println(s1==s2);//true
        System.out.println(s1==s3);//true
        System.out.println(s3==s2);//true

jdk1.6

		String s1 = new String("1")+new String("1");
        String s2 = s1.intern(); //是将s1对象的复制存入串池(s1对象是一个堆对象),s1的复制上传后,只有返回值s2都是是该字符串在串池中的引用
        String s3 ="11";//此时的s3是从串池中拉取的,因此s3和s2等价
        System.out.println(s1==s2);//false
        System.out.println(s1==s3);//false
        System.out.println(s3==s2);//true

StringTable面试题:
在这里插入图片描述

最后,用一张图来介绍每个区域存储的内容
在这里插入图片描述

引用文章:
https://blog.csdn.net/A_art_xiang/article/details/118568601
https://www.zhihu.com/question/23599282/answer/2423913695
https://blog.csdn.net/qq_41701956/article/details/81664921
https://blog.csdn.net/weixin_43122090/article/details/105093777
视频:黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值