【JAVA】堆、栈与方法区

一、Java中的内存管理:

1、程序,无论是代码还是数据,都需要存储在内存中,JVM为Java程序提供并管理所需要的内存空间。
2、JVM内存分为堆(heap)、栈(stock)、方法区(method)三个区域,分别用于储存不同的数据。
3、HotSpot是Sun JDK和Open JDK中所带的虚拟机(Sun JDK和Open JDK除了注释,代码实现基本上是相同的)。

下面我们来分别了解一下他们都分别存储了哪些数据:


二、堆(heap)

1、 JVM只有一个堆区,在虚拟机启动时创建,被所有线程共享,堆区不放基本类型(成员变量除外)和对象的引用,只存储对象本身(包括class对象和异常对象)和数组,堆是GC所管理的主要区域(对不需要的对象进行标记,而后进行清除)。

2、Java中堆内存划分: (下面是JDK1.8之前的空间组成)

a、在整个JVM的堆内存中实际上将内存分为了三部分:

  • 新生代:新生代几乎是所有Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质,新生代是 GC 收集垃圾的频繁区域。新生代分为三个区:Eden 和 两个存活区(From 和 To),默认分别占内存的80%、10%、10% ( 可以通过参数–XX:SurvivorRatio 来设定 )。

注: JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

  • 老年代:存储被长时间使用的对象, 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定)
  • 元空间(JDK1.8之前叫永久代):存储一些方法中操作的临时对象等,JKD1.8之前是占用JVM内存,JDK1.8之后直接占用物理内存。

b、在JDK1.8之后,将最初的永久代内存空间取消了,该图为JDK1.8之前的空间组成。
c、取消永久代的目的是将 HotSpot 于 JRockit 的两个虚拟机标注联合为一个。

3、GC(Garbage Controller)流程:

  • 基本所有数据都会保存在JVM的堆内存之中。
  • 对于整个的GC流程里面,最需要处理的事是 年轻代 和 老年代 的内存清理操作。
  • 元空间(永久代)都不在GC范围内。


 

GC类型:(根据不同区域分类)

  • young GC 或 Minor GC是发生在 新生代中的垃圾收集动作,所采用的是复制算法。

复制算法:

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但是,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

  • Full GC 或 Major GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。

标记-清除算法:标记-清除算法

老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

一般的,首先是进行年轻代的GC,然后是老年代的GC,如果要压缩,每个代需要分别压缩。

如果年老区本身就已经很满了,满到无法放下从survivor熬出来的对象,那么young GC就不会再次触发,而是会使用full GC堆整个堆进行GC(除了CMS这种GC,因为CMS不能对年轻代进行GC)。

CMS GC是一款并发、使用标记-清除算法的gc,主要针对老年代进行回收的。


垃圾回收器类型:(根据运行机制分类)
        实际上,JVM 有三种类型的垃圾回收器 (GC),程序员可以选择使用其中一种。一般情况下,JVM 会根据底层硬件来选择垃圾回收器类型。

  • 串行 GC —— 一种单线程回收器。通常用在小型应用里面,处理少量的数据。可以使用 -XX: +UseSerialGC 来指定并启用该类型的 GC。
  • 并行 GC —— 从名字中就可以看出来,和序列化 GC 的不同点在于,并行 GC 使用多线程来执行垃圾回收处理程序。这种 GC 能处理大量的数据。可以使用 -XX:+UserParallelGC 来指定并启用该类型 GC。
  • 伪并发 GC —— 垃圾回收处理程序是非常消耗资源的,当它运行的时候,所有的线程都会暂停。而这种伪并发 GC,它可以做到和应用程序几乎同时工作,当然并不会 100% 和应用程序并发,所有的应用线程仍然会暂停一段时间,而这暂停时间会保持尽可能短,以获得最好的 GC 性能。实际上,对于这种伪并发 GC,有两种具体实现:

3.1 G1垃圾回收器——一种暂停时间在可接受范围内的高吞吐量 GC,使用 -XX:+UseG1GC 开启。
3.2 并发标记扫描垃圾回收器 —— 最小化应用线程暂停时间的 GC,可以通过 -XX:+UseConcMarkSweepGC 指定。在 JDK 9 中,这种 GC 已被弃用。


