java虚拟机的内存管理

一、JVM整体架构

  • 根据JVM规范,JVM内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
    在这里插入图片描述

    名称特征作用配置参数异常
    程序计数器占用内存下,线程私有,生命周期与县城相同大致为字节码行号指示器
    虚拟机栈线程私有,生命周期与线程相同,使用连续的内存空间Java方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息-XssStackOverFlowError/OutOfMemoryError
    线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址保存对象实例,所有对象实例(包括数组)都要在堆上分配-Xms -Xsx -XmnOutOfMemoryError
    方法区线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址存储一杯虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:16M 、-XX:MaxPermSize64M 、-XX:MetaspaceSize=16M 、-XX:MaxMetaspaceSize=64MOutOfMemoryError
    本地方法栈线程私有为虚拟机使用到的Native方法服务StackOverflowError/ OutOfMemoryError
  • JVM 分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块
    在这里插入图片描述

二、JVM运行时内存

Java 虚拟机有自动内存管理机制,如果出现面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。
在这里插入图片描述
Java7和Java8内存结构的不同主要体现在方法区的实现
方法区是java虚拟机规范中定义的一种概念的区域,不同的厂商可以对虚拟机进行不同的实现。
我们通常使用的Java SE都是有Sun JDK 和 OpenJDK 所提供,这也是应用最广泛的版本。而该版本使用的就是HotSpot VM.通常情况下,我们所讲的java虚拟机值得就是HotSpot的版本。

在这里插入图片描述

针对JDK8虚拟机内存详解

在这里插入图片描述
JDK7和JDK8变化小结
在这里插入图片描述

线程私有的:
①程序计数器
②虚拟机栈
③本地方法栈

