JVM入门
jvm基础
什么是jvm
定义:Java Virtual Machine -java 程序的运行环境(java 二进制字节码的运行环境)
好处:
- 一次编写,到处运行的基石
- 自动内存管理机制,垃圾回收
- 数组下标越界检查
- 多态
内存结构
程序计数器 PC Register
定义
Program Counter Register 程序计数器(寄存器)
- 在程序执行过程中,记录下一次jvm指令的执行地址
- 在代码运行过程中,java代码会被编译成有序的jvm指令,再由解释器解释成程序能够识别的二进制指令
- 在上一条指令执行完成后,程序会从程序计数器中获取下一条指令的地址
- 特点:
- 线程私有
- 不会存在内存溢出的区
虚拟机栈
- 先进后出结构
- 线程运行时需要的内存空间(线程私有)
- 虚拟机栈由多个栈帧组成(每个方法运行时需要的内存-》参数、局部变量、返回地址)
- 每个线程只能有一个活动栈帧,对应着正在执行的那个方法
public static void main(String[] args) {
method1();
}
private static void method1() {
method2(1,2);
}
private static int method2(int a ,int b) {
int c = a + b;
return c;
}
备注:debugger运行,可通过Frames变化查看当前线程对应虚拟机栈的内存变化,入栈操作为main-》method1-》method2,实际的执行顺序为反序
问题:
- 垃圾回收是否涉及栈内存?
不会:栈内存在每次方法执行结束后都会被出栈,释放内存,所以不需要垃圾回收 - 栈内存分配越大越好吗?
栈内存的默认大小都是1024kb(mac,linux…)
在windows环境下的默认大小是根据内存大小进行分配的
在相同内存大小下,如果栈分配的内存大小不一致,栈内存大的程序所能调用的线程数会比栈内存小的程序少 - 方法内的局部变量是否线程安全?
线程安全,方法内的局部变量对每个线程都会分配一份单独的内存空间,每个栈内的内存空间互不影响,如果变量是共享的,会存在线程安全问题。
变量是否线程安全:
- 是否是方法内的局部变量
- 局部变量是否逃离方法的作用范围(参数或返回)
- 如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
- 栈内存溢出
- 栈内栈帧过多(方法的递归调用)
- 栈帧过大(一个栈帧大小就超过了栈内存大小)
private static int count;
public static void main(String[] args) {
try {
method3();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(count);
}
}
private static void method3() throws Exception {
count++;
method3();
}
默认栈内存大小下,递归发生了23522次
修改VM option栈内存大小参数 Xss256k 后:-递归次数变为3558
java.lang.StackOverflowError
- 线程运行诊断
- 案例1:cpu占用过多
定位:
top命令查看当前系统中进程占用资源情况
ps H(线程中的进程数) -eo(显示的字段) pid,tid,%cpu -展示进程中线程的属性展示
jstack 进程id 查看进程中的线程信息
根据进程id找到有问题的线程,进一步定位到问题代码的源代码行数 - 案例2:程序运行很长时间没有结果
jstack 进程id 查看进程中的线程信息
找到线程死锁代码
本地方法栈-Native method stack
给本地方法的运用提供内存空间(java代码存在局限,无法直接和操作系统进行交互,需要通过C等代码编写的native方法 和操作系统形成交互)
堆-Heap
通过new关键字,创建对象都会使用堆内存
- 特点:
- 线程共享,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
- 堆内存溢出
堆内的对象都存在引用,且一直存在对象的新增
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
Java.lang.OutOfMemoryError: Java heap space
-Xmx8m:设置堆内存大小
- 堆内存诊断
jps工具
查看当前系统中有那些java进程
60064 RemoteJdbcServer
7424 RemoteMavenServer
10728
1544 Test3
33400 ApiApplication
53148 Launcher
6940 Jps
jmap工具
查看堆内存占用情况:jmap -heap 进程id
- 堆配置
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4261412864 (4064.0MB)#最大内存配置
NewSize = 88604672 (84.5MB)#新生代内存
MaxNewSize = 1420296192 (1354.5MB)#最大新生代内存
OldSize = 177733632 (169.5MB)#老年代内存
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB #元空间大小
G1HeapRegionSize = 0 (0.0MB)
- Eden区内存使用情况
Eden Space:
capacity = 66584576 (63.5MB)
used = 1331712 (1.27001953125MB)
free = 65252864 (62.22998046875MB)
2.0000307578740157% used
- jconsole工具
图形界面的,多功能的监测工具,可以连续监测:jconsole - 案例:垃圾回收后,内存依旧占用很高
jvisualvm 可视化工具工具
查看jvm参数等:
方法区
所有java虚拟机进程共享的区域
- 存储类结构相关信息:成员变量、方法数据、成员方法、构造方法、特殊方法
- 运行时常量池
- 在虚拟机启动时被创建
- 逻辑上是堆的存储部分
- 方法区内存溢出
hotspot 在jdk1.8之前在堆空间中划分永久代保存方法区内的数据
1.8以后,永久代被替代为元空间,且使用系统物理内存作为数据存储
一般不会出现内存溢出问题,需要修改相关jvm参数
-XX:MaxMetaspaceSize=8m:修改元空间最大内存为8m
public static void main(String[] args) {
int j = 0;
try {
Test4 test4 = new Test4();
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter wa = new ClassWriter(0);
//版本号,public,类名,包名,父类,接口
wa.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回byte
byte[] code = wa.toByteArray();
//执行了类的加载
test4.defineClass("Class"+i,code,0,code.length);
}
} finally {
System.out.println(j);
}
}
结果:
Error occurred during initialization of VM
MaxMetaspaceSize is too small.
场景:
- spring
- mybatis
运行时常量池
- 静态常量池:.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念;
- 运行时常量池:则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池,除此外,运行期间也可以将新的常量放入池中,如String类的intern()方法,会在常量池查找是否存在一份equal相等的字符串,如果有则返回该字符串的引用,否则自己添加字符串进入常量池。
优点:
- 实现了对象的共享,避免了频繁的创建和销毁对象而影响系统性能
- 节省内存空间,如字符串常量池
- 合并相同字符串只占用一个空间
- 节省运行时间,进行比较时==比equals快,只判断引用是否相等就可以判断实际值是否相等
如:
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
s1 == s2 作为直接赋值,使用的字符串字面量,在编译时会直接加载到class文件的常量池中,内容被合并且指向一致
s1==s3 s3对象为拼接对象,但是作为拼接的两个字符串也是字面量,在编译时会被优化为s1 = “Hello”,等同于s1和s2
s1==s4 s4中存在new的新对象,其地址为堆中地址,与常量池地址不一致,需要在等到运行时才会知道地址
s1==s9 s9为拼接对象,虽然s7,s8中使用的是字符串字面量,但是拼接s9时,s7,s8作为两个变量,地址不可预料,不能在编译时确定,所有不存在优化
s4==s5 两个都是new出来的堆对象,地址不可能一致
s1==s6 s6使用intern()方法在常量池中寻找,如果有直接调用常量池中的地址
特例:static修饰符
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
结果是一致对象。
原因:static修饰方法在编译时直接定死了AB的值和地址,所以s值也为定值
特例2:静态代码块
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
并不是同一个对象
原因:AB为变量,但是没有直接赋值,程序不知道它们何时赋值和赋予什么样的值,那么s值就不能作为一个定时被初始化,只能在运行时创建。
注意:
- 运行时常量池中的常量基本来源于class文件中的常量池
- 程序运行时,除非手动向常量池中添加常量(intern方法),否则jvm不会自动添加常量到常量池中
基本数据类型和其包装类中除了Float和Double外都实现了常量池技术,但是数值类型的常量池不能手动添加常量,程序启 动时常量池中的常量就已经确定了.
如整形常量池中的常量范围:-128-127,(Byte,Short,Integer,Long,Character,Boolean)这五种包装类默认创建了数值【-128,127】之间的相应类型缓存数据,超过此范围后都会创建新的对象到堆中。
- StringTable 串池
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池机制,避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(jdk1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
位置:
1.6时,是跟随常量池放在永久代空间中
1.7,1.8后在堆中单独开辟一个空间存放StringTable
原因:
- 永久代的回收效率不高
直接内存-Direct Memory
- 常见于NIO操作,用于数据缓冲区
- 分配回收成本高,但读写性能高
- 不受jvm内存回收管理
- 相较于原始的io操作,原始io在进行文件读写时需要先由操作系统划分内存空间,再由java程序划分内存空间,进行内容的复制,最后交由内核进行内容写入
直接内存同样也是系统开辟内存空间,但是这块内存空间java程序可以直接访问,减少了java自己开辟空间以及进行内容拷贝的时间
- 内存溢出
static int defaultMember = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(defaultMember);
list.add(byteBuffer);
i ++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
-XX:+DisableExplicitGC:禁用显示的垃圾回收
System.gc:显示的垃圾回收,FUll GC,导致程序出现较长时间的停滞