GC具体流程:

  1. 当现在有一个新生的对象产生,JVM需要为该对象进行内存空间申请。
  2. 先判断Eden区是否有内存空间,如果有,直接将新对象保存在Eden区。
  3. 如果Eden区的内存空间不足,会自动执行一个Minor GC操作,将Eden区无用内存空间进行清理。
  4. 清理Eden区之后继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eden区。
  5. 如果执行了Minor GC之后发现Eden区的内存仍然不足,那就判断存活区的内存空间,并将Eden区的部分活跃对象保存在存活区。
  6. 活跃对象迁移到存活区后,继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eben区。
  7. 如果存活区也没有空间了,则继续判断老年区,如果老年区充足,则将存活区部分活跃对象保存在老年区。
  8. 存活区的活跃对象迁移到老年区后,则将Eden区的部分活跃对象保存到存活区。
  9. 活跃对象迁移到存活区后,继续判断Eden区内存空间情况,如果充足,则将新对象直接保存在Eden区。
  10. 如果老年区也满了,这时候产生Major GC(full GC)进行老年区的内存清理。
  11. 如果老年区执行Major GC之后发现无法进行对象保存,会产生OutOfMemoryError异常。

4、堆内存参数调整:(调优关键)

  • 实际上每一块子内存区中都会存在有一部分可变伸缩区
  • 如果空间不足时,则在可变范围之内扩大内存空间
  • 当一段时间后,内存空间有余,再将可变空间进行释放:

堆内存空间调整参数:

  • -Xms:设置初始分配大小,默认为物理内存的1/64
  • -Xmx:最大分配内存,默认为物理内存的1/4
  • -XX:+PrintGCDetails:输出详细的GC处理日志
  • -XX:+PrintGCTimeStamps:输出GC的时间戳信息
  • -XX:+PrintGCDateStamps:输出GC的时间戳信息(以日期的形式)
  • -XX:+PrintHeapAtGC:在GC进行处理的前后打印堆内存信息
  • -Xloggc:(SavePath):设置日志信息保存文件
  • 在堆内存的调整策略中,基本上只要调整两个参数:-Xms和-Xmx

可通过Runtime类获取内存的整体信息:

代码如下:

package cn.liang.jvm;
public class memoryTest {
  public static void main(String[] args) {
      Runtime runtime = Runtime.getRuntime();
      long maxMemory = runtime.maxMemory();
      long totalMemory = runtime.totalMemory();
      System.out.println("max_memory=" + maxMemory /(double)1024/1024 + "M");
      System.out.println("total_memory=" + totalMemory /(double)1024/1024 + "M");
  }
}

输出结果:

max_memory=3641.0M
total_memory=245.5M

说明整个内存空间的可变范围(伸缩区):245.5M ~ 3641.0M之间,有可能造成整个程序的性能。

为了避免伸缩区的可调策略,使初始化内存等于最大内存,从而提升整个程序性能:

输出结果:

max_memory=981.5M
total_memory=981.5M
Heap
 PSYoungGen      total 305664K, used 15729K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 262144K, 6% used [0x00000007aab00000,0x00000007aba5c420,0x00000007bab00000)
  from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000)
  to   space 43520K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007bd580000)
 ParOldGen       total 699392K, used 0K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000)
  object space 699392K, 0% used [0x0000000780000000,0x0000000780000000,0x00000007aab00000)
 Metaspace       used 2708K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 293K, capacity 386K, committed 512K, reserved 1048576K

观察GC的触发操作
代码如下:

package cn.liang.jvm;
import java.util.Random;
public class gctest {
  public static void main(String[] args) {
      Random random = new Random();
      String str = "hello liang";
      while (true) {
          str +=str + random.nextInt(99999999);
          str.intern();   
      }           
  }
}

输出结果:

