深入理解Jvm(一)--内存理解

一、Java内存区域理解

Java与C++之间有一堵由内存动态分配(加载)和垃圾收集技术所围成的高墙,墙外面的1人想进去,墙内边的人想出去。

1.Jvm内存:

  • Jvm在执行java程序的过程中会把Jvm所管理的内存划分为若干个不同数据区域。 方法区 Method Area虚拟机栈VM Stack本地方法栈Native Method Stack堆Heap程序计数器Program Counter Register

    深入理解Jvm(一)--内存理解_第1张图片

深入理解Jvm(一)--内存理解_第2张图片

 

 

  • 注:方法区、堆运行时所有数据是线程共享的栈、本地栈、程序计数器运行时所有数据是线程独享的

深入理解Jvm(一)--内存理解_第3张图片

  • Person:存放在元空间,也可以说方法区;
  • person:存放在Java栈的局部变量表中;
  • new Person():存放在Java堆中。

2.方法区:1.7永久代(堆)、1.8元空间(本地内存) https://www.jianshu.com/p/87354570f362

        方法区的定义:方法区是Jvm规范中定义的规范,主要用于存储已被虚拟机加载的【Class】类型信息、常量、静态变量、即时编译器编译后的代码缓存等。其大小可通过参数调节。

深入理解Jvm(一)--内存理解_第4张图片

        方法区的垃圾回收:方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量不再使用的类型       

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

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

        域信息:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序:

                1)域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)。

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

                1)方法名称
                2)方法的返回类型(或void)
                3)方法参数的数量和类型(按顺序)
                4)方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
                5)方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
                6)异常表(abstract和native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

        静态类变量:静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。

                1)静态类变量被类的所有实例共享,即使没有类实例时,你也可以访问它。

深入理解Jvm(一)--内存理解_第5张图片


        方法区的实现:JDK1.7及以前:Hotspot虚拟机对方法区的实现是永久代,方法区和永久代的关系就类比如Java中的接口和实现类。永久代就是HotSpot虚拟机对Jvm虚拟机规范中方法区的一种实现方式。此时的方法区是堆的逻辑组成部分。《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于Java堆的内存空间。

深入理解Jvm(一)--内存理解_第6张图片

        JDK1.7方法区(永久代)的特点:

                1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
                2)方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
                3)方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
                4)方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace。
                5)关闭JVM就会释放这个区域的内存。

        方法区的实现:JDK1.8:Hotspot虚拟机对方法区的实现是元空间,以前的方法区实现永久代是在Jvm分配的内存中,且设置好的固定大小,所以溢出的可能性比较大。JDK1.8方法区的实现是元空间,不在虚拟机内存中,而是使用本地内存。降低了内存溢出。-XX:MaxMetaspaceSize标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小

        永久代和元空间最大的区别:永久代是在Jvm分配的内存中,元空间不在Jvm分配的内存中,而是使用的本地内存。方法区的大小不必设置,JVM可以根据应用的需要动态调整。

深入理解Jvm(一)--内存理解_第7张图片

        JDK1.8方法区(元空间)的特点:

                1)方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
                2)方法区的大小可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定,默认是本地内存,JVM可以根据引用动态调整。
                3)如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace。
                4)方法区不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。
                5)关闭JVM就会释放这个区域的内存。

-XX:MetaspaceSize
    class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,
    同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,
    那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
-XX:MaxMetaspaceSize
    可以为class metadata分配的最大空间。默认是没有限制的。
-XX:MinMetaspaceFreeRatio
    在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
-XX:MaxMetaspaceFreeRatio
    在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。

3.程序计数器:Program Counter Register

  1. 字节码行号指示器:程序计数器是一块较小的内存空间,可以看成是当前线程所执行的字节码的行号指示器
  2. 字节码解释器:是通过改变这个计数器的值来选取下一条需要执行的字节码指令。计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程等基础功能都需要依赖计数器来完成。
  3. 多线程切换控制:Jvm的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的。所以程序计数器会为多个线程分别分配独立的程序计数器,各条线程之间计数器互不影响,独立存储当前线程的字节码行号。
  4. 线程私有:程序计数器是线程私有的。
  5. 字节码指令地址:如果线程在执行一个java方法,那么程序计数器就记录的是当前线程所执行的字节码的行号地址,如果正在执行的是一个native方法,那此时的计数器值就为Undefined。

4.Jvm栈:Jvm stack

  1. 栈内存:是线程私有,栈内存的生命周期和线程相同。
  2. 栈帧:在这个栈内存中,Java中每个方法要运行Jvm就会创建一个栈帧,用于存储方法中的局部变量、方法出口等信息。方法开始运行到结束,对应的就是栈帧的入栈和出栈。
  3. 局部变量表:是栈帧内存中用于存储编译期可知的各种基本数据类型、引用数据类型(存储指针)的一块空间。存储的方式是以局部变量槽Slot来表示,其中64位长度的long和double类型占用两个槽Slot。其他各占用一个局部变量槽。
  4. 局部变量空间的分配:局部变量表所需要的空间在编译期间就完成了空间的计算分配,当Jvm执行一个方法时,Jvm会为这个方法分配栈帧内存,此时这个栈帧内存中需要多大的局部变量空间,就是确定的,运行时这个空间也不会改变。
  5. 局部变量表的存储单位:局部变量槽Slot,一个槽占用32bit还是64bit,这个取决于Jvm的配置。