线程共享的:
①堆
②方法区 直接内存(非运行时数据区的一部分)

  • 对于Java8,HotSpots 取消了永久代,那么是不是就没有方法区了呢?
    当然不是,方法区只是一个规范,只不过它的实现变了。
    在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不在与堆连续,而且是存在于本地内存(Native memory).

  • 方法区Java8之后的变化

    1、 移除了永久代(PermGen),替换为元空间(Metaspace)
    2、永久代中的class metadata (类元信息) 转移到了 native memory (本地内存,而不是虚拟机)
    3、永久代中的interned Strings(字符串常量池) 和 class static variables (类静态变量)转移到了 Java heap
    4、永久代参数(PermSize MaxPermsize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

  • Java8 为什么将永久代替换成Metaspace?

    1、字符串存在永久代中,容易出现性能问题和内存溢出
    2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
    3、永久代会为GC带来不必要的复杂度,并且回收效率低
    4、Oracle可能会将HotSpot与JRockit合二为一。JRockit没有所谓的永久代

2.1 PC 程序计数器
  • 概念
    程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能 都需要依赖这个计数器来完成。

  • 特点
    1、区别于计算机硬件的pc寄存器,两者略有不同。计算机用pc寄存器来存放"伪指令"或地址。而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切地说存放的是将执行指令的地址。
    2、当虚拟机正在执行的方法时一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
    3、程序计数器是线程私有的,它的生命周期与线程相同,每一个线程都有一个。
    4、此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

在这里插入图片描述

Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称为这类内存区域为"线程私有"的内存。

2.2 虚拟机栈
  1. 什么是虚拟机栈
    Java虚拟机栈(Java Virtual Machines Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量、操作数栈、动态链接、用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

    public class StackDemo {
    
        public static void main(String[] args) {
            StackDemo sd = new StackDemo();
            sd.A();
        }
    
        public void A(){ int a = 10; System.out.println(" method A start");
            System.out.println(a);
            B();
            System.out.println("method A end");
        }
    
        public void B(){
            int b = 20;
            System.out.println(" method B start");
            C();
            System.out.println("method B end");
        }
    
        private void C() {
            int c = 30;
            System.out.println(" method C start");
            System.out.println("method C end");
        }
    }
    

    在这里插入图片描述

  2. 什么是栈帧
    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
    在这里插入图片描述
    在这里插入图片描述

  3. .设置虚拟机栈的大小
    -Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

Linux/x64 (64-bit): 1024 KB
macOS (64-bit): 1024 KB
Oracle Solaris/x64 (64-bit): 1024 KB
Windows: The default value depends on virtual memory

-Xss1m
-Xss1024k
-Xss1048576

public class StackTest {
    static long count = 0 ;
    public static void main(String[] args) {
        count++;
        // 默认:8467   -Xss256k:1847
        System.out.println(count);
        main(args);
    }
}

在这里插入图片描述

  1. 局部变量表
    局部变量表(Local Variable Table) 是一组变量存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
    其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用一个。

  2. 操作数栈
    操作数栈(Operand Stack)也称作操作栈,是一个后人先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或者对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

    public class StackDemo2 { 
    	public static void main(String[] args) { 
    	int i = 1; 
    	int j = 2; 
    	int z = i + j; 
    	} 
    }
    
  3. 动态链接
    Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking).
    动态链接的作用:将符号引用转换成直接引用

  4. 方法返回地址
    方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

局部变量表
在这里插入图片描述

操作数栈
在这里插入图片描述

2.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈锁发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务

  • 特点
    1、本地方法栈加载native的方法,native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的
    2、虚拟机为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务
    3、是线程私有的,它的生命周期与线程相同,每个线程都有一个。

在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:
1、StackOverFlowError:线程请求的栈深度>所允许的深度
2、OutOfMemoryError: 本地方法栈扩展是无法申请到足够的内存

2.4 堆
2.4.1 Java堆概念
  1. 简介
    对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里"几乎"所有的对象实例都在这里分配内存。"几乎"是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
    在这里插入图片描述

  2. 堆的特点
    1、是Java虚拟机所管理的内存中最大的一块
    2、堆是jvm所有线程共享的堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
    3、在虚拟机启动的时候创建
    4、唯一的目的就是存放对象实例,几乎所有的对象实例以及数组毒药在这里分配内存。
    5、Java堆是垃圾收集管理的主要区域
    6、因此很多时候java堆也被称为"GC堆"(Garbage Coolected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden空间,From Survivor空间,To Survivor 空间。
    7、java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)
    8、方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候才移除
    9、如果在堆中没有内存完成实例的分配,并且堆也无法在扩展是,将会抛出OutOfMemoryError异常。

  3. 设置堆空间大小
    内存大小-Xmx/-Xms
    使用示例: -Xmx20m -Xms5m
    说明: 当下Java应用最大可用内存为20M, 最小内存为5M

public class TestVm { 
	public static void main(String[] args) { 
	//补充 
	//byte[] b=new byte[5*1024*1024]; 
	//System.out.println("分配了1M空间给数组"); 
	
	System.out.print("Xmx=");
	System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); 
	System.out.print("free mem="); 
	System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); 
	System.out.print("total mem="); 
	System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
	} 
}

执行结果

Xmx=20.0M
free mem=4.1877593994140625M
total mem=6.0M

  1. .堆的分类

在Java7 Hotspot虚拟机中将Java堆内存分为3个部分:
青年代Young Generation
老年代Old Generation
永久代Permanent Generation
在这里插入图片描述
在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了,在几天前(2018年9约25日)Java11正式发布以后,官网上Java11中垃圾收集器的官方文档,文档中没有提到“永久代”,而只有青年代和老年代。

在这里插入图片描述

2.4.2 年轻代和老年代
  1. 1.JVM中存储java对象可以被分为两类:
    1、年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
    2、年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
    在这里插入图片描述

  2. 配置新生代和老年代堆结构占比

默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5 Eden空间和另外两个Survivor空间占比分别为8:1:1
可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8
几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁.

在这里插入图片描述
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

2.4.3 对象分配过程

JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收是否存在空间中间产生内存碎片。
分配过程:

1、new的对象先放在伊甸区。该区域有大小限制
2、当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额外对象进行销毁,再加载新的对象放到伊甸园区。
3、然后将伊甸园区中的剩余对象移动到幸存者0区
4、如果再次经历垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区
5、如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区
6、如果累计次数达到默认的15次,这会进入养老区。可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
7、养老区内存不足时,会再次触发GC:Major GC 进行养老区的内存清理
8、如果养老区执行力Major GC后仍然没有办法进行对象的保存,就会报OOM异常。
在这里插入图片描述
在这里插入图片描述
分配对象的流程:
在这里插入图片描述

