JVM运行时内存理论实践结合

Stella981
• 阅读 555

原创JudyGril

JVM运行时内存理论实践结合

JVM内存区域

程序运行会将编译好的.class 文件(静态),想要运行必须装载到JVM内存中,通过ClassLoad加载到JVM内存区域,将.class文件加载到内存中形成逻辑映射,简单来说程序运行需要:1.数据,2.执行方法,3.从哪个指令开始执行

思维导图

JVM运行时内存理论实践结合

内存区域图

JVM运行时内存理论实践结合 JVM运行时内存理论实践结合

内存区域发展历史及原因

JVM内存区域中方法区实现发生过变化, 在JDK1.8之前使用的是HotSpot虚拟机 开发 , 使用永久代来实现方法区, 实际上实现方法区不用做统一的规范,对于其它虚拟机根本就没有永久代的概念, 所以有的时候如果源头理不清楚我们会混乱知识结构, 方法区不等于永久代

JDK6的时候逐步放弃永久代改为本地内存

JDK7把原本放在永久代常量池,静态变量移除,存放在堆中

JDK8彻底放弃永久代使用元空间(本地内存)

为什么要永久代替换成元空间 ?

如果常量池存储在永久代容易出现溢出

内存区域包含哪几部分

程序计数器

占用一小块内存空间, 是当前线程所执行字节码的行号指示器,字节码运行过程中通过改变程序计数器的值来获取下一条字节码指令,  该字节码指令对应的是一个线程, 另一个线程是无法获取的, 否则就乱套了, 所以程序计数器是线程私有内存.

Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: iconst_2
       6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       9: astore_2
      10: aload_1
      11: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      14: aload_2
      15: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      18: iadd
      19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      22: astore_3
      23: return
}

虚拟机栈

虚拟机栈针对的是Java方法执行的内存模型,所以他也是线程私有的. 每个方法在执行的时候都会创建栈帧, 栈帧里存储的是方法信息. 每个方法调用都对应着入栈和出栈

  • 局部变量表

存储的是方法变量,方法参数

  • 操作数栈

底层数据结构也是栈, 主要用途为 操作数据运行, 当方法运行的时候存储一个栈帧, 栈帧里 操作数栈内存为空, 当有变量进行运算的时候, 变量入栈到操作数栈, 等运算完结果进行出栈操作,把数据返回给变量或方法调用者.

  • 动态链接

在类加载阶段或者第一次使用的时候方法区中的符号引用(符号引用在常量池中)转换为直接引用

  • 返回地址

方法执行完成之后,返回方法调用的地方(程序计数器+栈帧)

  • 异常情况

由于虚拟机栈是线程私有的,每一个方法允许都会创建 栈帧 , 如果递归运行无限创建栈帧, 最终会超出虚拟机所允许的深度, 会报出stackOverflowError

本地方法栈

为什么有虚拟机栈还会有本地方法栈呢?  他们的区别在于 虚拟机栈是为虚拟机执行的方法, 而本地方法栈是Native方法

堆是线程共享的一个内存区域, 在虚拟机启动的时候创建, 它主要是为存储对象实例, Java堆还有另一个知识点是垃圾收集器, 目前 先不在这里做简述.

堆中在jdk7的时候把常量池移动到堆中.

对象的内存布局

当对象被创建的时候会被分配到堆中, 对象有三部分组成, 对象头, 实例数据, 对象填充,  对象头是由两部分组成,1存储对象运行时数据, 例如GC年龄 , 是否持有锁, 锁状态 2 类型元数据指针,以便于区分该对象是属于什么类型的对象.那个类的实例

方法区

方法区与堆一样都是内存共享区域, 有一个别名叫做非堆,主要目的是为了区分堆. 字符串常量和静态变量在jdk7的时候已经移动到堆中,方法区存储实现是元空间,元空间使用的是本地内存存储.

直接内存(堆外内存)

直接内存并不是JVM内存区域的一部分,但它也会频繁的调用, 在JDK1.4的时候引入了NIO, NIO通过Channel与Buffer可以使用Native函数库来分配对外内存, 通过DirectByteBuffer对象作为这块内存的引用来进行操作,这样避免了Java堆和native堆中来回复制数据。

为什么使用堆外内存

1 减少垃圾回收

2 提升复制速度

堆内内存有JVM管理,属于用户态,堆外内存由操作系统管理属于内核态,如果从堆内向磁盘写数据时,数据先被复制到堆外内存(内核缓冲区),然后再由操作系统写入磁盘,所以如果直接使用堆外内存可以避免这个过程,所以提高复制速度。

