java堆内存探究

来源:互联网 发布:淘宝邮箱注册企业名称 编辑:程序博客网 时间:2024/04/29 10:04

一些jvm内部区域的概念

1:栈

   在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。  

   当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

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

2:堆

   堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。  引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是java比较占内存的原因。实际上,栈中的变量指向堆内存中的变量,这就是java中的指针! 

   Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

   jvm只有一个堆区(heap)被所有线程共享。

3、方法区(method area)

   方法区跟堆一样,被所有的线程共享。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

4、常量池(constant pool)

   常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名; 字段的名称和描述符; 方法和名称和描述符。虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。在程序执行的时候,常量池会储存在方法区(Method Area),而不是堆中(jdk7之前是的,从jdk7开始常量池迁移至堆中)。

   栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享(指的是线程共享,而给进程共享)。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象句柄(引用)。

具体java堆的介绍

堆内存

java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致为:

 

                         (网络图片)

    从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
本人使用的是 JDK1.8,以下涉及的 JVM 默认值均以该版本为准。
默认的,新生代 ( 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% )的新生代空间。

GC堆概念介绍

    Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。
当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 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 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人 "早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

网络查找的一些常用的jvm参数配置:

  -XX:+PrintGC 启动java虚拟机后,只要遇到gc,就打印日志。

 -XX:+PrintGCDetails gc发生时,打印更详细的日志。

-XX:+PrintHeapAtGC gc发生时,打印更详细的堆信息。

 -XX:+PrintGCTimeStamps gc发生时,额外打印gc时间,该时间为虚拟机启动到现在的时间偏移量。

 -XX:+PrintGCApplicationConcurrentTime gc时打印应用程序执行的时间。

-XX:+PrintGCApplicationStoppedTime gc时打印应用程序由于gc产生停顿的时间。

-XX:+PrintReferenceGC 跟踪系统内的软引用,若引用,虚引用和Finallize队列。

-Xloggc 指定gc日志的保存路径。

-XX:+TraceClassLoading 跟踪类加载。

-XX:+TraceClassUnloading 跟踪类卸载。

 -XX:+PrintVMOptions 程序运行时,打印虚拟机接收到的命令行显示参数。

-XX:+PrintCommandLineFlags 打印传递给虚拟机的显式和隐式参数。

-XX:+PrintFlagsFinal 打印所有系统参数的值。

-Xms 指定初始堆空间的大小,例如-Xms20m

-Xmx 指定最大堆空间的大小,例如-Xmx100m

-Xmn 指定新生代的大小,例如-Xmn1m

-XX:MaxHeapSize 指定最大内存。

-XX:SurvivorRatio 指定新生代中eden区和from/to区的比例关系。

 -XX:NewRatio 设置新生代和老生带的比例,注意:这个值的含义是 老生带/新生代。

-XX:+HeapDumpOnOutOfMemoryError 内存溢出时,导出整个堆的信息,和下一个参数配合使用。

 -XX:HeapDumpPath 导出的堆信息的保存路径,和上一个参数配合使用。

-XX:OnOutOfMemoryError 内存溢出发生错误时执行一个脚本文件。

-XX:PermSize 配置初始永久区的大小(JDK8中永久区已经被彻底移除,使用了新的元数据区存放类的元数据)。

-XX:MaxPermSize 配置最大永久区的大小(JDK8中永久区已经被彻底移除,使用了新的元数据区存放类的元数据)。

-XX:MaxMetaspaceSize 指定永久区最大可用值。

-Xss 指定线程的栈大小。

-XX:MaxDirectMemorySize 指定最大可用直接内存值。

-server 指定虚拟机在server模式下工作。

-client 指定虚拟机在client模式下工作。

 接下来进行测试堆内存对象存活的不同年龄段:

代码:

