程序计数器
作用
记住下一条
JVM
指令的执行地址。
每次执行一条
jvm
指令的同时,也会把下一条
jvm
指令的地址给到程序计数器,之后执行都可以通过程序计数器的地址取下一条指令。
Java
源代码到
cpu
运行的整个流程:
- java 源代码先被编译器 javac.exe,编译成字节码文件(.class)。
- 然后字节码文件再被 java.exe 执行。
- java.exe会将字节码文件中的 JVM 指令,通过解释器编译成机器码再交给cpu执行。
特点
- 线程私有。 每个线程有一个单独的程序计数器,当线程在时间片还没执行完,通过程序计数器记录上次程序执行到哪个位置。
- 不会存在内存溢出。
虚拟机栈(一般指Java栈)
栈&栈帧的关系
- 栈:用于存储栈帧,先进后出,类似弹夹。
- 栈帧: 线程每执行一个方法,就会产生一个栈帧存储到栈。当方法执行完后,就会释放该栈帧的内存。栈帧用于存储局部变量,局部变量随栈帧销毁而消失。
定义
- 每个线程运行时所需要的内存,就是虚拟机栈。
- 每个线程只有一个活动栈帧,也就是线程运行到对应的那个方法。
FAQ
1.
垃圾回收是否涉及栈内存?
- 不会。因为方法执行完之后,就会自动释放栈帧的内存。
2.
栈内存分配越大越好吗?
- 不是。 因为物理内存是固定的,假设每个栈内存设置1M,物理内存总共50M,那么最多就能同时运行 50个线程。如果设置为2M,那么最多只能执行25个线程。 栈内存设置大了,可以存放更多栈帧,线程可以递归调用更多次,但是运行效率不会提升。
3.
方法内的局部变量是否线程安全?
- 如果方法内的局部变量没有在方法范围之外,那么就是线程安全的。
- 如果局部变量引用对象,且出了方法范围之外,那么可能线程不安全(基本类型不会)。
// m1的StringBuilder线程安全,因为没有逃离方法之外。
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// m2的StringBuilder线程不安全,因为m2的sb参数来自于外界
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// m3的StringBuilder线程不安全,因为m3的sb参数返回到外界
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
栈溢出(StackOverFlowError)
通常是方法递归过深导致的。
cpu占用过高排查步骤
1. top
命令,找到
java
的
pid(
进程号
)

2. ps H -eo pid,tid,%cpu | grep pid
,找到该进程的所有线程,定位哪一个线程引起
cpu
占用问题。

3. jstack pid
,可定位到哪个线程出了问题,并且可以定位到源代码哪一行(步骤
2
的
tid
换算成
16
进
制找到才能哪个线程出了问题)

线程死锁排查步骤
1. jstack pid
,翻到最后看到这个信息

2.
查看源码
29
行和
21
行

3.
线程
1
锁住
a
,睡眠
2
秒。然后线程
2
锁住
b
,再锁住
a
,但是
a
被线程
1
锁了。线程
1
苏醒,然后想锁
住
b
。两个线程互相等待,导致线程死锁。
本地方法栈
本地方法栈:本地方法运行时用到的内存
,就是本地方法栈。
有些功能不能由
JAVA
代码直接实现,需要间接使用本地方法来实现,此类方法由
native
修饰。
堆
堆内存能存储对象、数组。
特点
- 线程共享,所有线程都能访问堆中的对象。
- 存在垃圾回收机制。
堆内存诊断工具
如果环境变量没有配置好,以下命令会用不到
jps:查看当前系统中有哪些java进程

jmap:查看堆内存占用情况 (命令:jmap -heap pid)

堆内存配置信息

堆内存使用情况(年轻代)

jconsole:图形化界面

jvisualvm:图形化界面(实用)
点击“堆 dump”可以某一时刻的堆空间情况进行快照
可查看该时刻有哪些对象占用内存空间比较多
方法区
在
JDK1.6
时,方法区通过永久代来实现(方法区是一种规范)。该方法区主要存储类的信息,类加载器,运行时常量池等。
到了
JDK1.8
,永久代被元空间代替,并且不再由
JVM
来管理元空间的内存。StringTable(字符串池)也改成放到堆内存中。
元空间代替永久代的意义:
把方法区放到本地内存后,
JVM
的内存也变得充裕了,而且垃圾回收机制也交给了元空间管理,垃圾回
收的效率也相对提高了。
StringTable
1. 常量池中的字符串仅仅是符号,当代码被执行到该字符串时,才被创建为字符串对象,并放入串池。

