龙空技术网

Java堆的使用

编程码农张 1227

前言:

此刻你们对“java的堆”大约比较关心,朋友们都想要学习一些“java的堆”的相关内容。那么小编同时在网上网罗了一些关于“java的堆””的相关资讯,希望咱们能喜欢,看官们一起来了解一下吧!

Java堆的基本概念

Java 堆是虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一作用就是存放对象实例,几乎所有的对象实例都是在这里分配的(不绝对,在虚拟机的优化策略下,也会存在栈上分配、标量替换的情况)。当类加载器读取了类文件后,需要把类、方法、常量、变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

Java 堆是 GC 回收的主要区域,因此很多时候也被称为 GC 堆。从内存回收的角度看,Java 堆还可以被细分为新生代和老年代;再细一点新生代还可以被划分为 Eden Space、From Survivor Space、To Survivor Space。从内存回收的角度看,线程共享的 Java 堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。「属于线程共享的内存区域」

堆在逻辑上分为三个区域:

Java7:

Java8:

可以看到,在Java7时代,堆分为新生区(新生区包含伊甸园区和幸存区,幸存区又包含幸存者0区和幸存者1区。此外,幸存者0区又称为From区,幸存者1区又称为To区,From区和To区并不是固定的,复制之后交互,谁空谁是To),养老区和永久代;在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间与永久代之间最大的区别在于:永久代使用的JVM的堆内存(但是逻辑上是非堆的),但是java8以后的元空间并不在虚拟机中而是使用本机物理内存(所以在上图中,我用虚线表示)。

永久代:是一个常驻内存的区域,用于存放JDK自身所携带的Class,Interface的元数据,即存储的是运行环境必须的类信息,被转载进此区域的数据是不会被垃圾回收的,只有关闭JVM才会释放此区域所占用的内存空间。

元空间:取代永久代,不在Java虚拟机的堆中实现,而是使用本机物理内存实现。默认情况下元空间大小仅受本地内存限制。类的元数据放入native memory,字符串常量在Java堆中(运行时常量和基本类型常量在元空间——方法区)

PS:jdk1.8,jvm把字符串常量池移到了堆内存里。此时方法区=元空间

堆之所以要分区是因为:Java程序中不同对象的生命周期不同,70%~99%对象都是临时对象,这类对象在新生区“朝生夕死”。如果没有分区,GC时搜集垃圾需要对整个堆内存进行扫描;分区后,回收这些“朝生夕死”的对象,只需要在小范围的区域中(新生区)搜集垃圾。所以,分区的唯一理由就是为了优化GC性能。

堆空间对象分配过程

下面通过一个例子来讲述这几个区的交互逻辑:

1.几乎任何新的对象都是在伊甸园区被new出来创建,刚开始的时候两个幸存者区和养老区都是空的:

2.随着对象的不断创建,伊甸园区空间逐渐被填满:

3.这时候将触发一次Minor GC(Young GC),删除未引用的对象,GC剩下来的还存在引用的对象将移动到幸存者0区,然后清空伊甸园区:

4.随着对象的创建,伊甸园区空间又满了,再一次触发Minor GC,删除未引用的对象,留下存在引用的对象。这次和上一次Minor GC有些不同,这轮GC留下的对象将被移动到幸存者1区,并且上一轮GC留下来的存储在幸存者0区的对象年龄递增并移动到幸存者1区。当所有幸存对象都移动到幸存者1区后,幸存者0区和伊甸园区空间清除:

5.随着对象的创建伊甸园区空间再一次满了,触发了第三次Minor GC,这一次幸存区空间将发生互换,GC留下来的幸存者将移动到幸存者0区,幸存者1区的幸存对象年龄递增后也移动到幸存者0区,然后伊甸园区和幸存者1区的空间被清除:

6.随着Minor GC的不断发生,幸存对象在两个幸存区不断地交换存储,年龄也不断递增。如此反反复复之后,当幸存对象的年龄达到指定的阈值(这个例子中是8,由JVM参数MaxTenuringThreshold决定)后,它们将被移动到养老区:

7.随着上述过程的不断出现,当养老区快满时,将触发Major GC(Full GC)进行养老区的内存清理。若养老区执行了GC之后发现依然无法进行对象的保存,就会产生OOM异常。

