JVM系列文章
- 深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器_eden used total max-CSDN博客
- JVM深入原理(一+二):JVM概述和JVM功能-CSDN博客
- JVM深入原理(三+四):JVM组成和JVM字节码文件-CSDN博客
- JVM深入原理(五):JVM组成和JVM字节码文件-CSDN博客
- JVM深入原理(六)(一):JVM类加载器-CSDN博客
- JVM深入原理(六)(二):双亲委派机制-CSDN博客
- JVM深入原理(七)(一):运行时数据区-CSDN博客
- JVM深入原理(七)(二):运行时数据区-CSDN博客
- JVM深入原理(八)(一):垃圾回收-CSDN博客
- JVM深入原理(八)(二):垃圾回收-CSDN博客
目录
7.5. 运行时数据区-本地方法栈
- 本地方法栈的作用:Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。
- 本地方法栈的栈空间:在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来
编辑
7.6. 运行时数据区-Java堆
7.6.1. 堆-概述
- 堆的作用:存放对象的内存空间,它是空间最大的一块内存区域.栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
编辑
- 堆的特点:
- 线程共享:堆中的对象都需要考虑线程安全的问题
- 垃圾回收:堆有垃圾回收机制,不再引用的对象就会被回收
7.6.2. 堆-空间
- 堆空间的值:used,total,max,如果不设置任何的虚拟机参数,max默认是系统内存的1/4,total默认是系统内存的1/64.在实际应用中一般都需要设置total和max的值。
编辑
- used:当前已使用的堆内存
- total:java虚拟机已经分配的可用堆内存
- max:java虚拟机可以分配的最大堆内存
- 随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆。如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。
编辑
7.6.3. 堆-设置大小
- 要修改堆的大小,可以使用虚拟机参数-Xmx(max最大值)和-Xms(初始的total)。
- 语法:-Xmx值-Xms值
- 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
- 限制:Xmx必须大于2MB,Xms必须大于1MB
7.6.4. 堆内存溢出
程序运行时不断地创建新的对象就会出现堆内存溢出OOM
java.lang.OutOfMemoryError: Java heap space
编辑
7.6.5. 堆内存诊断
- jsp工具:查看当前系统有哪些Java进程
- jmap工具:查看堆内存占用情况,使用时加上参数-heap javaid,查看指定进程的堆内存占用情况
- jconsole图形化界面查看内存占用情况
- jvisualvm图形化界面查看内存占用情况
7.7. 运行时数据区-方法区
7.7.1. 方法区-概述
- 方法区的概述:方法区是存放基础信息的位置,线程共享,主要包括:
- 类的元信息:保存了所有类的基本信息
- 运行时常量池:保存了字节码文件中的常量池内容
- 静态常量池:字节码文件通过编号查表的方式找到常量
- 运行时常量池:当静态常量池加载到内存中后,可以通过内存地址快速查找常量池中的内容
- 字符串常量池:保存了字符串常量
7.7.2. 方法区-实现
- 方法区概念:方法区是Java虚拟机规范上的虚拟设计理念,每款JVM实现各不相同
- 不同版本的实现:
编辑
编辑
- JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
- JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。
7.7.3. 方法区-内存溢出
- JDK7的方法区内存溢出:永久代内存溢出,Java.lang.OutofMemoryError:PermGen space,设置永久代大小参数-XX:MaxPermSize=8m
- JDK8的方法区内存溢出:超过操作系统内存上限后,才会出现元空间内存溢出,有可能影响其他程序的内存,java.lang.OutofMemoryError:Metaspace,设置元空间大小参数XX:MaxMetaspaceSize=8m
7.7.4. 方法区-字符串常量池
7.7.4.1. 字符串常量池-概述
- 字符串常量池的作用:用于存储在代码中定义的常量字符串
- 字符串常量池和运行时常量池的关系:早期字符串常量池是运行时常量池的一部分存储位置也是一致的,后期将字符串常量池和运行时常量池做了区分
编辑
7.7.4.2. 字符串常量池-工作流程
- 利用代码来解释字符串常量池工作流程:
编辑
7.7.4.2.1. 案例1
------源代码------
编辑
------执行结果------
- s3 == s4:s3指向的是字符串常量池中的ab字符串,s4指向的是堆中的ab字符串对象,所以两者地址不相等为false
- s3 == s5:s3指向的是字符串常量池中的ab字符串,s5的字符串a和字符串b的拼接在编译期直接完成,指向的是字符串常量池中的ab字符串,所以两者地址相等为true
- s3 == s6:s3指向的是字符串常量池中的ab字符串,s6的值是s4未放入字符串常量池而返回的字符串常量池中ab字符串,所以两者地址相等为true
------字节码------
编辑
------代码解析------
- ldc #2 <a>:从常量池中获取字符串a的地址压入操作数栈
- astore_1:将操作数栈中的值(a)弹出放入局部变量表1号位置(变量s1的位置)
- ldc #3 <b>:从常量池中获取字符串b的地址压入操作数栈
- astore_2:将操作数栈中的值(b)弹出放入局部变量表1号位置(变量s2的位置)
- ldc #4 <ab>:从常量池中获取字符串ab的地址压入操作数栈
- astore_3:将操作数栈中的值(ab)弹出放入局部变量表1号位置(变量s3的位置)
- new #5 <java/lang/StringBuilder>:创建StringBuilder对象
- dup
- invokespecial #6 <java/lang/StringBuilder.<init>>:初始化StringBuilder为空字符串""
- aload_1:加载局部变量表中的1号位置的值(a)到操作数栈
- invokevirtual #7 <java/lang/StringBuilder.append:调用StringBuilder的append拼接刚刚操作数栈加载的字符串a
- aload_2:加载局部变量表中的1号位置的值(b)到操作数栈
- invokevirtual #7 <java/lang/StringBuilder.append:调用StringBuilder的append拼接刚刚操作数栈加载的字符串b
- invokevirtual #8 <java/lang/StringBuilder.toString:调用StringBuilder的toString方法将StringBuilder对象转换成String对象存入堆内存中
- astore 4:弹出操作数栈中的数据(ab字符串对象)放入局部变量表中的4号位置(变量s4的位置)
- ldc #4 <ab>:从常量池中获取字符串(ab)的地址压入操作数栈
- astore 5:弹出操作数栈中的数据(ab)放入局部变量表中的4号位置(变量s5的位置)
- aload 4:加载局部变量表中的4号位置的值(ab)到操作数栈
- invokevirtual #9 <java/lang/String.intern>:调用String的intern方法尝试将ab字符串加入到字符串常量池中,判断字符串常量池中是否有ab字符串,有则直接返回的字符串常量池中的ab字符串到操作数栈中,无则将4号位置的引用(ab字符串对象)转换到字符串常量池中(引用变为字符串常量池中)并返回字符串常量池中的引用到操作数栈中
- intern方法会主动将调用方法的字符串对象放入字符串常量池中,判断字符串常量池中是否有这个字符串常量池.
- JDK8中有则直接返回字符串常量池中的字符串常量,无则将当前堆中的字符串对象引用转到字符串常量池中(堆中的字符串对象之后会被垃圾回收,因为无人引用了),然后返回字符串常量池中的引用
- JDK6中有则直接返回字符串常量池中的字符串常量,无则将当前堆中的字符串对象复制一份到字符串常量池中(相当于两份引用,原来的并不会转变到字符串常量池中),然后返回字符串常量池中的引用
- JDK1.6中StringTable没有这个对象时会复制对象再放入,本质上是两个对象
- astore 6:弹出操作数栈中的数据(ab)放入局部变量表中的6号位置(变量s6的位置)
编辑
- String s1 = new String("abc"):new关键字创建一个abc字符串对象,当代码编译成字节码后,abc字符串会存入静态常量池中,当程序运行字节码读取到内存后,abc字符串就会存入字符串常量池中,由于字符串对象是new关键字创建出来的,所以这个对象会存储在堆内存中,在栈内存中使用s1局部变量去保存堆内存中abc的地址
- String s2 = "abc":并没有创建字符串对象,而是直接去字符串常量池中找是否有abc字符串,有则直接返回,无则在字符串常量池中创建一个abc字符串再返回
- s1 == s2:s1指向的是堆中的abc字符串对象,s2指向的是字符串常量池中的abc字符串
7.8. 直接内存
7.8.1. 直接内存-概述
- 直接内存的作用:直接内存(Direct Memory)并不在Java虚拟机规范中存在,并不属于Java运行时的内存区域,在JDK4中引入NIO机制使用到了直接内存,主要解决两个问题:
- 解决Java堆中的对象垃圾回收时会影响对象的创建和使用的问题
- 解决IO操作浪费资源的情况,NIO在堆中可以直接访问直接内存,而不是IO的复制一份数据到Java堆中缓存
- 直接内存的特点:
- 分配回收成本高,同时读写性能也高
- 不收JVM内存回收管理
7.8.2. 直接内存-工作流程
- 传统IO-工作流程:
编辑
- Java调用本地方法将CPU状态由用户态转变为内核态(因为Java并不具备磁盘读写的能力)
- CPU切换至内核态后,会在系统内存和Java堆内存中各开辟一块缓存区域,系统缓存区分批次读取磁盘文件数据后再复制一份到Java堆缓存区中,完成整个磁盘读取后会Java再次调用本地方法将将CPU状态由内核态转变为用户态
- CPU切换为用户态后,Java使用IO读取Java堆缓存区中的数据
- NIO-工作流程:
- Java调用本地方法将CPU状态由用户态转变为内核态(因为Java并不具备磁盘读写的能力)
- CPU切换至内核态后,会直接在系统内存中划分一块儿缓冲区直接内存,Java堆和系统可以直接访问直接内存中的数据,这样就不用复制一份到Java堆中了
给直接内存分配太多空间但没有及时回收就会造成直接内存溢出
OutOfMemory:Direct Buffer Memory
7.8.3. 直接内存-调整内存大小
- 如果需要手动调整直接内存的大小,可以使用-XX:MaxDirectMemorySize=大小
- 单位k或K表示千字节,m或M表示兆字节,g或G表示干兆字节。默认不设置该参数情况下,JVM自动选择最大分配的大小。
- 以下示例以不同的单位说明如何将直接内存大小设置为1024KB:
- -XX:MaxDirectMemorySize=1m
- -XX:MaxDirectMemorySize=1024k
- -XX:MaxDirectMemorySize=1048576
7.8.4. 直接内存分配释放原理
- Java中Unsafe对象完成直接内存的分配回收,回收时需要主动调用UnSafe对象的freeMemory方法
- Java中的ByteBuffer接口实现类内部使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会有ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法来释放直接内存
- 一般JVM调优时,会使用-XX:+DisableExplictGC禁用显示垃圾回收(system.gc()),因为显示垃圾回收是一种full GC,对程序影响大,不仅会回收新生代对象,而且老年代对象一并回收,但是开启禁用之后,直接内存的回收就不会被回收,需要手动去获取UnSafe对象回收直接内存
编辑
编辑