做一个简单的网站要多少钱,免费发布信息,卡密网站怎么做,网页内容有哪些挖掘JVM堆内存结构 文章目录 挖掘JVM堆内存结构堆的核心概念堆的特点 堆的内存结构内存划分新生代/新生区#xff08;Young Generation#xff09;老年代#xff08;Tenured Generation#xff09;永久代#xff08;或元数据区#xff09;#xff08;PermGen 或 MetaSpa…挖掘·JVM堆内存结构 文章目录 挖掘·JVM堆内存结构堆的核心概念堆的特点 堆的内存结构内存划分新生代/新生区Young Generation老年代Tenured Generation永久代或元数据区PermGen 或 MetaSpace 设置堆空间的大小与OOM手动设置堆内存大小的值OOM 堆的内存分配*对象分配过程 GC垃圾回收概念Minor GC、Major GC、Full GC案例与日志分析 *内存分配策略TLAB(Thread-Local Allocation Buffer)什么是TLAB为什么要有TLAB有TLAB对象的分配过程 *逃逸分析(Escape Analysis)逃逸分析的基本行为参数设置逃逸分析优化1) 代码优化-栈上分配2) 代码优化-同步消除3) 代码优化-标量替换 堆空间的参数设置总结总结资料 堆的核心概念
JVMJava虚拟机中的 “堆” 是指Java程序运行时用来存储对象实例的一块内存区域。堆内存是JVM管理的最大一块内存区域之一它的主要作用是存储对象实例包括类实例、数组和其他对象。 以下是运行时数据区的整体结构图其中方法区和堆区都是属于线程共有的也就是一个进程中只有一个堆区和方法区。 我们所运行的Java文件就可以看作为一个进程而与之对应的是JVM实例进程中会包含许多的线程而这些线程就会共享方法区和堆区。
堆的特点
一个JVM实例就只是对应着一个堆区堆是Java内存管理的一大核心区域。堆内存的大小在JVM启动时可以指定但通常是在运行时动态分配的。这意味着堆内存可以根据程序的需要动态增长或缩小以容纳新创建的对象。堆的存储可以是在物理上不连续但是在逻辑上连续。[1]虽然堆是线程共有的但是可以划分线程私有缓冲区Thread Local Allocation Buffer, TLAB[2]对象和数组都不会存储在栈上栈帧中保存的是引用这个引用指向的是对象或数组在堆中的位置。在方法结束之后堆的对象不会立马被清除仅仅只会在垃圾收集的时候才被移除。堆是垃圾回收的重点区域。 [1]对于物理不连续逻辑连续可以通过以下几个内容来认识。 磁盘存储在硬盘驱动器或固态硬盘等存储设备上数据通常存储在不同的磁道、扇区或块上。物理上这些数据存储单元可能不是紧密相连的它们分布在存储介质上的不同位置。然而逻辑上操作系统和文件系统会将这些数据组织成文件和文件系统以使它们在逻辑上看起来是连续的。内存分配在计算机内存中变量和数据结构可以分散存储在不同的内存地址上。这些内存地址在物理上可以是不连续的但在编程中我们可以通过变量名或指针引用来访问它们从逻辑上看起来它们是连续的。数据库表在数据库中表中的行和列可以以物理上不连续的方式存储在磁盘上但从查询和应用程序的角度来看它们是逻辑上连续的。数据库管理系统负责处理物理存储和逻辑视图之间的映射。 [2]TLAB旨在提高多线程环境中的对象分配性能减少锁竞争和减小垃圾回收的开销。当数据存放在堆区由于多线程对此数据进行操作这就会导致并发问题。 堆的内存结构
堆内存是JVM管理的最大的内存区域之一它通常用于存储在运行时创建的对象包括类实例、数组等。堆内存逻辑结构主要分为三部分新生区、养老区、永久代/元数据区在jdk7之前是叫永久代而在jdk8之后就开始称为元数据区。
内存划分 新生代/新生区Young Generation
新生代主要分为三个部分Eden区、Survivor区1S0或From区和Survivor区2S1或To区幸存者区[3]。新对象首先会被分配到Eden区。当Eden区满时一部分对象会被移动到Survivor区1或Survivor区2经过多次循环后仍然存活的对象会被晋升到老年代。Minor GC年轻代垃圾收集通常在新生代执行回收不再存活的对象。
老年代Tenured Generation
老年代是存储已经经过多次Minor GC并且存活的对象。老年代都是具有较长的生命周期。Full GC全局垃圾收集通常会在老年代执行回收老年代中的垃圾对象。
永久代或元数据区PermGen 或 MetaSpace
永久代JDK1.7之前用于存储类的元数据信息、常量池、静态变量等。在较新的JVM版本中JDK1.8之后永久代被元数据区MetaSpace取代元数据区不再固定大小而是由操作系统动态分配从而减轻了永久代内存问题。这些区域中的对象通常具有较长的生命周期。 [3]: Survivor Space幸存者区是Java虚拟机JVM中新生代Young Generation的一部分用于存储在垃圾收集过程中仍然存活的对象。幸存者区分为两部分大小是相同的Survivor区1S0或From区和Survivor区2S1或To区其中Survivor区1和Survivor区2一般被用来交替地存储幸存的对象。在执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor也就是To Survivor同时Eden区域的内存会在垃圾回收的过程中全部释放另一个survivor即From Survivor里不能被回收的对象也会被放入这个survivor即To Survivor然后To Survivor 和 From Survivor的标记会互换始终保证一个survivor是空的。 设置堆空间的大小与OOM
JVM中的堆空间是用来存储对象实例的然而这个堆大小在JVM启动的时候就已经确定了大小了我们可以通过-Xms和-Xmx来设置。
-Xms: 用来设置堆空间(年轻代老年代)的初始内存大小等同-XX:InitialHeapSize-Xmx: 用来设置堆空间(年轻代老年代)的最大内存大小等同-XX:MaxHeapSize
如果堆内存大于了堆空间的最大内存那么就会出现OOM即抛出OutOfMemoryError异常。
*在开发中建议是将Xms和Xmx的值设置成一样其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小从而提高性能。
如果没有设置堆空间大小我们可以通过Java代码来查看默认的堆空间大小。通过Runtime每个Java应用程序都有一个Runtime类实例该类允许应用程序与运行应用程序的环境进行交互。
public class HeapSpaceInitial {public static void main(String[] args) throws InterruptedException {// JVM推内存容量long initialMemory Runtime.getRuntime().totalMemory() / 1024 / 1024;// JVM最大堆内存量long maxMemory Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println(-Xms: initialMemory M);System.out.println(-Xmm: maxMemory M);System.out.println(系统内存大小: initialMemory * 64.0 / 1024 G);System.out.println(系统内存大小: maxMemory * 4.0 / 1024 G);}
}在默认情况下堆空间的大小都有各自的计算方式
初始内存大小物理电脑内存大小 / 64最小内存大小物理电脑内存大小 / 4
手动设置堆内存大小的值
通过-Xms和-Xmx在idea中我们可以更改运行配置在VM options 中输入-Xms600m -Xmx600m来设置600M的内存大小。我们直接运行以上代码看一下7/8行所打印出来的数据。
-Xms: 575M
-Xmm: 575M我们明明设置了600M但为何只剩下575M了呢首先可以通过在VM Options中追加配置-XX:PrintGCDetails来打印GC的详细信息他是基于程序运行结束之后显示的数据。运行以上代码出现的结果
-Xms: 575M
-Xmm: 575M
HeapPSYoungGen total 179200K, used 12288K [0x00000000f3800000, 0x0000000100000000, 0x0000000100000000)eden space 153600K, 8% used [0x00000000f3800000,0x00000000f44001b8,0x00000000fce00000)from space 25600K, 0% used [0x00000000fe700000,0x00000000fe700000,0x0000000100000000)to space 25600K, 0% used [0x00000000fce00000,0x00000000fce00000,0x00000000fe700000)ParOldGen total 409600K, used 0K [0x00000000da800000, 0x00000000f3800000, 0x00000000f3800000)object space 409600K, 0% used [0x00000000da800000,0x00000000da800000,0x00000000f3800000)Metaspace used 3315K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 364K, capacity 388K, committed 512K, reserved 1048576K我们看一下如何计算首先是新生代(PSYoungGen)其总数是179200K这个179200K153600K25600K也就是幸存者区只会计算一个区域的数值因为会始终保持一个区域是为空的。这样算的话那么内存大小就是(PSYoungGen(179200K)ParOldGen(409600K)) / 1024 575M我们设置的600M(eden(153600K)from(25600K)to(25600K)ParOldGen(409600K) )/ 1024 也就是说计算内存大小的时候并不会吧两个幸存者区都计算进去的因为实际上有一个是不存储数据的。 补还可以利用命令的方式来查看GC信息 jstat -gc 进程号 OOM
OutOfMemoryErrorOOM说明你的Java应用程序耗尽了可用的堆内存资源导致无法继续分配更多的内存。我们来做一个例子让它能够导致OOM错误并通过jvisualvm工具来查看具体情况这个工具是jdk内置含有的在Java#bin目录下。我们通过死循环去执行创建链表数组并且每次new一个对象实例。设置内存大小为500M-Xms500m -Xmx500m
public class OOMTest {public static void main(String[] args) throws InterruptedException {ListImages list new ArrayList();int i 0;while (true) {Thread.sleep(10);list.add(new Images(1024 * 1024));}}
}
class Images {private byte[] bytes;public Images(int len) {this.bytes new byte[len];}
}打开jvisualvm工具可以在概述页面看到程序的一些基础信息。通过抽样器可以查看内存中那些内容占据的大小是最多的。通过Visual GC就可以看到内存的变化直到最后会把old区域打满。这里如果没有安装Visual GC插件可以点击 工具-插件-可用插件-选择下载Visual GC
堆的内存分配
在JVM中存储的对象有的生命周期是比较短的还有一些是比较长的。Java堆划分为年轻代和老年代。年轻代又划分为Eden伊甸园、Survivor区1S0或From区和Survivor区2S1或To区幸存者区。在默认情况下年轻代和老年代的比例是1:2并且可以通过-XX:NewRation来设置它们的比例。我们跑一个Java程序设置内存大小为600M然后Visual GC工具来查看其默认比例。我们可以计算出年轻代1502525200M老年代为400M比例为1:2 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1可以通过-XX:SurvivorRatio来配置。还能够通过-XX:SurvivorRatio来设置新生代中Eden与Survivor的比例。JDK中会自动默认开启了-XX:UseAdaptiveSizePolicy自适应的内存分配策略。可以通过-Xmn来设置新生代的最大内存大小一般是不会去设置需要注意的是 几乎所有的Java对象都是在Eden区中new出来的。绝大部分的Java对象的销毁都在新生代进行。 *对象分配过程
new对象会先放在伊甸园区(Eden Space)这个区域会有大小限制。当伊甸园的空间填满时程序又需要创建对象JVM 的垃圾回收器将对伊甸园区进行垃圾回收Minor GC将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。然后将伊甸园中的剩余对象移动到幸存者 0 区年龄计数器将会被加1。如果再次发生垃圾回收上次幸存者0区的数据如果没被回收就会移动到幸存者1区每次的垃圾回收都会使得幸存者区域进行一次交换经过多次垃圾回收后如果年龄计数器的值达到了一个阈值那么就会触发Promotion(晋升)将数据移动到养老区。这个可以通过-XX:MaxTenuringThresholdN来修改
说明一下对上图的理解
第一阶段当对象实例之后会存放在Eden区直到Eden区满了之后会触发垃圾回收YGC/Minor GC将不用的对象数据回收如果还有用的就会被移动到S0区中并且年龄计数器会加1。第二阶段当Eden区再次满了之后还会触发垃圾回收剩余的对象会被移动到S1区此时S1是空的每次都会有一个空的区域要么是S0要么是S1如果S0此时的对象还不能回收那就会被移动到S1区中。第三阶段这里代表过了多次垃圾回收后的情况。当Eden区还有需要保留的对象都会放在S0/S1看交换到谁此图画的是移动到S0然而当S1也有不能销毁的数据并且年龄达到了阈值图中设置为15将会被晋升到老年代中。 *注这里需要注意的是幸存者区S0、S1在移动的时候是会进行交换的都会有一个为空。在垃圾回收方面幸存者区是不会主动触发垃圾回收的而是在伊甸园区触发垃圾回收的时候进行垃圾回收。对于垃圾回收频繁的在新生区收集很少在养老区几乎不会再永久代/元空间收集。 GC垃圾回收概念 Minor GC、Major GC、Full GC
在进行 GC 时并非每次都对堆内存新生代、老年代方法区区域一起回收的大部分时候回收的都是指新生代。针对 HotSpot VM 的实现它里面的 GC 按照回收区域又分为两大类部分收集Partial GC整堆收集Full GC。Minor GC
Minor GC也称为年轻代垃圾回收是针对年轻代Young Generation的垃圾回收操作。年轻代包括伊甸园区Eden Space和幸存者区Survivor Spaces。在Minor GC中Java虚拟机会检查并回收年轻代中不再被引用的对象通常采用标记-复制Mark and Copy或标记-整理Mark and Compact算法。Minor GC的目标是回收年轻代中的垃圾对象将存活的对象晋升到老年代。Minor GC会引发STW暂停其他用户的线程等待垃圾回收结束用户线程才会恢复运行。
Major GC或称为 “Tenured GC”
Major GC 是对老年代Old Generation的垃圾回收操作。老年代中的对象具有较长的生命周期通常经过多次Minor GC后才会被回收。Major GC通常使用标记-清除Mark and Sweep或标记-整理Mark and Compact算法。目标是回收老年代中的不再被引用的对象以释放内存。经常会伴随着至少一次的Minor GC在老年代空间不足时会先尝试触发Minor GC如果空间还是不足则触发Major GC。Major GC的速度一般会比Minor GC慢10倍以上STW的时间更长。如果垃圾回收后还是不足就会出现OOM。
Full GC
Full GC 是对整个堆内存的垃圾回收操作包括年轻代和老年代以及元空间Metaspace或永久代在Java 8之前的版本。Full GC的执行通常需要暂停整个应用程序因为它可能涉及到大量的内存回收和整理。Full GC的目标是在发现内存不足的情况下进行一次全面的内存回收以避免OutOfMemoryError。导致触发的可能有 调用了System.gc()系统建议执行Full GC但是不必然执行。老年代空间不足、方法区空间不足。通过Minor GC后进入老年代的平均大小大于老年代的可用内存。有伊甸园区、幸存者区(S0/S1)向幸存者区(S1/S0)复制时对象大于(S1/S0对应的区)可用内存则把该对象转存到老年代且老年代的可用内存小于该对象大小。 *注Full GC是开发或调优中尽量要避免的。 案例与日志分析
我们先定义一个案例死循环去给拼接字符串并且存到一个ArrayList数组中。
public class GCTest {public static void main(String[] args) {int count 0;try {// 定义数组ArrayListString list new ArrayList();String text 不断的拼接##; // 字符串存在堆空间while (true) {list.add(text);text count text;count;}} catch (Exception e) {e.printStackTrace();System.out.println(遍历次数: count);}}
}这里需要配置VM Options设置为-Xms9m -Xmx9m -XX:PrintGCDetails把空间设置小一点这样就能狗明显看到GC日志。
[GC (Allocation Failure) [PSYoungGen: 2040K-488K(2560K)] 2040K-943K(9728K), 0.0006030 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K-504K(2560K)] 2991K-1961K(9728K), 0.0005728 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2552K-512K(2560K)] 4009K-3178K(9728K), 0.0004624 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2559K-512K(2560K)] 5225K-4140K(9728K), 0.0004935 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2560K-512K(2560K)] 6188K-5163K(9728K), 0.0004562 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2560K-512K(1536K)] 7211K-6191K(8704K), 0.0004587 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1536K-960K(2048K)] 7215K-6991K(9216K), 0.0003252 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 960K-0K(2048K)] [ParOldGen: 6031K-6073K(7168K)] 6991K-6073K(9216K), [Metaspace: 3311K-3311K(1056768K)], 0.0047554 secs] [Times: user0.00 sys0.00, real0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1024K-672K(2048K)] 7097K-6745K(9216K), 0.0003614 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 672K-0K(2048K)] [ParOldGen: 6073K-6457K(7168K)] 6745K-6457K(9216K), [Metaspace: 3311K-3311K(1056768K)], 0.0111496 secs] [Times: user0.00 sys0.00, real0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 1021K-0K(2048K)] [ParOldGen: 6457K-6928K(7168K)] 7478K-6928K(9216K), [Metaspace: 3311K-3311K(1056768K)], 0.0161660 secs] [Times: user0.00 sys0.00, real0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 1024K-492K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7952K-7421K(9216K), [Metaspace: 3313K-3313K(1056768K)], 0.0011506 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1024K-756K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7952K-7685K(9216K), [Metaspace: 3320K-3320K(1056768K)], 0.0012778 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K-886K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7951K-7814K(9216K), [Metaspace: 3320K-3320K(1056768K)], 0.0010096 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1020K-946K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7948K-7874K(9216K), [Metaspace: 3320K-3320K(1056768K)], 0.0009717 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1017K-976K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7945K-7904K(9216K), [Metaspace: 3320K-3320K(1056768K)], 0.0109962 secs] [Times: user0.00 sys0.00, real0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K-996K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7951K-7924K(9216K), [Metaspace: 3320K-3320K(1056768K)], 0.0013137 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1015K-1006K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7944K-7935K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0013365 secs] [Times: user0.02 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1021K-1006K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7950K-7935K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0011589 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1016K(2048K)] [ParOldGen: 6928K-6928K(7168K)] 7947K-7945K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0012006 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K-1007K(2048K)] [ParOldGen: 7164K-7063K(7168K)] 8187K-8070K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0010010 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7063K-7063K(7168K)] 8083K-8080K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009194 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1007K(2048K)] [ParOldGen: 7165K-7124K(7168K)] 8184K-8131K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009735 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7124K-7114K(7168K)] 8143K-8131K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009866 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7165K-7144K(7168K)] 8184K-8162K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009173 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7165K-7155K(7168K)] 8184K-8172K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0008445 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7165K-7155K(7168K)] 8184K-8172K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009346 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7165K-7165K(7168K)] 8184K-8182K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0009103 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 1017K-1017K(2048K)] [ParOldGen: 7165K-7147K(7168K)] 8182K-8165K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0166929 secs] [Times: user0.00 sys0.00, real0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7157K-7147K(7168K)] 8177K-8165K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0011293 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1019K-1017K(2048K)] [ParOldGen: 7157K-7157K(7168K)] 8177K-8175K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0010992 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 1017K-1017K(2048K)] [ParOldGen: 7157K-7157K(7168K)] 8175K-8175K(9216K), [Metaspace: 3325K-3325K(1056768K)], 0.0010983 secs] [Times: user0.00 sys0.00, real0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1021K-0K(2048K)] [ParOldGen: 7167K-731K(7168K)] 8189K-731K(9216K), [Metaspace: 3344K-3344K(1056768K)], 0.0029345 secs] [Times: user0.00 sys0.00, real0.00 secs]
HeapPSYoungGen total 2048K, used 29K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 1024K, 2% used [0x00000000ffd00000,0x00000000ffd077a0,0x00000000ffe00000)from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 7168K, used 731K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 10% used [0x00000000ff600000,0x00000000ff6b6c70,0x00000000ffd00000)Metaspace used 3356K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 368K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread main java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOfRange(Arrays.java:3664)at java.lang.String.init(String.java:207)at java.lang.StringBuilder.toString(StringBuilder.java:407)at com.lyd.testboot.jvm.GCTest.main(GCTest.java:19)我们来分析以上日志首先会将对象存放在伊甸园区但是伊甸园区直接爆满就会触发YGC(Minor GC)第一行日志中
[GC (Allocation Failure) [PSYoungGen: 2040K-488K(2560K)] 2040K-943K(9728K), 0.0006030 secs] [Times: user0.00 sys0.00, real0.00 secs] 第一部分[PSYoungGen: 2040K-488K(2560K)]2040K代表GC之前新生代占用的内存大小488K代表GC之后新生代占用的内存大小这里没有变0是因为还有一些数据在幸存者区(2560K)是记录着新生代的总空间大小。第二部分2040K-943K(9728K)2040K是堆空间在垃圾回收前占用的大小943K是堆空间垃圾回收之后占用的大小(9728K)是堆空间的总大小。再看第七行日志
[GC (Allocation Failure) [PSYoungGen: 1536K-960K(2048K)] 7215K-6991K(9216K), 0.0003252 secs] [Times: user0.00 sys0.00, real0.00 secs] 很明显7215K-6991K(9216K)堆空间的占用却变大了这是因为数据放大了老年代里面了。再看第八行日志
[Full GC (Ergonomics) [PSYoungGen: 960K-0K(2048K)] [ParOldGen: 6031K-6073K(7168K)] 6991K-6073K(9216K), [Metaspace: 3311K-3311K(1056768K)], 0.0047554 secs] [Times: user0.00 sys0.00, real0.00 secs] 随着放到老年代的数据增多老年代的空间也就不足了就会触发Full GC触发之后[PSYoungGen: 960K-0K(2048K)]新生代就被清空掉了。而元数据区[Metaspace: 3311K-3311K(1056768K)]是没有变化的因为没有涉及到内存卸载的情况。到最后一次GC日志
[Full GC (Ergonomics) [PSYoungGen: 1021K-0K(2048K)] [ParOldGen: 7167K-731K(7168K)] 8189K-731K(9216K), [Metaspace: 3344K-3344K(1056768K)], 0.0029345 secs] [Times: user0.00 sys0.00, real0.00 secs] 之后就内存不足了也伴随着就会报OutOfMemoryError错误了。
*内存分配策略
这部分主要的是对象在伊甸园区创建并经过第一次Minor GC之后依然存活并且能被幸存者区容纳将被移动到幸存者空间之中并且将对象年龄设置为1。对象在经过每次的Minor GC之后还存活年龄就会增加1当年龄达到一定的阈值默认是15每个JVM、每个GC都会有所不同就会被晋升到老年代。修改这个阈值可以通过-XX:MaxTenuringThreshold来设置。对于不同的年龄段对象
优先分配到Eden大对象会直接分配到老年代长期存活的对象会分配到老年代动态对象年龄判断如果幸存者区中相同年龄的所有对象大小的总和大于幸存者区空间的一半年龄大于或者等于该年龄的对象可以直接进入老年代。
我们通过一个例子来看如果幸存者空间不足以存放对象对象将会放在老年代。
public class YoungOldAreaTest {public static void main(String[] args) {byte[] bytes new byte[1024 * 1024 * 20]; // 20m}
}在运行VM Option配置中设置参数-Xms60m -Xmx60m -XX:NewRatio2 -XX:SurvivorRatio8 -XX:PrintGCDetails运行之后我们可以看到都没有执行过Minor GC就已经将对象放在老年代了。
HeapPSYoungGen total 18432K, used 3300K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)eden space 16384K, 20% used [0x00000000fec00000,0x00000000fef39010,0x00000000ffc00000)from space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)to space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000)ParOldGen total 40960K, used 20480K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)object space 40960K, 50% used [0x00000000fc400000,0x00000000fd800010,0x00000000fec00000)Metaspace used 3327K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 365K, capacity 388K, committed 512K, reserved 1048576K因为后面参数带了-XX:PrintGCDetails如果有执行垃圾回收将会被打印出来。
TLAB(Thread-Local Allocation Buffer)
TLABThread-Local Allocation Buffer是一种用于提高多线程并发应用程序性能的内存分配优化技术。TLAB是专门为每个线程分配的小内存区域用于分配对象实例。它的主要目的是减少线程之间的竞争和锁争用以提高对象分配的效率。
什么是TLAB
虽然堆是线程共有的但里面含有一个线程私有的TLAB。从内存模型来看它是对Eden区继续细分的一块区域JVM 为每个线程分配了一个私有缓存区域。在多线程同时分配内存时使用TLAB能够避免一些非线程安全的问题同时还能够提升内存分配的吞吐量这种内存分配也称为快速分配策略。
为什么要有TLAB
堆区是线程共享的任何线程都可以访问到堆区中的共享数据。由于对象实例的创建在 JVM 中非常频繁因此在并发环境下从堆区中划分内存空间是线程不安全的。为避免多个线程操作同一地址需要使用加锁等机制进而影响分配速度 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存但 JVM 确实是将 TLAB 作为内存分配的首选。 通过-XX:UseTLAB设置是否开启TLAB空间。 默认情况下TLAB 空间的内存非常小仅占有整个 Eden 空间的 1%我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。 一旦对象在 TLAB 空间分配内存失败时JVM 就会尝试着通过使用加锁机制确保数据操作的原子性从而直接在 Eden 空间中分配内存。 有TLAB对象的分配过程
我们再来回顾一下对象分配过程当对象在伊甸园区创建之后首先会分配到TLAB如果TLAB分配得下就会进行对象实例化如果分配不下就会在伊甸园区分配在里面的一系列分配规则同此文上面所描述的过程。
*逃逸分析(Escape Analysis)
逃逸分析Escape Analysis是一种编译器和运行时优化技术用于确定在程序中创建的对象是否会逃逸出它们创建的方法或作用域也就是说它们是否会在方法的外部被引用或访问。逃逸分析的主要目标是帮助编译器进行更有效的优化减少内存分配和提高程序的性能。是一种可以有效减少Java程序中同步负载和内存分配压力的跨函数全局数据流分析算法。
逃逸分析的基本行为
逃逸分析的基本行为就是分析对象动态作用域
当一个对象在方法中被定义后对象只在方法内部使用则认为没有发生逃逸。当一个对象在方法中被定义后它被外部方法所引用则认为发生逃逸。例如作为调用参数传递到其他地方中称为方法逃逸。
我们可以通过以下代码来了解。
public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb;
}如以上代码StringBuffer sb是一个方法内部变量上述代码中直接将sb返回这样这个StringBuffer 有可能被其他方法所改变这样它的作用域就不只是在方法内部虽然它是一个局部变量但是其能够逃逸到了方法外部。甚至还有可能被外部线程访问到譬如赋值给类变量或可以在其他线程中访问的实例变量称为线程逃逸。如果要防止它逃逸出去可以在返回的时候转成String对象。
public static String createStringBuffer(String s1, String s2) {StringBuffer sb new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();
}参数设置
在JDK1.7之后Hotspot就默认开启了逃逸分析。可以通过-XX:DoEscapeAnalysis显式开启逃逸分析通过-XX:PrintEscapeAnalysis查看逃逸分析结果。 *注在开发中能使用局部变量就不要使用在方法外定义。 逃逸分析优化
开启逃逸分析后编译程序能对代码进行优化减少内存分配和提高程序的性能。
栈上分配如果逃逸分析确定一个对象不会逃逸出方法或作用域编译器可以选择在栈上分配这个对象而不是在堆上。栈上分配的对象具有较短的生命周期不需要垃圾回收因此可以提高程序的性能。同步消除逃逸分析可以确定某些对象只能从一个线程访问到不会被多个线程访问从而可以避免不必要的同步操作提高多线程程序的性能。分离对象或标量替换有的对象可能不需要作为一个连续的内存结构存在也可以被访问到那么对象的部分或全部可以不存储在内存而存储在 CPU 寄存器中。 逃逸分析是一种复杂的分析技术通常由编译器和运行时系统共同实施。编译器会在编译阶段分析代码以确定对象的逃逸情况然后生成更有效的代码。在Java虚拟机中逃逸分析通常会结合垃圾回收策略和即时编译技术来实现 1) 代码优化-栈上分配
JIT 编译器在编译期间根据逃逸分析的结果发现如果一个对象并没有逃逸出方法的话就可能被优化成栈上分配。分配完成后继续在调用栈内执行最后线程结束栈空间被回收局部变量对象也被回收。这样就无需进行垃圾回收了。 常见栈上分配的场景成员变量赋值、方法返回值、实例引用传递 我们通过案例来看一下执行时间首先我们先准备一个类在主线程中循环1000万次每次调用allocation()方法此方法会new一个对象这个对象是不会发生逃逸的。
public class StackAllocation {public static void main(String[] args) {long start System.currentTimeMillis();// 循环10000000次去执行allocation相当于一直创建对象for (int i 0; i 10000000; i) {allocation();}long end System.currentTimeMillis();System.out.println(时间消耗 (end - start) ms);try {Thread.sleep(1000000);} catch (InterruptedException e) {throw new RuntimeException(e);}}private static void allocation() {// 局部变量Show show new Show();}static class Show {}
}刚开始我们在VM Options配置-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:PrintGCDetails先把逃逸分析关闭。运行之后我们到Visual VM查看抽样器可以看到这个Show对象实例达到了1000万个。运行结束后我们看一下控制台时间消耗达到了92ms。
时间消耗92 ms
HeapPSYoungGen total 305664K, used 241173K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)eden space 262144K, 92% used [0x00000000eab00000,0x00000000f9685588,0x00000000fab00000)from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)Metaspace used 9131K, capacity 9472K, committed 9728K, reserved 1058816Kclass space used 1069K, capacity 1161K, committed 1280K, reserved 1048576K接下来我们把逃逸分析打开-Xmx1G -Xms1G -XX:DoEscapeAnalysis -XX:PrintGCDetails可以看到实例就没那么多了。运行结束看看控制台时间就经历了2ms。所以可以看到有了逃逸分析在栈上分配会提高性能。
时间消耗2 ms
HeapPSYoungGen total 305664K, used 89129K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)eden space 262144K, 34% used [0x00000000eab00000,0x00000000f020a518,0x00000000fab00000)from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)Metaspace used 9245K, capacity 9600K, committed 9984K, reserved 1058816Kclass space used 1069K, capacity 1161K, committed 1280K, reserved 1048576K2) 代码优化-同步消除
线程同步的代价是相当高的同步的后果是降低并发性和性能。在动态编译同步块的时候JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否能够被一个线程访问而没有被发布到其他线程。如果没有那么 JIT 编译器在编译这个同步块的时候就会取消对这个代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫做同步省略也叫锁消除。我们看以下代码其中对one对象进行了加锁但是one对象的生命周期只有在fun()方法里面并不会被其他线程访问所以在JIT编译阶段就会被优化。
public void fun() {Object one new Object();synchronized (one) {System.out.println(one);}
}会被优化成以下代码片段
public void fun() {Object one new Object();System.out.println(one);
}3) 代码优化-标量替换
标量Scalar 是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。相对的那些的还可以分解的数据叫做聚合量Aggregate Java 中的对象就是聚合量因为其还可以分解成其他聚合量和标量。在 JIT 阶段通过逃逸分析确定该对象不会被外部访问并且对象可以被进一步分解时JVM 不会创建该对象而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。这个过程就是标量替换。 通过-XX:EliminateAllocations可以开启标量替换默认是开启的允许将对象打散分配到栈上-XX:PrintEliminateAllocations查看标量替换情况。 我们通过一段代码来理解同样的例子只是我们内部有个Work类里面有一个workId和workName字段并且来查看统计消耗时间首先我们在VM Options中配置-Xmx100m -Xms100m -XX:DoEscapeAnalysis -XX:PrintGC -XX:-EliminateAllocations先不开启标量替换。
public class ScalarReplace {public static void main(String[] args) {long start System.currentTimeMillis();// 循环10000000次去执行allocation相当于一直创建对象for (int i 0; i 10000000; i) {allocation();}long end System.currentTimeMillis();System.out.println(时间消耗 (end - start) ms);}private static void allocation() {// 局部变量Work work new Work(); // 未发生逃逸work.workId 1;work.workName working;}static class Work {public int workId;public String workName;}
}我们可以看一下运行结果很明显能够看到对象分配会经历GC垃圾回收消耗时间为35ms
[GC (Allocation Failure) 25600K-1152K(98304K), 0.0009344 secs]
[GC (Allocation Failure) 26752K-1024K(98304K), 0.0005531 secs]
[GC (Allocation Failure) 26624K-976K(98304K), 0.0005057 secs]
[GC (Allocation Failure) 26576K-1040K(98304K), 0.0005935 secs]
[GC (Allocation Failure) 26640K-1008K(98304K), 0.0004587 secs]
[GC (Allocation Failure) 26608K-1072K(100864K), 0.0005802 secs]
[GC (Allocation Failure) 31792K-813K(100864K), 0.0005920 secs]
[GC (Allocation Failure) 31533K-813K(100864K), 0.0002328 secs]
时间消耗35 ms当我们把标量替换打开我们可以看到就没有出现垃圾回收所消耗的时间也就小很多。
时间消耗2 ms逃逸分析的效果取决于具体的代码和编译器实现不是所有的逃逸都可以被消除但它可以在一些情况下提供显著的性能提升特别是在高性能和低延迟应用程序中。 堆空间的参数设置总结
这里来整理一下堆空间的一些参数设置 -XX:PrintFlagsInitial查看所有的参数的默认初始值。-XX:PrintFlagsFinal查看所有的参数的最终值。-XX:PrintGCDetails输出详细的GC处理日志。 简要信息-XX:PrintGC or -verbose:gc -Xms初始化堆空间内存默认物理内存的1/64-Xmx设置最大堆空间内存默认物理内存的1/4-Xmn设置新生代的大小初始值即最大值。-XX:NewRatio配置新生代和老年代在堆结构的占比。-XX:SurvivorRatio设置新生代Eden和S0/S1空间比例。-XX:MaxTenuringThreshold设置新生代垃圾的最大年龄。-XX:HandlePromotionFailure是否设置空间分配担保-XX:DoEscapeAnalysis开启逃逸分析开启-关闭-XX:EliminateAllocations开启标量替换开启-关闭-Server启动server模式开启了才能使用逃逸分析默认开启 总结
本次学习了堆的结构以及对象的分配过程了解新生代、老年代以及永久代元数据区各自结构以及作用对象分配优先经过那些地方以及对Minor GC、Major GC、Full GC三种GC的使用和触发阶段。也透过了逃逸分析来了解三个优化代码方式学到了几种参数配置的使用。
资料
JVM中新生代为什么要有两个Survivorform,to - 知乎
创作不易可能有些语言不是很通畅如有错误请指正感谢观看记得点赞哦