[GC (Allocation Failure) [PSYoungGen: 1769K->511K(2560K)] 1769K->775K(9728K), 0.0015982 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2374K->240K(2560K)] 2638K->1119K(9728K), 0.0011725 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2100K->256K(2560K)] 7841K->5996K(9728K), 0.0005402 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 256K->240K(2560K)] 5996K->5980K(9728K), 0.0005811 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 240K->0K(2560K)] [ParOldGen: 5740K->3925K(7168K)] 5980K->3925K(9728K), [Metaspace: 2662K->2662K(1056768K)], 0.0064126 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 41K->32K(2560K)] 6397K->6388K(9728K), 0.0003653 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(1536K)] 6388K->6388K(8704K), 0.0003294 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(1536K)] [ParOldGen: 6356K->2710K(7168K)] 6388K->2710K(8704K), [Metaspace: 2662K->2662K(1056768K)], 0.0035285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 19K->0K(2048K)] 5160K->5140K(9216K), 0.0004489 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5140K->5140K(9216K), 0.0003114 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5140K->5140K(7168K)] 5140K->5140K(9216K), [Metaspace: 2662K->2662K(1056768K)], 0.0030502 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5140K->5140K(9216K), 0.0003198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5140K->5127K(7168K)] 5140K->5127K(9216K), [Metaspace: 2662K->2662K(1056768K)], 0.0039555 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:3332)
  at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
  at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
  at java.lang.StringBuilder.append(StringBuilder.java:136)
  at cn.liang.jvm.gctest.main(gctest.java:11)