while (true){
            //创建DirectByteBuffer对象
            ByteBuffer.allocateDirect(10 * 1024 * 1024);
        }
  // 创建DirectByteBuffer对象
  public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
   }
   DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        //内存是否按也分配对齐
        boolean pa = VM.isDirectMemoryPageAligned();
       //获取每页大小
        int ps = Bits.pageSize();
       //如果是按页对齐的,多分配一页容量(因为后续可能需要按页对齐)
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);
        long base = 0;
        try {
            //分配本地内存也就是直接内存,base表示直接内存起始地址
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
       // 为申请的内存批量填充0值
        unsafe.setMemory(base, size, (byte) 0);
       //是否要求地址按页向上对齐
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
       // 用清理器追踪对象,当引用失效时,回收内存
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

Java对象创建过程

public class JVMShowcase {
//静态类常量,
public final static String ClASS_CONST = "I'm a Const";
//私有实例变量
private int instanceVar=15;
public static void main(String[] args) {
//调用静态方法
runStaticMethod();
//调用非静态方法
JVMShowcase showcase=new JVMShowcase();
showcase.runNonStaticMethod(100);
}
//常规静态方法
public static String runStaticMethod(){
return ClASS_CONST;
}
//非静态方法
public int runNonStaticMethod(int parameter){
int methodVar=this.instanceVar * parameter;
return methodVar;
}
}

JVM运行时内存理论实践结合

JVM运行时内存

上篇文章介绍JVM区域都有什么, 存储什么数据. 这篇文章讲的是JVM堆中对象的内存分配 . 在堆中Java对象可以分为两大类,新生代和老年代

宏观图

JVM运行时内存理论实践结合

新生代

新生代表示新生成的对象会存放到新生代.一般对象在新生代的时候就会被回收GC

Eden区

对象创建的时候先存储在新生代Eden区, Eden区是有空间限制的, 如果空间满了会触发GC, 没有引用的对象这个时候会被回收, 如果有存留下来的幸存者 ,包含ServivorFrom,ServivorTo,占比8:1:1

老年代

在新生代GC多次之后(15)则会进入老年代,在老年代存活的时间长,因为空间比较大,GC的几率不是很高

举例子

新生代GC条件

解说一下内容

  • 堆最大为40M,新生代大小为20M,PrintGCDetails为打印日志。

  • alloc1,alloc2  , alloc3 总共加起来 15 , 满足新生代20M ,当alloc4加载的时候发现无法放入,这个时候新生代触发GC,[DefNew: 12216K->592K(18432K), 0.0073585 secs] , 12216GC之后变为592

  • def new generation   total 18432K, used 16594K 总共需要18432K,eden占用16384K,from占用2048K,to占用2048K

  • 由于Servivor区空间也不够使用,所以对象进入老年代,   the space 20480K,  50% used

    /**

    • @Author: judy
    • @Description:参数设置:-verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
    • @Date: Created in 8:09 2020/7/9

    */ public class demo { public static void main(String[] args) { final int tenMB = 1024 * 1024; byte[] alloc1, alloc2, alloc3, alloc4; alloc1 = new byte[5 * M]; alloc2 = new byte[5 * M]; alloc3 = new byte[5 * M]; alloc4 = new byte[10 * M]; } } 运行结果: [GC (Allocation Failure) [DefNew: 12216K->592K(18432K), 0.0073585 secs] 12216K->10832K(38912K), 0.0074091 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] Heap def new generation total 18432K, used 16594K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000) eden space 16384K, 97% used [0x00000000fd800000, 0x00000000fe7a09f0, 0x00000000fe800000) from space 2048K, 28% used [0x00000000fea00000, 0x00000000fea94178, 0x00000000fec00000) to space 2048K, 0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000) tenured generation total 20480K, used 10240K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000) the space 20480K, 50% used [0x00000000fec00000, 0x00000000ff600020, 0x00000000ff600200, 0x0000000100000000) Metaspace used 3225K, capacity 4496K, committed 4864K, reserved 1056768K class space used 350K, capacity 388K, committed 512K, reserved

对象进入老年代

  • 大对象直接进入老年代

  • 长期存活对象进入老年代

  • 总结就是空间不够用的时候进入老年代

**1 .**当对象大小等于或大于Eden区,这个时候会抛出异常,因为空间不够使用, 你会不会思考为什么不直接进入老年代呢?因为系统不认为他是大对象。

public class Demo {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc4 = new byte[20 * M];
    }
}
[GC (Allocation Failure) [DefNew: 1648K->581K(18432K), 0.0022828 secs][Tenured: 0K->580K(20480K), 0.0026501 secs] 1648K->580K(38912K), [Metaspace: 3068K->3068K(1056768K)], 0.0050093 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 580K->562K(20480K), 0.0027778 secs] 580K->562K(38912K), [Metaspace: 3068K->3068K(1056768K)], 0.0028232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at Demo.main(Demo.java:14)
Heap
 def new generation   total 18432K, used 819K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,   5% used [0x00000000fd800000, 0x00000000fd8cced0, 0x00000000fe800000)
  from space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
  to   space 2048K,   0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
 tenured generation   total 20480K, used 562K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,   2% used [0x00000000fec00000, 0x00000000fec8cb90, 0x00000000fec8cc00, 0x0000000100000000)
 Metaspace       used 3155K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 10