5.本地方法栈:Native Method Stack 

  1. 本地栈和Jvm栈的区别:Jvm栈是Jvm在运行Java代码是为Java代码分配的内存空间。本地栈是Jvm在运行native方法时分配的内存空间。

6.堆:Heap :

  1. Heap大小占比:Jvm内存中最大的一块区域。
  2. 堆内存线程共享:堆内存中的数据是可以被所有线程所共享的
  3. 创建时机和作用:在Jvm启动时创建堆内存,此区域唯一的作用就是存放对象实例,包括数组。
  4. GC管理区域:堆是Jvm垃圾回收机制管理的区域。现在的大部分垃圾回收机制都是基于分代收集理论设计,这些分代区域的划分(新生代、老年代、元空间、幸存0、幸存)仅仅是一部分垃圾回收器的共同特性或者说是设计风格而不是某个Java虚拟机具体实现固有内存分布,更不是Jvm规范中对堆内存的划分。比如:最新Hotspot虚拟机已经出现了不采用分代设计的垃圾回收器了。
  5. 堆空间的再次细分:堆内存中都存储的是对象的实例,将堆进行细分只是为了更好地回收内存、或者更快的分配内存
  6. 堆大小的配置:Jvm中堆的大小可以配置成固定大小,也可以配置成可扩展大小。通过-Xmx和-Xms设定如果堆内存没有可分配的内存,也没有可扩展的内存,就会出现OutOfMemeryError异常。

 7.OOM:OutOfMemoryError异常:(http://www.360doc.com/content/20/0707/21/58006001_922857147.shtml)

  1. Jvm规范的OOM:如果已经占用的内存+程序需要申请的内存超出了Jvm分配的最大内存,就会抛出OOM错误。
  2. 常见的OOM异常

1、java.lang.StackOverflowError栈内存异常:栈内存具有一定的深度,栈内存的大小也可以指定。

  • 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况

  • 方法内声明了海量的局部变量,方法中的变量存储在栈帧中的局部变量表中。

  • 执行了大量方法,导致线程栈空间耗尽

2、java.lang.OutOfMemoryError: Java heap space堆内存:存储对象实例

  • 请求创建一个超大对象,通常是一个大数组,堆内存大小是有限制的。

  • 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收程序结束Connection类的连接没有关闭,无法被GC

  • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。

  • 过度使用终结器(Finalizer),该对象没有立即被 GC。

3、java.lang.OutOfMemoryError:GC overhead limit exceeded :重复GC无效上线

  • Jvm默认配置中如果进程花费98%以上的时间执行GC,但是回收的内存不到%2,且该动作连续重复5次,就会抛出重复GC上线(俗称垃圾回收上头)。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。假如不抛出 GC overhead limit exceeded 错误,那GC清理的内存很少,很快就会被再次填满,迫使 GC 再次执行,这样恶性循环,CPU 使用率 100%,而 GC 没什么效果。

4、java.lang.OutOfMemoryError:Direct buffer memory :直接内存OOM

  • 我们使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。
  • Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
  • 如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出Direct Memory

5、java.lang.OutOfMemoryError:unable to create new native thread 线程数上线

  • 每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 Thread线程时,如果没有足够的资源分配就会报此类错误。

6、java.lang.OutOfMemoryError:MetaSpace 元空间OOM,实质是本地内存溢出Direct Memory

  • JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。
  • Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常

8.如何解决OOM或者堆空间异常:

        诊断工具:要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析.

        区分问题:是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow);

        内存泄漏问题:就是有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题。如果是内存泄漏,通过工具查看泄漏对象到GC Roots的引用链,于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。

        内存溢出问题:如果内存溢出问题,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

9.常量、静态变量、变量存储的内存区域:

  • 常量、静态变量:都存储在方法区的常量池。存放在java堆中。因为它不是静态的变量,不会独立于类的实例而存在,而该类实例化之后,放在堆中,当然也包含了它的属性i。
  • 对象中的成员属性:无论是基本类型还是引用类型都存储在堆内存中,不会独立于类的实例而存在,而是该类实例化之后就放在堆中。
  • 局部变量:方法中的变量,基本类型存储在栈帧中,引用类型,引用的变量储存在栈内存中,引用的对象存储在堆内存中。方法中的变量随着入栈创建,随着出栈销毁。

10.直接内存:Direct Memory

  • 直接内存并不是Jmv运行时数据区的一部分,也不是Jvm规范中定义的内存区域。但是直接内存频繁使用,也可能出现OOM异常。(见7.4)