Heap
 PSYoungGen      total 2048K, used 40K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 3% used [0x00000007bfd00000,0x00000007bfd0a120,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 7168K, used 5127K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
  object space 7168K, 71% used [0x00000007bf600000,0x00000007bfb01c78,0x00000007bfd00000)
 Metaspace       used 2693K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 292K, capacity 386K, committed 512K, reserved 1048576K

日后如果发现你的程序执行速度变慢了,可以针对程序的运行内存进行分析:

  • 可视化工具:…\Java\jdk1.8.0_131\bin\jvisualvm.exe
  • 命令查看:jmap(jmap -heap JavaPID)

三、栈(stock)

1、每个线程包含一个栈区(堆只有一个所有线程共享),栈中只保存基本数据类型的对象和自定义对象的引用,对象都存放在堆区中。
2、每个栈中的数据(原始类型 和 对象引用)都是私有的,其他栈不能访问。
3、栈分为3部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

4、过程:栈用于存储程序运行时在方法中声明的所有局部变量(栈主要存储方法中的局部变量)。JVM会为每一个方法分配一个对应的空间,这个空间称为该方法的栈帧。一个栈帧对应一个正在调用的方法,栈帧中存储了该方法的参数、局部变量等数据。当某一方法调用完成后,其对应的栈帧将被清除,局部变量失效。(方法结束,局部变量失效,从栈中清除)


四、方法区(method)

1、方法区又叫静态区,里存储着class文件的信息和动态常量池,class文件的信息包括类信息和静态常量池。
2、用于储存已被虚拟机加载的类信息、常量、静态常量、即使编译器编译后的代码等数据。
3、垃圾收集行为在方法区很少出现,这块区域回收的主要目标是针对常量池的回收和对类型的卸载。
4、运行常量池是方法区的一部分,常量池用于存放编译期生成的各种字面量和符号引用(还有翻译出来的直接引用),这部分内容在类加载后进入方法区的运行时常量池中存放。
5、运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,运行期也可能将新的常量放入池中。
6、字面量:如文本字符串,声明为final的常量值等。

public stick final int i =3;
String s="abc";

7、符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。在对java文件进行编译的过程中,并不会向C语言那样有连接这一步,也就是说class文件中不会存储方法、字段的最终内存布局信息,所以符号引用是不能被虚拟机直接使用的,虚拟机会在加载类时动态的去获取常量池中的符号引用,然后解析到对应的内存地址中,才可以使用
8、方法区用于存放类的信息,Java程序运行时,首先会通过类装载器载入文件的字节码信息,经过解析后将其装入方法区。类的各种信息(包括方法)都在方法区储存。(将类的成员都加载到方法区)类在实例化对象时,多个对象会拥有各自在堆中的空间,但所有实例对象是共用在方法区中的一份方法定义的。方法只有一份。


五、Java 堆栈方法区总结

1、基础类型直接在栈空间分配,方法的形式参数直接在栈中分配,当方法调用完成后从栈空间回收。
2、引用数据类型,需要new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。
3、方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,在方法调用完成后从栈空间回收。
4、局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间会被立即回收,堆空间区域等待GC回收。
5、方法调用时传入literal(常量)参数,在方法调用完成后从栈空间分配。
6、字符串常量在 DATA 区域分配 ,this 在堆空间分配 。
7、数组既在栈空间分配数组名称, 又在堆空间分配数组实际的大小。
8、static在DATA区域分配。

从Java的这种分配机制来看,堆栈又可以这样理解:堆栈是操作系统在建立某个进程或者线程(在支持多线程的操作系统中是线程)上为这个线程建立的储存区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中创建的所有类实例或数组都放在堆中,并由应用所有的线程共享,跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的储存空间都是在堆中分配的,但是这个对象的引用却是在栈中分配的,也就是说建立一个对象时从两个地方都分配了内存,在堆中分配的内存实际上建立了这个对象,而在栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。


六、例:创建一个对象过程运行时数据区的变化

AppMain.java

public   class  AppMain {    //运行时, jvm 把appmain的信息都放入方法区   
      //main 方法本身放入方法区。
    public   static   void  main(String[] args) {     
           //test1是引用,所以放到栈区里, Sample是自定义象应该放到堆里面
        Sample test1 = new  Sample( " 测试1 " );   
        Sample test2 = new  Sample( " 测试2 " );   
        test1.printName();   
        test2.printName();   
    } 
    //Sample.java
    public   class  Sample {        //运行时, jvm 把appmain的信息都放入方法区   
        private name;      //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里   
        public Sample(String name) { //构造方法
                  this .name = name;   
        }   
        public void  printName() {  //print方法本身放入 方法区里。   
            System.out.println(name);   
        }   
    }    
} 

下面是行动向导图:

        系统收到我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取整个文件中的二进制数据,然后把AppMain.class类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。
        接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令,这个main()方法的第一条语句是:Sample test1 = new Sample(“测试1”);
        语句很简单,就是让Java虚拟机创建一个Sample实例,并且,使引用变量test1引用这个实例。下面就让我们跟踪一下虚拟机,看看它究竟是怎么来执行这个任务的:

  1. Java虚拟机一看,不就是想创建一个Sample实例嘛,简单,于是直奔方法区而去,先找到Sample类的类型信息再说,结果呢,没找到,这会方法区里还没有Sample类呢,于是虚拟机立马加载了Sample实例,分配内存,把Sample类的类型信息存放在方法区里。
  2. 好啦、资料找到啦,下面开始干活,Java虚拟机先是在堆中为一个新的Sample实例分配内存,这个Sample实例持有着指向方法区的Sample类的类型信息的引用,这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就有点类似C语言里的指针了,而这个地址呢,就存放在Sample实例的数据区里。
  3. 在Java虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据,OK,原理讲完了,就让我们来继续我们的跟踪行动,位于"=“前的test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,就会被添加到了执行main()方法的主线程的Java方法调用栈中。而”="将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。
  4. OK,到这里为止呢,Java虚拟机就完成了简单语句的执行任务,参考我们的行动图,我们终于初步摸清Java虚拟机的一点底细了!接下来,Java虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后一次执行他们的printName()方法,当Java虚拟机执行test1.printName()时,Java虚拟机会根据局部变量持有的引用,定位到堆中的Sample实例,再根据Sample实例持有的引用,定位到方法区中Sanple类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令。

七、例:创建一个基本类型的值的过程

//栈中的数据可以共享:

int a = 3;
int b = 3;

编译器先处理int a = 3;首先它会再栈中创建一个变量a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存进来,然后将a指向3,接着处理int b = 3;在创建完b的引用后,因为栈中已经有3这个值,便将3直接指向3,这样就出现了a于b同时指向3的情况。这时,如果再令a = 4;那么编译器会重新搜索栈中是否有4,如果没有,则将4存起来,将a指向4,如果已经有了,则直接将a指向这个地址,因此a值得改变不会影响b值。要注意这种数据得共享与两个对象得引用指向一个对象得这种共享是不同的,因为这种情况a的修改并不会影响到b,它是由编译器完成的,它有利于节省空间,而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

==比较的是对象的地址,也就是是否是同一个对象;

equal比较的是对象的值。
 

发布了165 篇原创文章 · 获赞 533 · 访问量 109万+

猜你喜欢

转载自blog.csdn.net/qq_33591903/article/details/102741487
今日推荐