**2 .**当设置大对象大小的时候,判断大对象是否可以直接进入老年代,不会抛出溢出异常

-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1024000

可以看出from space 和 to space都是0并且也没有触发GC, 老年代 tenured generation   total 20480K, used 5120K,使用了5120,说明对象直接进入老年代。

public class Demo {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc4 = new byte[5 * M];
    }
}
Heap
 def new generation   total 9216K, used 1858K [0x00000000fe200000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 8192K,  22% used [0x00000000fe200000, 0x00000000fe3d0ab0, 0x00000000fea00000)
  from space 1024K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000feb00000)
  to   space 1024K,   0% used [0x00000000feb00000, 0x00000000feb00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 5120K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  25% used [0x00000000fec00000, 0x00000000ff100010, 0x00000000ff100200, 0x0000000100000000)
 Metaspace       used 3139K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

3 . 当对象超过一定年龄则会进入老年代(默认年龄为15,每发生一次GC则会触发一次GC)

-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1

首先我们设置对象的年龄为1则进入老年代, eden space 838912K, 100% 可以看出eden区内存已经全部占用,但是from和to都是0% from space 104832K,   0%  to   space 104832K,   0% ,老年代使用的空间为tenured generation   total 1048576K, used 614400K ,所以可以看出对象在第一个gc的时候直接进入老年代。满足对象GC年龄为1则进入老年代。

public class Demo {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[300 * M];
        alloc2 = new byte[300 * M];
        alloc3 = new byte[300 * M];
        alloc4 = new byte[500 * M];
    }
}
[GC (Allocation Failure) [DefNew: 664734K->616K(943744K), 0.3629964 secs] 664734K->615016K(1992320K), 0.3630391 secs] [Times: user=0.13 sys=0.23, real=0.36 secs]
Heap
 def new generation   total 943744K, used 839528K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
  eden space 838912K, 100% used [0x0000000080000000, 0x00000000b3340000, 0x00000000b3340000)
  from space 104832K,   0% used [0x00000000b99a0000, 0x00000000b9a3a2c8, 0x00000000c0000000)
  to   space 104832K,   0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
 tenured generation   total 1048576K, used 614400K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
   the space 1048576K,  58% used [0x00000000c0000000, 0x00000000e5800020, 0x00000000e5800200, 0x0000000100000000)
 Metaspace       used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

4 . Survivor 区中相同年龄对象内存站一半多内存,则如果对象大于该年龄的对象,则直接进入老年代

-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
在执行alloc3的时候发现-Xmn1024M已经无法放入,所以触发新生代GC,由于900M是-Xmn1024M一半多,所以该对象直接进入老年代。public class Demo {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[100 * M];
        alloc2 = new byte[100 * M];
        alloc3 = new byte[900 * M];
    }
}
Heap
 def new generation   total 943744K, used 271913K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
  eden space 838912K,  32% used [0x0000000080000000, 0x000000009098a598, 0x00000000b3340000)
  from space 104832K,   0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
  to   space 104832K,   0% used [0x00000000b99a0000, 0x00000000b99a0000, 0x00000000c0000000)
 tenured generation   total 1048576K, used 921600K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
   the space 1048576K,  87% used [0x00000000c0000000, 0x00000000f8400010, 0x00000000f8400200, 0x0000000100000000)
 Metaspace       used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1

如果该对象没有新生代的一半那进入老年代吗?

可以看出tenured generation   total 1048576K, used 0K , 老年代使用为0 , eden区完全可以容纳这些对象。

public class Demo {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[100 * M];
        alloc2 = new byte[100 * M];
        alloc3 = new byte[500 * M];
    }
}
Heap
 def new generation   total 943744K, used 783913K [0x0000000080000000, 0x00000000c0000000, 0x00000000c0000000)
  eden space 838912K,  93% used [0x0000000080000000, 0x00000000afd8a5a8, 0x00000000b3340000)
  from space 104832K,   0% used [0x00000000b3340000, 0x00000000b3340000, 0x00000000b99a0000)
  to   space 104832K,   0% used [0x00000000b99a0000, 0x00000000b99a0000, 0x00000000c0000000)
 tenured generation   total 1048576K, used 0K [0x00000000c0000000, 0x0000000100000000, 0x0000000100000000)
   the space 1048576K,   0% used [0x00000000c0000000, 0x00000000c0000000, 0x00000000c0000200, 0x0000000100000000)
 Metaspace       used 3225K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

参考文章

https://www.cnblogs.com/myseries/p/12084266.html

https://www.cnblogs.com/qianguyihao/p/4744233.html

https://segmentfault.com/a/1190000023083836

https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html

JVM运行时内存理论实践结合

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
京东云开发者 京东云开发者
6个月前
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这