JVM学习笔记(一):引言、JVM内存结构
引言Java Virtual Machine:java程序的运行时(java二进制字节码的运行环境)优点一次编写,到处运行自动内存管理,垃圾回收功能数组下标越界检查多态jvm, jre, jdk之间的区别:JVM整体结构:JVM 内存结构JVM 内存结构主要包括:程序计数器(PC Register)虚拟机栈(JVM Stacks)本地方法栈(Native Met...
引言
Java Virtual Machine:java程序的运行时(java二进制字节码的运行环境)
优点
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
jvm, jre, jdk之间的区别:
JVM整体结构:
JVM 内存结构
JVM 内存结构主要包括:
- 程序计数器(PC Register)
- 虚拟机栈(JVM Stacks)
- 本地方法栈(Native Method Stacks)
- 堆(Heap)
- 方法区(Method Area)
一、程序计数器(PC Register)
含义:Program Counter Register 程序计数器(寄存器)
作用:记住下一条指令的执行地址。现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。
特点:
- 程序计数器具有线程隔离性
- 不会存在内存溢出的问题,是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域
- 程序计数器占用的内存空间非常小,可以忽略不计
- 执行native本地方法时,程序计数器的值为空。原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计
二、虚拟机栈(JVM Stacks)
定义
含义:Java Virtual Machine Stacks(Java虚拟机栈)
- Java线程运行时所需要的内存成为虚拟机栈
- 每个栈中可以放入多个栈帧(Frame),栈帧用于存储局部变量表,操作数栈,动态链接方法出口等信息。
- 每一个方法被调用到执行完毕的过程,就对应着一个栈桢在虚拟机栈中从入栈到出栈的过程
- 每个线程在一个时刻只能有一个活动的栈桢,对应着栈顶那个正在执行的方法
问题:
1、垃圾回收是否涉及虚拟机栈?
答:不涉及,虚拟机栈是每个线程自己使用的内存空间,不需要GC管理。
2、栈内存是否越大越好?
答:不是。虽然栈内存越大调用深度就可以越深,但是由于每个线程对应一个栈内存,则栈内存越大可分配的线程就越少。
3、方法内的局部变量是否是线程安全的?
答:如果方法内的局部变量没有逃脱他的作用范围,则它是线程安全的。如果局部变量引用了对象,并且逃离了方法的作用范围,则需要考虑线程安全问题。
异常:
- 如果线程请求栈的深度大于虚拟机所允许的深度,则抛出StackOverflowError异常
- 如果Java虚拟机栈的容量可以动态扩展,当栈扩展时无法申请到足够的内存时会抛出OutOfMemory异常
栈内存溢出(java.lang.StackOverflowError)
出现的原因:
-
调用深度过多导致的栈桢过多超出栈内存溢出
-
方法中过多的本地变量导致的栈桢多大而引发的栈内存溢出
线程运行诊断
案例一:CPU占用过多
定位方法
- 在linux系统下使用top命令定位哪个进程对cpu的占用高
- 用ps命令进一步定位是哪个线程引起的cpu占用过高
ps H -eo pid,tid,%cpu | grep 进程id
- jstack 进程id
- 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
案例二:程序运行很长时间没有结果
使用 jstack 进程id 查看运行的相关信息:
三、本地方法栈
- 本地方法栈是指为虚拟机使用到本地(Native)方法(比如调用C语言的方法)服务。
- 本地方法栈也会在栈溢出或者栈扩展失败时分别抛出:StackOverflowError异常和OutOfMemory异常
四、Java堆
定义:
Java堆是被所有线程共享的内存区域,所有对象实例以及数组(通过new关键字创建的)都应该在堆上分配。
特点:
- 所有线程共享的,堆中所有对象都应该考虑线程安全问题
- 有垃圾回收机制
- 如果从内存分配的角度来看,所有线程贡献的Java堆可以划分为多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
堆内存溢出
如果在Java堆中没有内存完成实力分配,并且堆无法再扩展时,Java虚拟机会抛出OutOfMemory异常
其中可以使用-Xmx和-Xms设置堆的最大内存和最小内存。
堆内存诊断
内存诊断工具:
-
jps工具:查看当前系统中有哪些java进程
-
jmap工具:查看堆内存的占用情况
-
jconsole工具:具有图形界面,多功能的检测工具,可以连续监测
-
jvirsualvm:可以查看堆中各个对象占用内存的大小
五、方法区
定义:
方法区是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
-
方法区是线程共享的
-
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemory异常
-
JDK8之前,hotspot使用永久代来实现方法区,使得垃圾收集器可以像管理堆一样管理方法区,省区了专门编写内存管理代码的工作
-
JDK8及之后在本地内存中实现的元空间(Metaspace)来代替了永久代
方法区内存溢出问题
方法区内存溢出的主要原因是加载的类过多导致超出内存空间
- jdk8 之前会导致永久代内存溢出
// 报错:java.lang.OutOfMemory: PremGen space
// 参数:-XX:MaxPremSize=8M
- jdk8 之后会导致元空间内存溢出
// 报错:java.lang.OutOfMemory: Metaspace space
// 参数:-XX:MaxMetaspaceSize=8M
问题产生的场景:当前许多主流的框架,如Spring、MyBatis对类进行增强时,都会使用CGLib这类字节码技术,当增强的类越多,就需要越大的方法区空间来保证动态生成的新类型可以载入内存。
运行时常量池
常量池:就是一张表,虚拟机执行根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里边的符号地址变为真实地址。
StringTable串池★
StringTable是运行时一个用于存储String常量的HashTable结构区域,不能够扩容。
StringTable串池特性
- 常量池中的字符串仅仅是符号,第一次用到时才会变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量的拼接原理是StringBuilder(如上图所示),toString()方法实际上new String(‘a’)新对象
- 字符串常量的拼接原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8版本中,intern方法尝试将字符串对象放入串池中,如果有则不会放入,如果没有则将其放入串池中,并将在串池中的该对象返回
- 1.6版本中,intern方法尝试将字符串对象放入串池中,如果有则不会放入,如果没有则将它的拷贝放入串池中,并将在串池中的拷贝对象
试题:
-
1.6版本下:位于永久代
-
1.8版本下:位于堆中
-
调整的原因:因为永久代触发GC的条件比较严格,而由于一般程序中都存在大量的过期无用字符串常量导致StringTable十分臃肿并且不能及时进行垃圾回收,所以1.8版本之后将其放在堆中
验证其存放位置从永久代(PermGen)到堆(Heap)的转变方法:不断创建具有引用的字符串常量,看哪个区域报OutOfMemory错误。代码如下:
/**
* 测试StringTable所在的位置
* //当使用UseGCOverheadLimit时,如果GC占用了98%的时间,但是只释放了2%的空间,则报此错误。
* //这里没有使用(-号表示不使用,+号表示使用)
* 1.8:-Xmx10m -XX:-UseGCOverheadLimit
* 1.6: -XX:MaxPermSize=10m
*/
public class StringTableLocationTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int j = 0;
try {
for (int i = 0; i < 300000; i++) {
//将j转化为字符串并放入StringTable中,再将返回的对象引用放入list
list.add(String.valueOf(j).intern());
j++;
}
}catch (Exception e){
e.printStackTrace();
} finally {
System.out.println(j);
}
}
}
1.8版本的结果:
1.6版本结果
StringTable垃圾回收
测试代码
/**
* 串池GC测试
* 最大堆内存为10m 打印串池统计信息 打印垃圾回收的详细信息:次数、时间等
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class StringTableGCTest {
public static void main(String[] args) {
int j = 0;
try {
for (int i = 0; i < 10000; i++) {
String.valueOf(j).intern();
j++;
}
}catch (Exception e){
e.printStackTrace();
} finally {
System.out.println(j);
}
}
}
当没有在串池中加入字符串常量时,串池中总的字符串个数为1754。并且没有发生GC。
当试图在串池中加入10000字符串常量时,串池中总的字符串个数为7226,只增加了5000多个,因为在这个过程中发生了GC。
StringTable调优★
-
如果应用中存在大量的字符串,而且这些字符串中存在大量的重复,那么就可以使用String::intern方法将字符串放入串池,减小字符串对象的个数,进而减少字符串对内存的消耗。
-
调整 -XX:StringTableSize=桶的个数。StringTable内部采用HashTable的形式进行存储,当HashTable桶的个数非常小时,容易产生哈希冲突,减慢StringTable的存取速度。通过StringTableSize参数可以调整HashTable桶的个数,默认为60013个。
六、直接内存
定义
本机系统中的内存
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但是读写性能高
- 由于不是JVM运行时数据区的一部分,所以不受JVM垃圾回收机制的影响
直接内存的底层原理
java要读取大文件,首先CPU要切换到内核态,然后调用Native方法将磁盘文件先读入到系统缓存区,然后从系统缓冲区复制到java缓冲区,过程中需要两次复制。
而使用直接内存之后,可以在系统内存中分配一块区域用于存储磁盘文件,并且Java中也可使用DirectByteBuffer作为这块内存的引用进行操作,这样就能在一些场景中显出提升性能。
直接内存溢出
如果直接内存中存储的数据过大也会导致OutOfMemory异常:
直接内存分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类DirectByteBuffer内部使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleanr的clean方法调用freeMemory方法释放直接内存。
当直接内存使用较多,而且使用-XX:DisableExplicitGC禁用掉代码中System.gc()时,建议直接使用Unsafe对象对直接内存进行管理,示例代码如下:
更多推荐
所有评论(0)