package jvmgc;@SuppressWarnings("unused")public class jvmInfo {/** * jvm堆信息 * 堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。 * 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。 * 新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor(幸存者)、To Survivor。 * 既然虚拟机采用了分代收集的思想来管理内存, * 那么内存回收时就必须能识别哪些对象应放在新生代, * 哪些对象应放在老年代中。 * 为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。 * 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话, * 将被移动到Survivor空间中,并且对象年龄设为1。 * 对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁, * 当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。 * 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。 * 分别对-XX:MaxTenuringThreshold = 1和-XX:MaxTenuringThreshold = 15进行测试VM参数设置 * -verbose:gc        -Xms60M  * -Xmx60M        -Xmn20M  * -XX:+PrintHeapAtGC  * -XX:SurvivorRatio=8-XX:MaxTenuringThreshold=1 -XX:PermSize=30m -XX:PermSize=30m */private static final int _1MB = 1024 * 1024;private  static byte[] NUM1, NUM2;public static void main(String[] args) {    NUM1 = new byte[4 * _1MB];   NUM1 = null;  //System.gc();  NUM2 = new byte[4 * _1MB];  System.gc();}}


 Minor GC(新生代 GC)前堆内存情况:

{Heap before GC invocations=1 (full 0): PSYoungGen      total 18432K, used 8519K   eden space 16384K, 52% used   from space 2048K, 0% used   to   space 2048K, 0% used PSOldGen        total 40960K, used 0K  object space 40960K, 0% used  PSPermGen       total 30720K, used 2941K   object space 30720K, 9% used [GC 8519K->4416K(59392K), 0.0066020 secs]

Minor GC(新生代 GC)后堆内存情况:

Heap after GC invocations=1 (full 0): PSYoungGen      total 18432K, used 320K  eden space 16384K, 0% used  from space 2048K, 15% used  to   space 2048K, 0% used  PSOldGen        total 40960K, used 4096K   object space 40960K, 10% used PSPermGen       total 30720K, used 2941K   object space 30720K, 9% used}


Full GC(老年代 GC)前堆内存情况

 

{Heap before GC invocations=2 (full 1): PSYoungGen      total 18432K, used 320K   eden space 16384K, 0% used   from space 2048K, 15% used   to   space 2048K, 0% used  PSOldGen        total 40960K, used 4096K   object space 40960K, 10% used  PSPermGen       total 30720K, used 2941K   object space 30720K, 9% used [Full GC 4416K->4289K(59392K), 0.0048373 secs]


 

 Full GC(老年代 GC)后堆内存情况

 Heap after GC invocations=2 (full 1): PSYoungGen      total 18432K, used 0K  eden space 16384K, 0% used   from space 2048K, 0% used   to   space 2048K, 0% used  PSOldGen        total 40960K, used 4289K   object space 40960K, 10% used PSPermGen       total 30720K, used 2941K  object space 30720K, 9% used }


分析打印结果:

为新生代分配的大小为-Xmn20M

堆中新生代的内存空间为 18432K ( 约 18M ),eden 的内存空间为 16384K ( 约 16M),from / to survivor 的内存空间为 2048K ( 约 2M)。

新生代 = eden + from + to = 16 + 2 + 2 = 20M,可见新生代的内存空间确实是按 Xmn 参数分配得到的。

而且这里指定了 SurvivorRatio = 8,因此,eden = 8/10 的新生代空间 = 8/10 * 20 = 16M。from = to = 1/10 的新生代空间 = 1/10 * 20 = 2M。
堆信息中新生代的 total 18432K 是这样来的: eden + 1 个 survivor = 16384K + 2048K = 18432K,即约为 18M。
因为 jvm 每次只是用新生代中的 eden 和 一个 survivor,因此新生代实际的可用内存空间大小为所指定的 90%。
因此可以知道,这里新生代的内存空间指的是新生代可用的总的内存空间,而不是指整个新生代的空间大小。
另外,可以看出老年代的内存空间为 40960K ( 约 40M ),堆大小 = 新生代 + 老年代。因此在这里,老年代 = 堆大小 - 新生代 = 60 - 20 = 40M。
最后,这里还指定了 PermSize = 30m,PermGen 即永久代 ,它还有一个名字,叫非堆,主要用来存储由 jvm 加载的类文件信息、常量、静态变量等。

分析执行GC的过程:

当第一次执行Minor GC前发现新生代上面的使用情况:

新生代:

YoungGen      total 18432K, used 8519Keden space 16384K, 52%from space 2048K, 0%to   space 2048K, 0%


老年代

OldGen  total 40960K, used 0K永久代PermGen       total 30720K, used 2941Kobject space 30720K, 9%[GC 8519K->4416K(59392K), 0.0066020 secs]


GC之后结果:

新生代:

YoungGen      total 18432K, used 320Keden space 16384K, 0% usedfrom space 2048K, 15% usedto   space 2048K, 0% used


老年代:

OldGen total 40960K, used 4096Kobject space 40960K, 10% Minor GC执行之后发现eden中被回收完了,进入了from survivor中一部分,而一部分进入了老年代,其余的被回收了[Full GC 4416K->4289K(59392K), 0.0048373 secs]

Full GC之后结果:

新生代

YoungGen      total 18432K, used 0Keden space 16384K, 0% used from space 2048K, 0% used to  pace 2048K, 0% used 


老年代

OldGen        total 40960K, used 4289Kobject space 40960K, 10%

  •  新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
    备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  •  老年代 GC(Major GC  / Full GC):指发生在老年代的 GC,出现了 Major GC,经常
    会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
    就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
    倍以上。

 Full GC之后发现新生代内存被回收了,老年代内存使用从4096->4289有一部分进入了老年代,新生代 ( YoungGen ) 中存活的对象又提前进入老年代了。

参考文献:http://www.blogjava.net/fancydeepin/archive/2013/09/29/jvm_heep.html

0 0
原创粉丝点击