文章目录

  • ​​前言​​
  • ​​一、从面试题出发​​
  • ​​二、JVM体系结构与组成成分​​
  • ​​1. jvm 位置​​
  • ​​2. 体系结构​​
  • ​​3. 上下四部分​​
  • ​​3.1 类装载器ClassLoader​​
  • ​​3.2 Execution Engine 执行引擎​​
  • ​​3.3 Native Interface本地接口​​
  • ​​4. 运行时数据区​​
  • ​​4.1 本地方法栈:Native Method Stack​​
  • ​​4.2. 程序计数器:program counter register​​
  • ​​4.3. java 栈:java stack​​
  • ​​4.4 方法区:Method Area​​
  • ​​4.5 堆:heap(java 7之前)​​
  • ​​4.6 堆:heap(java 8)​​
  • ​​三、堆内存 调优参数​​
  • ​​1. 查看:堆内存初始化信息​​
  • ​​2. VM 设置参数​​
  • ​​2.1 设置:堆内存初始信息​​
  • ​​2.2 设置:新生代中三个区、新生代与老年代的各个比例​​
  • ​​2.3 设置:栈的大小​​
  • ​​2.4 设置:方法区内存​​
  • ​​2.5 设置:直接内存 配置​​
  • ​​2.3 Java性能分析神器:MAT 和 Jprofiler​​
  • ​​四、垃圾回收​​
  • ​​1. 引用计数法​​
  • ​​2. 标记清除法​​
  • ​​3. 复制算法(目前新生代使用)重点​​
  • ​​4. 标记压缩法(目前老生代使用)重点​​
  • ​​5. 分代算法​​
  • ​​6. 分区算法​​
  • ​​五. 再谈 堆内存参数 设置​​
  • ​​1. 测试在Eden区的对象​​
  • ​​2. 说一下垃圾收集器​​
  • ​​3. 设置:对象经过多少GC的次数进入老年代,默认15​​
  • ​​4. 设置:进入老年代的对象大小​​
  • ​​4.1 注意TLAB区域​​

前言

为面试而准备,学习java虚拟机。

主要是学习JDK7,JDK8与 7 仅有少量不同之处。

此文章是看了视频和很多博客才写出来的。

一、从面试题出发

  1. java虚拟机的内存体系结构。
  2. 请谈谈你对JVM的理解?java8版有什么了解?
  3. 谈谈JVM中你对ClassLoader类加载器的认识?
  4. 什么是OOM?写代码使得分别出现StackOverflowError和OutOfMemoryError
  5. JVM的常用参数调优你了解吗?
  6. 内存快照抓取和MAT分析hprof文件干过吗?

二、JVM体系结构与组成成分

1. jvm 位置

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jvm


JVM是运行在操作系统之上的,它与硬件没有直接的交互

2. 体系结构

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jvm_02

如图所示:

最下面:虚拟机的最底层,是 ​​执行引擎​​​和​​本地方法接口​​​,在往下,就是 本地方法调用的C++。
最上面:虚拟机的最上层,是 ​​​类装载器子系统​​​ 加载 ​​.class 的字节码文件​​​到 java虚拟机 运行内存中。
中间:虚拟机的 运行时数据区,包括5个:

  1. ​堆、方法区​​ 属于线程共享。
  2. ​java 栈、本地方法栈、程序计数器​​ 属于线程私有。

下面就从图中的 8部分开始叙述:

3. 上下四部分

3.1 类装载器ClassLoader

负责加载 ​​class 字节码​​文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定

ClassLoader 加载 类的过程图:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_03


虚拟机自带的加载器:

  1. 启动类加载器(Bootstrap)C++ (爷爷辈)
  2. 扩展类加载器(Extension)Java (爸爸辈)
  3. 应用程序类加载器(App)Java,也叫系统类加载器,加载当前应用的classpath的所有类 (儿子辈)

用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

示意图:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_java_04

3.2 Execution Engine 执行引擎