2. 字符串变量加号拼接,实际上是StringBuilder的拼接。
String a = "a";
String b = "b";
String s1 = "ab";
String s2 = a + b; // new StringBuilder().append(a).append(b).toString(); new String("ab")
System.out.print(s1 == s2); // false
s2
是
StringBuilder
拼接的结果,
s2
指向的是堆中
String
对象的地址。
s1
是直接的字面量,指向的是串池中的字符串对象的地址。二者不是同一个对象,
false
。
3. 字符串的常量拼接,在编译期已经优化。
String a = "a";
String b = "b";
String s1 = "ab";
String s2 = "a" + "b";
String s3 = a + b;
System.out.print(s1 == s2); // true
System.out.print(s1 == s3); // false
s2
是两个常量的拼接,在编译期间的结果是不会变的,在编译期间就被确定为是字符串
"ab"
,和执行
s1 时是同一个字符串。
s3
是两个变量的拼接,在编译期间可能会变化,所以需要
StringBuilder
的方式来动态拼接字符串。
4. 串池中的字符串是唯一的。
System.out.print("1");
System.out.print("2"); // 串池中有1和2
System.out.print("1");
System.out.print("2"); // 串池还是只有1和2,因为前两行已经创建过,后面两行不会再创建
5.intern()方法的特性。
intern
方法的作用就是把字符串放入串池中。
JDK 1.7之后
例子1:
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == "ab"); //true
System.out.print(s2 == "ab"); //true
第一行代码执行完,
StringTable
中含有
[a,b]
,此时
s1
是指向堆中一个
String
对象的地址。
第二行代码执行完,成功把
ab
放入串池,所以
s1
指向的是串池中的
ab
字符串对象,
StringTable
中含有 [a,b,ab],
s2
返回的也是串池中的
ab
对象。
例子
2
:
String s = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == s); //false
System.out.print(s2 == s); //true
在例子
1
的基础上,在第一行加上一行代码。
第一行执行完,
StringTable[ab]
。
第二行执行完,
StringTable[a, b, ab]
。
第三行执行完,
ab
字符串已存在于
StringTable
,所以放入串池失败,
s1
依旧是堆中的
String
对象,但是 s2是串池中的对象。
JDK1.6
JDK1.7
之后的
intern
方法,和
JDK1.6
不一样。
JDK1.7
之后,
intern
方法是把原来的对象放入串池中。
而
JDK1.6
,
intern
方法是把原来的对象复制一份,放入串池,因此不是相同的对象。
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
System.out.print(s1 == "ab"); //false
System.out.print(s2 == "ab"); //true
第一行执行完,
StringTable[ab]
。
第二行执行完,
StringTable[a, b, ab]
。
s1
对象被复制了一份,放入到串池,所以
s1
仍然是堆中的
String 对象,而s2
返回的是串池的对象。
6. JDK1.7以后,StringTable从永久代迁移到堆内存的意义
在
JDK1.6
的时候,
StringTable
在永久代的回收效率非常低,触发回收的条件非常苛刻,需要
FullGC
才会触发。JDK1.7
后,迁移到了堆中,触发
MinorGC
就可以回收。
7. StringTable的性能调优
方案一:把StringTableSize调大,也就是把桶个数调大。
StringTable的底层其实是一张哈希表,把桶的个 数调大实际就是为了减少哈希碰撞的概率,提高查找字符串的效率。可以类比HashMap,当两个元素的哈希值一样时,就会形成链表,链表的查询速度是比较慢的,如果链表长度过长,那么查询效率就会非常低了。当大量字符串需要高效地放入串池,可以考虑这种方式优化。
方案二:考虑将字符串对象是否入池。
如果需要存放大量字符串,且有大量字符串重复,可以考虑入池,对相同的字符串对象做“去重”,减少堆空间占用的内存。
直接内存
定义
直接内存不属于JVM
管理的内存,属于操作系统的内存。
特点
- 分配和回收成本高,但是读写性能高。
- 不受JVM内存回收管理。
原理
使用byte[]做缓冲区,需要先从磁盘文件,把字节流读取到系统缓存区,然后再读取到JVM的byte[]缓冲区。
使用直接内存,磁盘文件的字节流就可以读取到直接内存,然后
JVM
就可以直接从直接内存读取,少了读取到byte[]
缓冲区这一步骤,所以使用直接内存读写效率会更高。

内存释放
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法。
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调freeMemory来释放直接内存。
演示代码:
private final static int _1GB = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
long base = unsafe.allocateMemory(_1GB) ;
unsafe.setMemory(base, _1GB, (byte)0);
System.in.read();
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe)f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
禁止显式调用:
- 所谓的显式调用,是指在 java 代码里使用System.gc()来回收直接内存。
- 这种方式一般不推荐使用,因为使用这种方式触发的是一次 FullGC,会影响整个系统的性能。
- 为了禁止程序员使用这种方式回收直接内存,可以使用 -XX:+DisableExcplicitGC 来使 System.gc() 这种方式无效化。
- 所以涉及到直接内存回收,最好的方式还是使用Unsafe的方式来回收。