一个对象被放置到养老区除了它的年龄达到阈值外,以下几种情况也会使得该对象直接被放置到养老区:

对象创建后,无法放置到伊甸园区(比如伊甸园区的大小为10m,新的对象大小为11m,伊甸园区不够放,触发YGC。YGC后伊甸园区被清空,但还是无法容下11m的“超大对象”,所以直接放置到养老区。当然如果养老区放置不下则会触发FGC,FGC后还放不下则OOM);

YGC后,对象无法放置到幸存者To区也会直接晋升到养老区;

如果幸存区中相同年龄的所有对象大小大于幸存区空间的一半,年龄大于或等于这些对象年龄的对象可以直接进入养老区,无需等到年龄阈值。

堆参数

以JDK1.8+HotSpot为例,常用的可调整的堆参数有:

参数

含义

-Xms,等价于-XX:InitialHeapSize

设置堆的初始内存大小,默认为物理内存的1/64

-Xmx,等价于-XX:MaxHeapSize

设置堆的最大内存大小,默认为物理内存的1/4

-XX:Newratio

设置新生区和养老区的比例,比如值为2(默认值),则养老区是新生区的2倍,即养老区占据堆内存的2/3

-XX:Surviorratio

设置伊甸园区和一个幸存区的比例,比如值为8(默认值)则表示伊甸园区占新生区的8/10(两个幸存区是一样大的)

-Xmn

设置堆新生区的内存大小(一般不使用)

-XX:MaxTenuringThreshold

设置转入养老区的存活次数,默认值为15

-XX:+PrintFlagsInitial

查看所有参数的默认初始值

-XX:+PrintFlagsFinal

查看所有参数的最终值(被我们修改后的值不再是默认初始值)

生产环境中,推荐将-Xms和-Xmx设置为一样大,因为这样做的话在Java垃圾回收清理完堆区后不需要重新计算堆区大小,从而提高性能。此外,要在程序中输出详细的GC处理日志,可以使用-XX:+PrintGCDetails。

比如,我的电脑内存为32GB,所以堆的默认初始值大小为500MB左右,堆的最大值大约为8000MB左右:

public class Test {undefined

public static void main(String[] args) {undefined

long maxMemory = Runtime.getRuntime().maxMemory();

long totalMemory = Runtime.getRuntime().totalMemory();

System.out.println("堆内存的初始值" + totalMemory / 1024 / 1024 + "mb");

System.out.println("堆内存的最大值" + maxMemory / 1024 / 1024 + "mb");

}

}

程序输出:

堆内存的初始值491mb

堆内存的最大值7282mb

可以通过IDEA调整堆的大小:

我们将堆内存的初始大小和最大值都设置为10mb,并且开启GC日志打印,重新运行下面这段程序:

public class Test {undefined

public static void main(String[] args) {undefined

long maxMemory = Runtime.getRuntime().maxMemory();

long totalMemory = Runtime.getRuntime().totalMemory();

System.out.println("堆内存的初始值" + totalMemory / 1024 + "kb");

System.out.println("堆内存的最大值" + maxMemory / 1024 + "kb");

}

}

输出如下所示:

堆内存的初始值9728kb

堆内存的最大值9728kb

Heap

PSYoungGen total 2560K, used 1388K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)

eden space 2048K, 67% used [0x00000007bfd00000,0x00000007bfe5b370,0x00000007bff00000)

from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)

to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)

ParOldGen total 7168K, used 0K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)

object space 7168K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfd00000)

Metaspace used 2947K, capacity 4496K, committed 4864K, reserved 1056768K

class space used 320K, capacity 388K, committed 512K, reserved 1048576K

可以看到,PSYoungGen(新生区)的总内存大小为2560k,ParOldGen(养老区)的总内存大小为7168k,总和刚好是9728K,这也说明了:Java8后的堆物理上只分为新生区和养老区,Metaspace(元空间)不占用堆内存,而是直接使用物理内存。

那为什么我们设置的堆内存大小是10m(10240kb),控制台输出却只有9728kb呢?从上面的例子我们知道,幸存者区分为0区和1区,根据复制算法的特点,这两个区同一时刻总有一个区是空的,所以控制台输出的内存计算方式为:2048K(eden space)+512K(from space or to space)+7168K(ParOldGen)=9728K。9728K再加一个幸存区的大小512K刚好是10240K。

