JVM内存管理(一)

2024-05-09 23:32
文章标签 jvm 内存 管理 java

本文主要是介绍JVM内存管理(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

JVM与操作系统的关系

1.JVM 基础
1.1 Java Virtual Machine是什么

​ JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

1.2 JVM的作用是什么

​ JVM是一个抽象层,有了JVM,java代码就可以跨平台运行。JVM的作用主要是将字节码"翻译"成操作系统可以执行的指令代码。

JVM的作用.png

1.3 JVM、JRE、JDK的关系

JVM只是一个翻译,把Class翻译成机器识别的代码,但是需要注意,JVM 不会自己生成代码,需要大家编写代码,同时需要很多依赖类库,这个时候就需要用到JRE。

JRE是Java 运行环境(Java Runtime Environment),它除了包含JVM之外,提供了很多的类库(就是我们说的jar包,它可以提供一些即插即用的功能,比如读取或者操作文件,连接网络,使用I/O等等之类的)这些东西就是JRE提供的基础类库。JVM 标准加上实现的一大堆基础类库,就组成了 Java 的运行时环境。

​ 但对于程序员来说,JRE还不够。我写完要编译代码,还需要调试代码,还需要打包代码、有时候还需要反编译代码。所以我们会使用JDK,因为JDK还提供了一些非常好用的小工具,比如 javac(编译代码)、java、jar (打包代码)、javap(反编译<反汇编>)等。这个就是JDK

2. JVM的结构
2.1 整体结构

​ 一个 Java 程序,首先经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。

​ 代码被处理的过程如下:Java 文件->编译器->字节码->JVM->机器码。

程序执行过程.png

2.2 JVM运行时数据区域

​ Java 引以为豪的就是它的自动内存管理机制。相比于 C++的手动内存管理、复杂难以理解的指针等,Java 程序写起来就方便的多。

​ 在 Java 中,JVM 内存主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈

运行时数据区组成.png

2.2.1 程序计数器

​ 较小的内存空间,当前线程执行的字节码的行号指示器;各线程之间独立存储,互不影响。

​ 程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如:分支、循环、跳转、异常、线程恢复等都依赖于计数器。

​ 由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。

​ 程序计数器也是JVM中唯一不会OOM(OutOfMemory)的内存区域

2.2.2 虚拟机栈

​ 虚拟机栈在JVM运行过程中存储当前线程运行方法所需的数据,指令、返回地址**。**

​ Java 虚拟机栈是基于线程的。哪怕只有一个 main() 方法,也是以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。

​ 栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。

​ 每个栈帧,都包含四个区域:(局部变量表、操作数栈、动态链接、返回地址)

  • 局部变量表:用于存放我们的局部变量的。首先它是一个32位的长度,主要存放我们的Java的八大基础数据类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可,实体对象在堆中存放。
  • 操作数据栈:存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法就是JVM一直运行入栈/出栈的操作。
  • 动态链接: Java语言特性多态(需要类运行时才能确定具体的方法)。
  • 返回地址: 正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

​ 栈的大小调节:缺省为1M,可用参数 –Xss调整大小,例如-Xss256k

2.2.3 本地方法栈

​ 本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的。

​ 本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。

​ 虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一 。

2.2.4 方法区(永久代) (线程共享区)

​ 方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、静态变量、常量、运行时常量池、字符串常量池。

​ JVM 在执行某个类的时候,必须先加载。在加载类(加载、验证、准备、解析、初始化)的时候,JVM 会先加载 class 文件,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。

​ 字面量包括字符串(String a=“b”)、基本类型的常量(final 修饰的变量),符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。

​ 而当类加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)。

元空间大小参数:

  • jdk1.7及以前(初始和最大值):-XX:PermSize;-XX:MaxPermSize;
  • jdk1.8以后(初始和最大值):-XX:MetaspaceSize; -XX:MaxMetaspaceSize
  • jdk1.8以后大小就只受本机总内存的限制(如果不设置参数的话)

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

​ 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。

​ 永久代内存经常不够用或发生内存溢出,抛出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为 8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

2.2.5 堆区(线程共享区)

​ 堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。
​ 堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。
​ 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。
​ 那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。
​ Java 的对象可以分为基本数据类型和普通对象。
​ 对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。
​ 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配

堆大小参数:

  • -Xms:堆的最小值;

  • -Xmx:堆的最大值;

  • -Xmn:新生代的大小;

  • -XX:NewSize;新生代最小值;

  • -XX:MaxNewSize:新生代最大值;

    例如- Xmx256m

2.3 直接内存

​ 不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;

​ 这块内存不受java堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常。

3.深入理解运行时数据区
3.1 测试代码
public class Test {public int calculate(){int i = 1;int j = 2;int k = (i + j) * 10;return k;}public static void main(String[] args) {Test test = new Test();test.calculate();}
}

​ 使用javac工具编译Test.java类生成Test.class,并且使用javap工具反编译Test.class字节码可以看到calculate()方法字节码如下图:

calculate字节码.png

​ 从图中可以看到calculate()方法被反编结果包含两个部分: 一个是“行号”,用于程序计数器指引下一行机器需要执行的指令;另一个是字节码指令,用于虚拟机翻译成机器码执行的指令代码。

