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

前言

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

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

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

一、从面试题出发

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

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

1. jvm 位置

在这里插入图片描述
JVM是运行在操作系统之上的,它与硬件没有直接的交互

2. 体系结构

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。
在这里插入图片描述

如图所示:

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

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

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

3. 上下四部分

3.1 类装载器ClassLoader

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

ClassLoader 加载 类的过程图:
在这里插入图片描述
虚拟机自带的加载器:

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

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

示意图:
在这里插入图片描述

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. 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

堆内存逻辑上分为三部分:新生+养老+永久
在这里插入图片描述
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. 逻辑上,堆包括新生区、养老区、永久区。
    实际上,堆只包括新生区、养老区。

堆的示意图:
在这里插入图片描述

4.6 堆:heap(java 8)

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

堆的示意图:
在这里插入图片描述

三、堆内存 调优参数

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

查看本机 分配给 jvm虚拟机内存的 初始值大小可用大小最大值大小
在这里插入图片描述
代码测试: 以下测试都在本类中测试

本机内存 为 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("还打印了堆内存的数据:");
    }
}

在这里插入图片描述
测试可看出:

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

2. VM 设置参数

2.1 设置:堆内存初始信息

在VM 中设置参数: 后在进行测试,后在后面打印出 详细的 GC 日志
VM参数:
在这里插入图片描述
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:
在这里插入图片描述
java 7:
在这里插入图片描述

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

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

IDEA 设置参数位置:
在这里插入图片描述
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. 第二次配置的日志:
    在这里插入图片描述
    分析:
    与第一次配置相同,仅改变 初始大小。内存设未20,可以满足代码所需的10M,这里不报错。

  2. 第三次配置的日志:
    在这里插入图片描述
    分析:
    老年代与新生代的比值 和配置的相同,为 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());
    }
}

日志分析:
在这里插入图片描述

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%,几乎没有,所以所有的对象都放在了新生代。
    }

日志分析:
在这里插入图片描述

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
        }
    }
}

日志分析:
事与愿违啊!!!
在这里插入图片描述

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 区域
}

日志分析:
第一次配置参数:
在这里插入图片描述

**第二次配置参数: 比第一次配置 多了一个配置::-XX:-UseTLAB 就是禁用 TLAB 区域 **
在这里插入图片描述
加了-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];
}

日志分析:
在这里插入图片描述

https://blog.csdn.net/leo187/article/details/88920036

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java冯坚持

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值