2.4.4 堆GC

Java中的堆也是GC收集垃圾的主要区域。GC分为两种:一种是部分收集器(Partial GC),另一类是整堆收集器(Full GC)
部分收集器:不是完整收集java 堆的收集器,它有分为:

新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
老年代收集(Major GC/ Old GC) : 只是老年代的垃圾收集(CMS GC 单独回收老年代)
混合收集(Mixed GC) : 收集整个新生代及老年代的垃圾收集(G1 GC会混合回收,region区域回收)

整堆收集(Full GC): 收集这个java堆和方法区的垃圾收集器

年轻代GC触发条件:

年轻代空间不足,就会触发Minor GC,这里年轻代指的是Eden代满,Survivor不满不会引发GC
Minor GC 会引发STW(stop the world),暂停其他用户的线程,等垃圾回收接收,用户线程才恢复

老年代GC (Major GC) 触发机制

老年代空间不足时,会尝试触发MinorGC,如果空间还是不足,则触发Major GC
如果Major GC,内存仍然不足,则报错OOM
Major GC的速度比Minor GC 慢10倍以上

Full GC 触发机制:

调用System.gc() ,系统会执行Full GC ,不是立即执行
老年代空间不足
方法区空间不足
通过Minor GC 进入老年代平均大小大于老年代可用内存

2.5 元空间

在JDK1.7 之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从JDK1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取消永久代的是元空间(Metespace)而已。

存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
存储内容不同:在原来的永久代划分中,永久代用来存放类的原数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
在这里插入图片描述

2.5.1 为什么要废弃永久代,引入元空间

相比于之前的永久代划分,Oracle为什么要做这样的改进呢?

1、在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize指定太小很容易造成永久代内存溢出。
2、移除永久代是为融合HotSpot VM 与 JRockit VM 而做出的努力,因为JRockit 没有永久代,不需要配置永久代
3、永久代会为GC带来不必要的复杂性,并且回收效率偏低

2.5.2 废除永久代的好处

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在是的内存溢出错误
将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性
将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率

2.5.3 Metaspace相关参数

-XX:MetaspaceSize,初始空间大小,达到改制就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize ,最大空间,默认没有限制的。如果没有使用该参数来设置类的元数据大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足元数据信息的存储。但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进二可能出现swap内存被耗尽,最终导致进程直接被系统kill掉。
-XX:MiniMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间导致的垃圾收集。

2.6 方法区
2.6.1 方法区的理解

方法区(Method Area) 与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在这里插入图片描述

元空间、永久代是方法区具体的落地实现。方法区看作一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。

在这里插入图片描述

创建对象各数据区域的声明:
在这里插入图片描述
方法区的特点:

方法区与堆一样是各个线程共享的内存区域
方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
方法区的大小跟堆空间一样,可以选择固定大小或者动态变化
方法区的对象决定了系统可以保持对少个类,如果系统定义了太多的类导致方法区溢出虚拟机同样会抛出(OOM)异常(Java7之前是PermGen Space (永久区)) ,Java8之后是MetaSpace (元空间)
关闭JVM就会释放这个区域的内存

2.6.2 方法区结构
  • 方法区的内部结构
    在这里插入图片描述
    类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
    方法区中存储的内容:

    类型信息(域信息、方法信息)
    运行时常量池

  • 类型信息
    对每个加载的类型(类Class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

    1、这个类型的完整有效名称(全名=包名.类名)
    2、这个类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
    3、这个类型的修饰符 ( public ,abstract ,final 的某个子类 )
    4、这个类型直接接口的一个有序列表

  • 域信息
    域信息,即为类的属性,成员变量
    JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序
    域的相关信息包括:域名称、域类型、域修饰符(public 、private、protected、static、final、volatile、transient的某个子集)

  • 方法信息
    JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序

2.7 运行时常量池
2.8 直接内存

三、实战OutOfMemoryError异常

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值