再举个OOM的例子,使用刚刚-Xms10m -Xmx10m -XX:+PrintGCDetails的设置,运行下面这段程序:

public class Test {undefined

public static void main(String[] args) {undefined

String value = "hello";

while (true) {undefined

value += value + new Random().nextInt(1000000000) + new Random().nextInt(1000000000);

}

}

}

输出如下:

[GC (Allocation Failure) [PSYoungGen: 1893K->491K(2560K)] 1893K->597K(9728K), 0.0007246 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

[GC (Allocation Failure) [PSYoungGen: 2207K->496K(2560K)] 2313K->1153K(9728K), 0.0008383 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[GC (Allocation Failure) [PSYoungGen: 2007K->496K(2560K)] 2664K->1897K(9728K), 0.0009456 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

[GC (Allocation Failure) [PSYoungGen: 2021K->496K(2560K)] 4894K->4113K(9728K), 0.0010814 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[GC (Allocation Failure) [PSYoungGen: 1359K->496K(2560K)] 6448K->5600K(9728K), 0.0015792 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[GC (Allocation Failure) [PSYoungGen: 496K->496K(1536K)] 5600K->5600K(8704K), 0.0006416 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(1536K)] [ParOldGen: 5104K->2585K(7168K)] 5600K->2585K(8704K), [Metaspace: 2982K->2982K(1056768K)], 0.0044783 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

[GC (Allocation Failure) [PSYoungGen: 61K->192K(2048K)] 7061K->7192K(9216K), 0.0012566 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

[Full GC (Ergonomics) [PSYoungGen: 192K->0K(2048K)] [ParOldGen: 7000K->1840K(7168K)] 7192K->1840K(9216K), [Metaspace: 3042K->3042K(1056768K)], 0.0072023 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

[GC (Allocation Failure) [PSYoungGen: 65K->160K(2048K)] 6321K->6416K(9216K), 0.0022603 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

[Full GC (Ergonomics) [PSYoungGen: 160K->0K(2048K)] [ParOldGen: 6256K->4785K(7168K)] 6416K->4785K(9216K), [Metaspace: 3076K->3076K(1056768K)], 0.0056740 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]

[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4785K->4785K(9216K), 0.0003871 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4785K->4765K(7168K)] 4785K->4765K(9216K), [Metaspace: 3076K->3076K(1056768K)], 0.0049903 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

Heap

PSYoungGen total 2048K, used 59K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)

eden space 1024K, 5% used [0x00000007bfd00000,0x00000007bfd0efb8,0x00000007bfe00000)

from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)

to space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)

ParOldGen total 7168K, used 4765K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)

object space 7168K, 66% used [0x00000007bf600000,0x00000007bfaa77b8,0x00000007bfd00000)

Metaspace used 3113K, capacity 4496K, committed 4864K, reserved 1056768K

class space used 339K, capacity 388K, committed 512K, reserved 1048576K

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:674)

at java.lang.StringBuilder.append(StringBuilder.java:208)

at cc.mrbird.Test.main(Test.java:19)

可以看到,经过数次的GC和Full GC后,堆内存还是无法腾出空间,最终抛出OOM错误。日志的含义如下图所示:

Young GC(Minor GC):

Full GC(Major GC):

TLAB

JVM对伊甸园区继续进行划分,为每个线程分配了一个私有缓存区域,这块区域就是TLAB(Thread Local Allocation Buffer)。多线程同时分配内存时,使用TLAB可以避免一系列非线程安全问题,同时还能够提升内存分配的吞吐量。尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选:

我们可以使用-XX:UseTLAB设置是否开启TLAB,举个例子:

public class Test {undefined

public static void main(String[] args) throws InterruptedException {undefined

TimeUnit.SECONDS.sleep(100);

}

}

运行main方法:

可以看到TLAB默认是开启的。

TLAB空间的内存非常小,仅占整个伊甸园区的1%,可以通过-XX:TLABWasteTargetPercent设置TLAB空间所占用伊甸园区空间的百分比。

有了TLAB的概念后,我们就不能说堆空间一定是线程共享的了。

私信666领取资料

标签: #java的堆