JVM第一篇,JVM的内存结构。
1. 程序计数器
Program Counter Register
- 作用:存放下一条指令所在单元的地址的地方,物理上使用寄存器来实现的。
- 特点:
- 线程私有。
- 唯一一个不会存在内存溢出的区域。
2.虚拟机栈
Java Vitural Machine Stacks
- 虚拟机栈:每个线程运行时所需要的内存。
- 栈帧:每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,当前栈内最顶部的栈帧,对应着当前正在执行的那个方法。
栈内存不需要进行垃圾回收。
栈内存划的大,方便更多次的方法调用,划的过大,会让线程数变少,因为物理内存是一定的。
方法内局部变量是线程私有的,不需要考虑线程安全,如果是公有的,需要考虑线程安全。
线程安全
判断一个变量是不是线程安全的,不仅要看他是不是方法内的局部变量,还要看他是否逃离了方法的作用范围,如method3。
1 | // 多线程同时执行此方法 |
1 | public class Demo1_1 { |
栈内存溢出
-
栈帧过多超过栈内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Demo_2{
private static int count;
public static void main(String[] args){
try{
method1();
} catch (Throwable e){
e.printStackTrace();
System.out.println(count);
}
}
private static void method1(){
count ++;
method1();
}
}
// java.lang.StackOverflowError
// 又如,自动转换Json时,部门类套职员类,职员类套部门类,无限嵌套。这时使用@JsonIgnore忽略员工的部门的转换,使用
-Xss256k
设置栈内存大小,使递归调用次数变大。 -
栈帧过大超过栈内存
即该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,即栈帧过多,会抛出
StackOverflowError
异常; - 栈进行动态扩展时如果无法申请到足够内存,会抛出
OutOfMemoryError
异常。
线程运行诊断
-
Demo 1: CPU占用过多
-
定位
-
使用linux的top命令定位哪个ID的进程对CPU的占用过高
1
top
-
使用ps查看进程的哪个线程占用率过高
1
ps J -eo pid,tid,%cpu | grep 进程ID
-
使用jstack命令查看有问题的线程,展示的线程ID为十六进制,可定位到问题代码的行数。
1
jstack 进程id
-
-
-
Demo 2: 程序运行很长时间没有结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException{
new Thread(()->{
synchronized(a){
try{
Thread.sleep(2000);
} catch(InterruptedException e){
e,printStackTrace();
}
synchronized(b){
System.out.println("我获得了a和b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized(b){
synchronized(a){{
System.out.println("我获得了a和b");
}
}
}).start();
}
// deadlock 死锁
// 线程1先锁住a然后休眠2秒,在其休眠这段时间一秒后新线程2锁住了b,当线程2锁线程a时发现已经被锁了需要等待。再过一秒线程1醒过来,想要锁住线程b但是需要等待,于是死锁。
3. 本地方法栈
Native Method Stacks,本地方法运行时候使用的内存。
本地方法:本地方法由其他语言如C或C++编写,编译成与处理器相关的机器代码。
4.堆
通过new关键字,创建对象都会使用堆内存。
- 线程共享,堆中的对象都需要考虑线程安全的问题。
- 有垃圾回收机制,不再被引用的对象会被回收。
堆内存溢出
1 | public static void main(String[] args){ |
使用-Xmx8m
修改堆空间大小。
堆内存诊断
-
jps工具
查看当前系统中有哪些java进程。
-
jmap工具
查看某一时刻堆内存占用情况。
1
jmap -heap 进程ID
-
jconsole工具
图形界面的多功能的检测工具,可以连续监测。
-
jvisualvm工具
使用
堆Dump
堆转储对堆内存信息进行快照。
Demo_1: 多次执行垃圾回收后,内存占用仍然很高。
1 | public class Demo{ |
5.方法区
Method Area
方法区,也称非堆(Non-Heap),又是一个被线程共享的内存区域。方法区用于存放class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等等。另外,方法区包含了一个特殊的区域“运行时常量池”。
方法区内存溢出
Demo
1 | // 1.8放在了元空间,1.8前放在永久代。元空间内存溢出,默认使用物理内存,不限制大小,因此默认不会看到溢出 |
Java1.8使用 -XX:MaxMetaspaceSizer=8m
设置最大元空间大小。
Java1.8前使用 -XX:MaxPermSize=8m
设置最大元空间大小。
场景
动态加载类
- Spring
- MyBatis
spring aop中都是使用到了cglib这类字节码的技术,动态代理的类越多,就需要越多的方法区来保证动态生成的class可以加载入到内存中去。
运行时常量池
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
可以通过javap -v命令反编译.class文件查看。
StringTable
特性
String table又称为String pool,字符串常量池,其存在于堆中(jdk1.7之后改的),hashtable结构,不能扩容。最重要的一点,String table中存储的并不是String类型的对象,存储的而是指向String对象的索引,真实对象还是存储在堆中。
String table还存在一个hash表的特性,里面不存在相同的两个字符串,延迟加载遇到没见过的才加进去。
此外String对象调用intern()方法时,会先在String table中查找是否存在于该对象相同的字符串,若存在直接返回String table中字符串的引用,若不存在则在String table中创建一个与该对象相同的字符串。
1 | public class Demo1_1 { |
存放位置
1 | // JDK 8下设置 -Xmx10m -XX:UseGCOvereadLimit |
在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
垃圾回收
当内存不足时,StringTable中那些没有被引用的字符串仍然会被回收。
1 | /*-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc*/ |
性能调优
-
调整hash桶的个数。如果系统里字符串常量非常多,可以适当调大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// -XX:StringTable=20000 -XX:PrintStringTableStatistics
public class Solution {
public static void main(String args[]) {
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("cost" + (System.nanoTime() - start) / 1000000);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 200000 401ms
// 1009 12000ms -
考虑将字符串对象是否入池。如果应用里有大量的字符串而且可能会重复,则可以考虑让字符串入池减少堆内存个数。
1 | public class Solution { |
6.直接内存
Direct Memory,不是JVM的内存,属于操作系统的内存。
- 常见于NIO操作时,用于数据缓冲区。
- 分配回收成本较高,但读写性能高。
- 不受JVM内存回收管理。
直接内存溢出
1 | public class Solution { |
释放原理
1 | public class Solution { |
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法。
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
禁用显式回收堆直接内存的影响
1 | -XX:+DisableExplicitGC # 显式GC,使System.gc();无效 |
1 | System.gc(); // 显示垃圾回收,Full GC,回收新生代老年代,造成程序暂停时间较长 |
上述代码段,不通过代码显式回收ByteBuffer回收,于是ByteBuffer只有等待真正垃圾回收才会被回收掉,内存会长时间占用较大。
所以可以用Unsafe的freeMemory方法来手动管理直接内存。