​ 因此,java代码执行是基于字节码指令;而C代码执行是基于寄存器。虽然基于寄存器的代码执行效率会高一点,但是可移植性会很差,不同平台不容易移植;而基于字节码指令的java代码,基于不同平台,只要虚拟机支持,java代码可以无缝移植。

3.2 运行时数据区执行过程

运行时数据区.png

对应的执行过程说明如下

0: iconst_1 -> 将int型 1 入操作数栈
1: istore_1 -> 将操作数栈栈顶int型值存入局部变量表(下标为1的位置)
2: iconst_2 -> 将int型 2 入操作数栈
3: istore_2 -> 将操作数栈栈顶int型值存入局部变量表(下标为2的位置)
4: iload_1 -> 将局部变量表中下标为1位置的值压入操作数栈
5: iload_2 -> 将局部变量表中下标为2位置的值压入操作数栈
6: iadd -> (1)将栈顶两个元素出栈 (2)执行相加 (3)将结果压入操作数栈
7: bipush 10 -> 将int型 10压入操作数栈
9: imul -> (1)将栈顶两个元素出栈 (2)执行相乘 (3)将结果压入操作数栈
10: istore_3 -> 将操作数栈顶int型数值压入局部变量表(下标为3的位置)
11: iload_3 -> 将局部变量表中下标为3位置的值压入操作数栈
12: ireturn -> 返回调用calculate方法的下一行地址继续执行

参考资料

JVM参数参考说明

这篇关于JVM内存管理(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/974795

相关文章

java设计模式之工厂模式--普通工厂方法模式(Factory Method)

1.普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。 2.先定义一个接口: package com.zhong.pattern.factorymethod;/*** 发送接口* @author admin**/public interface Sender {/*** 发送消息方法* @param msg*/void send(String msg);} 3

Java设计模式之代理模式2-动态代理(jdk实现)

这篇是接着上一篇继续介绍java设计模式之代理模式。下面讲解的是jdk实现动态代理。 1.)首先我们要声明一个动态代理类,实现InvocationHandler接口 package com.zhong.pattern.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;/*** 演

java设计模式之代理模式1--静态代理

Java编程的目标是实现现实不能完成的,优化现实能够完成的,是一种虚拟技术。生活中的方方面面都可以虚拟到代码中。代理模式所讲的就是现实生活中的这么一个概念:助手。 代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。 1.)首先新建一个表演的接口 package com.zhong.pattern.proxy;/*** 表演接口* @author admin*

java原型(Prototype)设计模式

原型模式就是讲一个对象作为原型,使用clone()方法来创建新的实例。 public class Prototype implements Cloneable{private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}@Overri

Java中23种设计模式之适配者模式

适配器模式的作用就是在原来的类上提供新功能。 主要可分为3种: 1.类适配:创建新类,继承源类,并实现新接口,例如:     class Adapter extends OldClass implements NewFunc{} 2.对象适配:创建新类持源类的实例,并实现新接口,例如:     class Adapter implements NewFunc { priva

java不依赖临时变量交换两个变量的值

java不依赖临时变量交换两个变量的值 1.简单易懂的实现方式     int a=1,b=2;     int temp = 0;     temp = a;     a = b;     b= temp; 2.算术算法 int a=1,b=2; a = a+b;// a = 1+2  b = a-b;// b = a-b --> b=3-2 -->1 a = a -b;/

Java中的SOLID原则及示例

类是任何Java应用程序的构建块。如果这些区块不强,那么建筑(即应用)将来将面临艰难时期。这实际上意味着,当应用程序范围上升或应用程序在生产或维护中面临某些设计问题时,不那么好的编写会导致非常困难的情况。 另一方面,一组精心设计和编写的类可以加速编码过程的突飞猛进,同时减少错误的数量。 在本教程中,我们将使用 5个最推荐的设计原则的示例来讨论Java中的SOLID原则,在编写类时我们应该记住这

Java比较和交换示例 - CAS算法

Java比较和交换示例 - CAS算法 由Lokesh Gupta | 提起下:多线程 一个Java 5中最好添加的是支持类,如原子操作AtomicInteger,AtomicLong等等。这些课程帮助您最大限度地减少复杂的(非必要)需要多线程的,如增加一些基本的操作代码或递减的值在多个线程之间共享。这些类内部依赖于名为CAS(比较和交换)的算法。在本文中,我将详细讨论这个概念。 1.乐观和

java并发编程之CyclicBarrier(循环栅栏)

package com.zhong;import java.util.concurrent.CyclicBarrier;/*** Cyclic意思是循环,Barrier意思是屏障,那么CyclicBarrier翻译过来就是循环栅栏。* 它是一个同步辅助类,能让一组线程互相等待,* 直到这一组线程都到了一个公共屏障点,各线程才能继续向下执行。因为该屏障能够在释放等待线程后继续重用,所以叫循环屏障。*

Java内存管理 - 垃圾收集算法

我们都知道Java 中垃圾收集器 [GC] 的功能。但只有少数人试图深入了解垃圾收集的工作原理。你不是其中之一,这就是你在这里的原因。 在这个Java内存管理教程中,我们将尝试了解Java垃圾收集的当前算法,我们将了解这些算法的演变。 目录1. Java中的内存管理2.引用计数机制3.标记和清除机制4.停止并复制GC 5.分代停止和复制6.如何提高Java中的内存利用率 1.