11.常量池:

11.常量池:

        Java中的常量池分为三种类型:https://blog.csdn.net/weixin_42030357/article/details/104015150   https://blog.csdn.net/luanlouis/article/details/39960815  

                1)类文件中常量池(The Constant Pool)也叫 Class 常量池,也叫常量池。         编译时存在于Class文件中
                2)运行时常量池(The Run-Time Constant Pool)                                                  运行时
                3)String常量池                                                                                                        运行时

深入理解Jvm(一)--内存理解_第8张图片

        类文件常量池:也叫 Class常量池(常量池==Class常量池)  存在于Class文件中 javap -v xxx.class

深入理解Jvm(一)--内存理解_第9张图片

 

 

                1)常量池的结构:
                        魔数:一串固定的值,用来进行Class文件的身份认证。不使用扩展名称是更安全。
                        版本号:是Class文件的版本号,用来标识JDK的版本。
                        常量池:简单理解就是Class文件的资源库。
                        访问标识:是否是Public、是否是Final、是否是接口、枚举、注解、等等。
                        ......还有更多
                2)一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外,还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用 常量池包含数量值、字符串值、类引用、字段引用、方法引用。
                3)为什么需要常量池:

                        一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会加载到运行时常量池。

public class SimpleClass {
    public void sayHello() {
        System.out.println("hello");
    }
}

//虽然上述代码只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。
private String name;
    public static void main(String[] args) {
       Object o = new Object();
    }

    public void setName(String name) {
        this.name = name;
    }

//将会被翻译成如下字节码:
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1     o   Ljava/lang/Object;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #3                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 11: 0
        line 12: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/aop/demo/annotation/JClassDemo;
            0       6     1  name   Ljava/lang/String;

                 4)总结:常量池是.java文件被编译后产生于.class文件中。可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。当Class文件被Jvm虚拟机加载进内存后,Class类信息存储于元空间,常量池也存储在元空间,每个对象拥有一份。常量池是当Class文件被Java虚拟机加载进来后存放各种字面量 (Literal)和符号引用。

深入理解Jvm(一)--内存理解_第10张图片

运行时常量池存在于内存的元空间中

                1)所处区域:内存的元空间
                2)诞生时间:JVM运行时
                3)内容概要:常量池(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区运行时常量池中。
                4)JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
                5)运行时常量池是当Class文件被加载到内存后,Java虚拟机会将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

字符串常量池存在与堆内存中  https://www.cnblogs.com/liangyueyuan/p/9796992.html

                1)所处区域:堆内存
                2)诞生时间:JVM运行时
                3)JDK1.7将字符串常量从永久代移动到了堆内存中是因为:。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
               4)JDK1.7分别将字符串常量池和静态成员变量迁移到了堆内存中。

        字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

常量赋值

        代码:String  s1 = "abc";在字符串常量池中创建,全局唯一共享。

深入理解Jvm(一)--内存理解_第11张图片

        代码:String s1 = new String("abc");

深入理解Jvm(一)--内存理解_第12张图片

        首先先来考虑一下这一句的执行过程,这句一共生成了两个对象,分别是"abc" 和  new String("abc"),在new String("abc")之前字符串常量池中就已经驻留了"abc"。如果字符串常量池中没有"abc",那么就在常量池创建,如果已经存在就不需要重复创建。然后在堆内存中创建String("abc")对象,这个对象指向常量池的所对应的字符串。返回堆内存中的地址引用。驻留的字符串是放在全局共享的字符串常量池中的。

        代码:String s1 = new String("a") + new String("b");

深入理解Jvm(一)--内存理解_第13张图片

        这句话一共生成了5个对象,首先会先在池子生成“a”和“b”,然后会在堆里生成对象new String("a") 和 new String("b),这两个对象分别指向常量池的所对应的字符串,接着由于“+”的作用下,创建了新的对象,这个对象通过利用之前的对象所指向的字符进行拼接生成“ab”,即这波操作是在堆里实现的,不会将生成的"ab"放在常量池里,此时之前的两个对象已经没有作用了,需要等待垃圾回收。

        总结:

                不管是new String("XXX")和直接常量赋值, 都会在字符串常量池创建.只是new String("XXX")方式会在堆中创建一个中转站去指向常量池的对象, 而常量赋值的变量直接赋值给变量

  当使用了变量字符串的拼接(+, sb.append)都只会在堆区创建该字符串对象, 并不会在常量池创建新生成的字符串。

                String intern() 方法:当调用intern方法时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。


12.静态变量存储:

        静态引用对应的对象实体始终都存在堆空间。JDK1.7之后1.8从方法区迁移到了堆内存中。


声明:本文是本人学习,阅读深入理解JVM第三版,和查看部分优秀博主文章总结。https://www.jianshu.com/p/87354570f362  https://blog.csdn.net/luanlouis/article/details/39960815

你可能感兴趣的:(java基础,java,内存管理)