JVM_01 内存结构篇
Posted 爪洼ing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM_01 内存结构篇相关的知识,希望对你有一定的参考价值。
JVM(Java Virtual Machine)
一、前言
1、什么是 JVM ?
1、定义:
- Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
2、好处:
- 一次编译,处处执行
- 自动的内存管理,垃圾回收机制
- 数组下标越界检查
3、比较:
JVM、JRE、JDK 的关系如下图所示
2、学习 JVM 有什么用?
- 面试必备
- 中高级程序员必备
- 想走的长远,就需要懂原理,比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事等待,JVM 是必须掌握的。
3、常见的 JVM
这里需要重点了解:JVM是一套规范,而我们也可以遵守这套规范实现自己的JVM(有能力的前提下!)
4、JVM整体预览
先做一个整体预览,然后逐个击破!
二、内存结构
1、程序计数器
1.1、概述:
JVM 中的程序计数器(Program Counter Register)有的时候也被称作PC寄存器,为了避免混淆这里解释一下,这里,并非是广义上所指的
物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)更加贴切(也称为程序钩子),并且也不容易引起误会。
JVM 中 PC 寄存器是堆物理 PC 寄存器的一种抽象模拟。
1.2、特点:
- 是线程私有的
- 不会存在内存溢出(OOM)
1.3、作用:
PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
2、虚拟机栈
栈帧:每个方法执行的时候需要的内存空间,其中占用空间的有(参数、局部变量、返回地址等)
当我们调用一个方法的时候,会在虚拟机栈当中给他开辟一个栈帧大小的空间,然后让栈帧入栈,方法执行完毕栈帧出栈!
定义:
- 每个线程运行需要的内存空间,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
相关问题:
1、垃圾回收是否涉及占栈内存 ?
答: 不会,因为我们的栈中的栈帧空间是用完就释放的!
2、栈内存的分配越大越好吗?
答:不是,由于我们的物理内存是有限的,一个栈的内存越大,能开线程越少,并发降低,其作用仅仅是增加了方法的递归调用
3、方法内的局部变量是否为线程安全的?
答:是的,局部变量是在线程私有的,不会与其他线程共享,不会产生线程安全问题(前提是局部变量没有逃离方法的作用范围!)
2.1、栈内存溢出
栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!
2.2、线程运行诊断
案例一:cpu 占用过多
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
- top 命令,查看是哪个进程占用 CPU 过高
- ps H -eo pid (进程id), tid(线程id),%cpu | grep : 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
- jstack 进程 id : 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。
案例二:长时间运行未出现结果(可能出现死锁)
解决方法:使用jstack工具 + 进程号,就会定位到死锁问题!
3、本地方法栈
一些带有 native 关键字的方法就是需要 JAVA 去调用本地的C或者C++方法,因为 JAVA 有时候没法直接和操作系统底层交互,所以需要用到本地方法栈,服务于带 native 关键字的方法。
4、堆
Heap 堆
- 通过new关键字创建的对象都会被放在堆内存
特点 :
- 它是线程共享,堆内存中的对象都需要考虑线程安全问题
- 有垃圾回收机制
4.1、堆内存溢出
发生堆内存溢出会抛出 java.lang.OutOfMemoryError
可以使用 -Xmx8m
来指定堆内存大小。将堆内存调小就可以便于我们排查问题!
4.2、堆内存诊断
1、jps工具:查看当前系统中有哪些java进程 : jps
2、jmap工具 :查看堆内存的占用情况 : jmap - heap + 进程id
3、jconsole工具 : 图形化界面多功能的检测工具,可以连续监测
4、jvisualvm工具 : 相对jconsole更加强大的可视化工具
案例 : 垃圾回收后,内存占用仍然很高!
使用jvisualvm,启动可视化工具检测我们的虚拟机内存,找到HeapDump,对堆内存进行一个快照(内存转储),然后获取并且分析此时详细数据,定位原因!
5、方法区
首先看一个定义:我们的JVM规范中对于方法区(Method Area)的定义:
看一下内存结构
5.1、方法区内存溢出
-
1.8 之前会导致永久代内存溢出
使用 -XX:MaxPermSize=8m 指定永久代内存大小
-
1.8 之后会导致元空间内存溢出
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
5.2、运行时常量池
首先先理解什么是常量池
1、我们可以通过堆一下代码得字节码文件进行反编译,拿到我们的反编译信息
//想要运行就需要被编译为二进制字节码(类基本信息,常量池,类方法定义,包含虚拟机指令)
public class ContentPoolTest
public static void main(String[] args)
System.out.println("Hello World");
2、找到字节码文件执行
javap -v ContentPoolTest.class
3、可以得出我们得字节码文件,反编译后的结果
然后我们对字节码文件进行说明
由此可以总结得出:常量池就是一张表,虚拟机指令根据这张常量表,去找到要执行的类名、方法名、参数类型、字面量等信息
我们一个类的字节码文件包含一个常量池,多个类一起运行的情况下,会将每个类的常量池表汇聚在一起,放在我们的运行时常量池
其中也不会是#1 #2 #3 这种地址,而是真实的内存地址!
5.3、串池 StringTable
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象【懒加载,只有当JVM指令用到的时候才会创建对象】
- 利用串池的机制,来避免重复创建字符串对象 【主要是因为串池是HashTable实现的,底层是Hash表不可扩容】
- 字符串变量的拼接原理:StringBuilder(1.8)
- 字符串常量的拼接原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池子!
案例一
//StringTable["a","b","ab"]
String s1 = "a" ; //当执行到此出处的时候会将a转为"a",然后放入StringTable中
String s2 = "b" ;
String s3 = "ab" ;
String s4 = s1 + s2 ; //new StringBuilder().append("a").append("b").toString ; new String("ab")保存在堆中!
String s5 = "a" + "b" ; //在编译期间已经优化,在编译期确定为ab ;
System.out.println(s3 == s4); // false ;
System.out.println(s3 == s5); // true
案例二
intern方法1.7以后
String s = new String("a") + new String("b") ;
//StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】
System.out.println(s == "ab"); //false ;
String str = s.intern();// 如果串池没有s,intern方法会将s的值传入串池当中,并且返回串池子中的对象
// 如果串池中有s,s就不会入池但是,仍然会返回串池中的对象 【无论怎样s都会入池】
System.out.println(str == "ab"); //返回true
System.out.println(s == "ab"); //true ;
intern方法1.6以前
String s = new String("a") + new String("b") ;
//StringBuilder动态字符串拼接,保存在堆中【此时串池没有"ab"】
System.out.println(s == "ab"); //false ;
String str = s.intern();// intern方法会将s的值拷贝一份传入串池当中,并且返回串池中的对象
// 【注意:此时的s仍然是在堆中】
System.out.println(str == "ab"); //返回true
System.out.println(s == "ab"); //false ;
5.4、StringTable 的位置
- jdk1.6 StringTable 位置是在永久代中
- jdk1.8 StringTable 位置是在堆中
5.5、StringTable 垃圾回收
-Xmx10m
指定堆内存大小-XX:+PrintStringTableStatistics
打印字符串常量池信息-XX:+PrintGCDetails
打印GC信息-verbose
:gc 打印 gc 的次数,耗费时间等信息``
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Code_05_StringTableTest
public static void main(String[] args)
int i = 0;
try
for(int j = 0; j < 10000; j++) // j = 100, j = 10000
String.valueOf(j).intern();
i++;
catch (Exception e)
e.printStackTrace();
finally
System.out.println(i);
5.6、StringTable 性能调优
- 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
- 考虑是否需要将字符串对象入池,可以通过 intern 方法减少重复入池
6、直接内存
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
6.1、使用直接内存的好处
文件读写流程:
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
减少了不必要的复制操作
6.2、直接内存回收原理
直接内存的回收不是通过 JVM 的垃圾回收来释放的,,而是通过unsafe.freeMemory 来手动释放。
public class Code_06_DirectMemoryTest
public static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException
// method();
method1();
// 演示 直接内存 是被 unsafe 创建与回收
private static void method1() throws IOException, NoSuchFieldException, IllegalAccessException
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(Unsafe.class);
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base,_1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
// 演示 直接内存被 释放
private static void method() throws IOException
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
System.gc(); // 手动 gc
System.in.read();
直接内存的回收机制总结
使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法freeMemory 来释放内存
简述:一但BtyeBuffer这个这个java类被回收,就会将我们的直接内存释放!
然而我们一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC //禁止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。
所以我们就通过 unsafe 对象主动的调用 freeMemory 的方式释放内存。
)
以上是关于JVM_01 内存结构篇的主要内容,如果未能解决你的问题,请参考以下文章