​Execution Engine执行引擎​​​ 负责解释命令,提交 ​​操作系统​​ 执行。

3.3 Native Interface本地接口

  • Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
  • 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为Native的代码,它的具体做法是Native Method Stack中登记Native方法,在Execution Engine 执行时加载Native libraries。
  • 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等等,不多做介绍。
  • ​Native Interface本地方法接口​​​ 就在​​数据运行区​​​ 中的 ​​本地方法栈​​ 中。

4. 运行时数据区

4.1 本地方法栈:Native Method Stack

它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

4.2. 程序计数器:program counter register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来**存储指向下一条指令的地址,也即将要执行的指令代码**),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

4.3. java 栈:java stack

  • ​栈也叫栈内存​​​,主管Java程序的运行,是在​​线程创建时创建​​,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题。
  • 只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配。

出现的异常:

Exception in thread “main” java.lang.StackOverflowError

4.4 方法区:Method Area

  1. ​方法区是线程共享​​的。
  2. 通常用来保存装载的类的元结构信息
    比如:​​运行时常量池​​+​​静态变量​​+​​常量​​+​​字段​​+​​方法字节码​​+ ​​在类/实例/接口初始化用到的特殊方法​​等。
  3. 通常和 ​​永久区​​ 关联在一起(Java7之前),但具体的跟JVM的实现和版本有关。

4.5 堆:heap(java 7之前)

  1. ​堆是线程共享的。​
  2. 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
  3. 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

堆内存​​逻辑​​上分为三部分:新生+养老+永久

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jvm_05


4. 新生区

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: ​​伊甸区​​​(Eden space)和​​幸存者区​​(Survivor pace) 。

​所有的类都是在伊甸区被new出来的​​。

幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。

  1. 过程,新生区->养老区->永久区
    当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区.若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生​​OOM异常“OutOfMemoryError”。​如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
    (1)Java虚拟机的堆内存设置不够,可以通过参数​​-Xms、-Xmx​​来调整。
    (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
  2. 逻辑上,堆包括新生区、养老区、永久区。
    实际上,堆只包括新生区、养老区。

堆的示意图:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_06

4.6 堆:heap(java 8)

JDK 1.8之后将最初的​​永久代取消​​​了,​​由元空间取代。​

堆的示意图:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jdk_07

三、堆内存 调优参数

1. 查看:堆内存初始化信息

查看本机 分配给 jvm虚拟机内存的 初始值大小​可用大小​​最大值大小​

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_Java_08


代码测试: 以下测试都在本类中测试

本机内存 为 8G。

package com.feng.jvm;

import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

public class Jvm {

@Test
public void test(){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
long freeMemory = Runtime.getRuntime().freeMemory() ;//返回 Java 虚拟机中的内存总量。
System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("FREE_MEMORY = " + freeMemory + "(字节)、" + (freeMemory / (double)1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
System.out.println("还打印了堆内存的数据:");
}
}

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_Java_09


测试可看出:

最大内存 :18014 约为 8G
总内存:123
64 约为 8G

2. VM 设置参数

2.1 设置:堆内存初始信息

在VM 中设置参数: 后在进行测试,后在后面打印出 详细的 GC 日志

VM参数:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_java_10


vm参数详解:

/**
* 第一次配置: -Xms1024m -Xmx1024m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
* -Xms1024m: 设置堆内存初始值 为1024M
* -Xmx1024m: 设置堆内存最大值 为1024M
* -XX:+PrintGCDetails: 打印 GC 详细信息
* -XX:+UseSerialGC:
* 使用 串行回收器 进行回收,这个参数会使新生代和老年代都使用串行回收器,
* 新生代使用复制算法,老年代使用标记-整理算法。
* Serial 收集器是最基本、历史最悠久的收集器,它是一个 单线程收集器。
* 一旦回收器 开始运行时,整个系统都要停止。
* client 模式下默认开启,其他模式默认关闭。
* -XX:+PrintCommandLineFlags: 打印 vm 的配置,并有显示其值 ,打印的第一行就是这个参数的原因
*/

java代码测试:

@Test
public void test01(){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
long freeMemory = Runtime.getRuntime().freeMemory() ;//返回 Java 虚拟机中的内存总量。
System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("FREE_MEMORY = " + freeMemory + "(字节)、" + (freeMemory / (double)1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
System.out.println("还打印了堆内存的数据:");
}

java 8:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jdk_11


java 7:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jvm_12

2.2 设置:新生代中三个区、新生代与老年代的各个比例

模拟一个 ​​内存溢出​​ 的情况。来查看GC信息。(上面是因为内存够用,GC回收正常,所以没打印)

IDEA 设置参数位置:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_Java_13


VM 配置参数:

/**
* 第一次配置 eden 2 = from 1 + to 1
* -Xms8m -Xmx8m -Xmn5m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=2这个参数出错
* -Xmn1m : 设置 新生代空间 为 5M
* -XX:SurvivorRatio=2: 设置新生代中伊甸园区(Eden) Eden / from = 2:1 Eden / to = 2:1
*
* 第二次配置
* -Xms20m -Xmx20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
*
* 第三次配置
* -Xms20m -Xmx20m -Xmn1m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
* -XX:NewRatio=2 : 表示 老年代 / 新生代 = 2 :1
*/

Java测试代码:

byte[] b = new byte[1*1024*1024]; // 1MB
@Test
public void test03(){
List<Jvm> list = new ArrayList<>();
try {
for (int i = 0; i<40; i++){
list.add(new Jvm());
}
} catch (Exception e){
e.printStackTrace();
}
}
  1. 第一次配置的日志:

    分析:
    打印出GC信息。
    并曝出内存溢出异常,java heap space。
    在堆内存信息中可看出:

eden/from = eden/to 约等于 2:1
eden + from + to 约等于 5M

  1. 第二次配置的日志:
  2. JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_java_14

  3. 分析:
    与第一次配置相同,仅改变 初始大小。内存设未20,可以满足代码所需的10M,这里不报错。
  4. 第三次配置的日志:
  5. JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_15

  6. 分析:
    老年代与新生代的比值 和配置的相同,为 2: 1

2.3 设置:栈的大小

一般不配置,Java 给优化好了,了解即可
java虚拟机提供了参数 -Xss 来指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度:
参数详解:

/**
* 设置线程中栈的大小,无最大最小值,确定的一个值
* -Xss1m : 设置栈大小为 1M
* -Xss5m : 设置栈大小为 5M
*/

代码测试:

private static int count = 0;
public static void recursion(){
count++;
recursion();
}
@Test
public void test04(){
try {
recursion();
}catch (Exception e){
System.out.println("调用的最大深度:"+count);
System.out.println("error:"+e.getLocalizedMessage());
}
}

日志分析:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_16

2.4 设置:方法区内存

一般不配置,Java 给优化好了,了解即可
和java 堆一样,方法区 是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置,在默认情况下,-XX:MaxPersSize 为 64MB,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以避免永久区内存溢出的问题。

参数详解

-XX:PermSize=64M -XX:MaxPermSize=64M
-XX:PermSize=64M : 初始化大小
-XX:MaxPermSize=64M : 最大的值

2.5 设置:直接内存 配置

一般不配置,Java 给优化好了,了解即可
直接内存也是java程序中非常重要的组成部门,特别是广泛用在NIO中,直接内存跳过了java堆,使java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度,但是说直接内存一定就可以提高内存访问速度也不见得,具体情况具体分析。

相关配置参数:-XX:MaxDirectMemorySize。如果不设置,默认值为最大堆空间,即-Xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效的释放空间,也会引起系统的OOM。

参数详解

-XX:MaxDirectMemorySize

2.3 Java性能分析神器:MAT 和 Jprofiler

分析java 内存性能需要工具,eclipse 是 MAT(Eclipse Memory Analyzer Tool)
Idea 就是 Jprofiler。
现在经常用的工具是 IDEA,这里我也没学过 MAT,我就学习Jprofiler了。

四、垃圾回收

  1. 垃圾回收(Garbage Collection),简称 GC。
  2. GC中的垃圾,是特指存于内存中、不会再被使用的对象,而 回收就是相当于把垃圾“倒掉”。
  3. 这里的内存就是指 ​​堆内存​​​(一般内存都是指堆内存),​​对象被new出来时,一般存放在堆中​​。
  4. 垃圾回收有很多算法:如引用计数法、标记压缩法、复制算法、分代、分区的思想。

1. 引用计数法

​引用计数法​​:这是个比较古老而经典的垃圾收集算法,其核心就是在对象被其他所引用时计数器加1,而当引用失效时则减1,但是这种方式有非常严重的问题:无法处理循环引用的情况、还有就是每次进行加减操作比较浪费系统性能。

2. 标记清除法

​标记清除法​​:就是分为 标记 和 清除 两个阶段进行处理内存中的对象,当然这种方式也有非常大的弊端,就是 空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。

3. 复制算法(目前新生代使用)重点

​复制算法​​:其核心思想就是将内存空间分为两块,,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之; 前正在使用的内存块中所有的对象,反复去交换俩个内存的角色,完成垃圾收集。(java中新生代的from和to空间就是使用这个算法)

4. 标记压缩法(目前老生代使用)重点

​标记压缩法​​:标记压缩法 在标记清除法基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。(java中老年代使用的就是标 记压缩法)

考虑一个问题:为什么新生代和老年代使用不同的算法?
因为新生代的对象死得快,被销毁的快,而经过15次的销毁后还存留下来的就放在 老年代。

5. 分代算法

​分代算法​​:就是根据对象的特点把内存分成N块,而后根据每个内存的特点使用不同的算法。

对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时都很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC.

6. 分区算法

​分区算法​​:其主要就是 将整个内存分为N多个小的独立空间,每个小空间都可以独立使用,这样细粒度的控制一次回收都少个小空间和那些个小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时间。

​垃圾回收器的任务​​是 识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态。停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿 保证了系统状态在某-一个瞬间的一致性,也有益于更好低标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。

五. 再谈 堆内存参数 设置

更细粒度的设置 堆内存参数,
主要针对 堆内存中的 新生区和 老年区的参数设置。

1. 测试在Eden区的对象

测试代码:

/**
* 初始的对象 在 Eden区
* PSYoungGen : 自带使用默认的 GC 收集器
* 申请 5M 空间
*/
@Test
public void test05(){
// 初始的对象 在 Eden区
// 参数 :-Xmx64m -Xms64m -XX:+PrintGCDetails
// PSYoungGen : 自带使用JDK默认自带的 GC 收集器 ,
// 原来设置的是 UseSerialGC :这是串行垃圾收集器
// 申请 5M 空间
for (int i = 0; i <5; i++){
byte[] b = new byte[1024*1024]; // 1M
}
// 老年区的使用为1%,几乎没有,所以所有的对象都放在了新生代。
}

日志分析:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jvm_17

2. 说一下垃圾收集器

默认使用的是 PSYoungGen 收集器,这是JDK自带的。
上面示例有时候设置参数为:-XX:+UseSerialGC,这是使用 串行收集器。

3. 设置:对象经过多少GC的次数进入老年代,默认15

一般而言对象首次创建会被放置在新生代的Eden区,如果没有GC介入,则对象不会离开Eden区,那么Eden区的对象如何进入老年代?一般来讲,只要对象的年龄达到一定的大小,就会自动离开年轻代进入老年代,对象年龄是由对象经历数次GC决定的,在新生代每次GC之后如果对象没有被回收则年里加1,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升老年代。

参数分析:

-XX:MaxTenuringThreshold,默认情况下为15,。//指定新生代对象经过多少次回收后进入老年代。

测试代码:

/**
* 测试进入老年代的对象
* 参数: -Xms=1024M -Xmx=1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails
* -XX:+PrintHeapAtGC
* -XX:MaxTenuringThreshold=15 : 对象进入 老年代 的次数是 15(即 对象经历了 15次的GC过滤)
*/
@Test
public void test06(){
// map 的意义在于,对象一直存在。都放在map中
Map<Integer, byte[]> map = new HashMap<>();
for (int i = 0; i<5; i++){
byte[] b = new byte[1024*1024]; // 1M
map.put(i, b);
}
// 申请 6G 内存
for (int k = 0; k< 20; k++){ // 6G 的内存
for (int j = 0; j <300; j++){
byte[] b = new byte[1024*1024]; // 1M
}
}
}

日志分析:

事与愿违啊!!!

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_18

4. 设置:进入老年代的对象大小

另外,大对象(新生代Eden区无法装入时,也会直接进入老年代)。JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接晋升老年代。

-XX:PretenureSizeThreshold      :可以指定进入老年代的对象大小,

可以指定进入老年代的对象大小,但是要注意TLAB区 域优先 分配空间。

代码测试、参数分析:

/**
* 第一次配置参数:-Xms30M -Xmx30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000
* 这种现象原因为:虚拟机对于体积不大的对象 会有限把数据分配到 TLAB 区域中,因此就失去了在老年代分配的机会
* 第二次配置参数:-Xms30M -Xmx30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB
* -XX:PretenureSizeThreshold=1000 : 当对象的内存大于 1000k时,就直接扔进 老年区。前提得禁用 TLAB 区域,小于 1000k 的话,会进入新生区中的伊甸园区
* -XX:-UseTLAB : 禁用TLAB 区域
*/
@Test
public void test07(){
Map<Integer, byte[]> map = new HashMap<>();
for (int i = 0; i<5*1024; i++){ // 5M 空间
byte[] b = new byte[1024]; // 1k 这里的1024 》1000 ,所以这里的对象都进入 老年代
map.put(i, b);
}
// 注意 TLAB 区域
}

日志分析:

第一次配置参数:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_java_19

**第二次配置参数: 比第一次配置 多了一个配置::-XX:-UseTLAB 就是禁用 TLAB 区域 **

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_堆内存_20


加了-XX:-UseTLAB 参数后,老年区的使用率变大。

4.1 注意TLAB区域

  1. TLAB全称是Thread Local Allocation Buffer即线程本地分配缓存,从名字上看是一一个线程专用的内存分配区域,是为了加速对象分配而生的。每- -个线程都会产生-一个TLAB,该线程独享的工作区域,java虚拟机使用这种
  2. TLAB区来避免多线程冲突问题,提高了对象分配的效率。TLAB空间- -般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上。
-XX:+UseTLAB使用TLAB
-XX:+TLABSize设置TLAB大小
-XX:TLABRefilWasteFraction设置维护进入TLAB空间的单个对象大小,他是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆创建对象。
-XX:+PrintTLAB查看TLAB信息
-XX:ResizeTLAB自调整TLABRefilWasteFraction阀值。

代码测试、参数分析:

/**
* TLAB 分配
* 参数: -XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGC -XX:TLABSize=102400 -XX:-ResizeTLAB -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis -server
* -XX:+UseTLAB :开启TLAB,默认是开启的
* -XX:+PrintTLAB : 打印 TLAB 信息
* -XX:TLABSize=102400 : 设置 TLAB大小 1000k
* -XX:-ResizeTLAB : 禁用 自动调整大小
* -XX:TLABRefillWasteFraction=100
* -XX:-DoEscapeAnalysis : 打印 TLAB 区的详细信息(本身是关闭的。现在把关闭 给关掉就是开启啦)
* -server :
*/
@Test
public void test08(){
long start = System.currentTimeMillis();
for (int i = 0; i< 10000000; i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println("使用的时间:"+(end-start));
}
public void alloc(){
byte[] b = new byte[2];
}

日志分析:

JVM虚拟机学习:JDK7内存体系结构、堆内存参数调优详解,JDK8的改变细节_jdk_21