JVM、JRE和JDK的区别?
JVM:Java虚拟机
JRE:Java运行时环境 = JVM+基础类库
JDK:Java开发工具包 = JVM+基础类库+编译工具
1. 内存结构
1.1 程序计数器(寄存器的抽象)
-
实质:操作系统的寄存器
-
作用:记住下一条 JVM 指令的执行地址
JVM 指令 到 CPU 上运行的流程:
jvm指令->放入程序计数器->解释器从程序计数器中获取 jvm 指令 -> 解释器将其转变为机器码->CPU 运行机器码
-
特点:
-
程序计数器是线程是私有的
体现在进行线程切换的时候:线程1 切换为线程2,程序计数器需要记住线程1目前已经执行到了哪一条指令,以便于线程1下次在 CPU 上运行时要从哪里开始执行。
-
不会存在内存溢出
-
1.2 虚拟机栈
-
定义:线程运行需要的内存空间
一个栈由多个栈帧组成
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)
垃圾回收是否涉及栈内存?
不涉及,栈内存的分配直接通过入栈来完成,栈内存的清理直接通过出栈来进行。
栈内存分配是否越大越好?
不是。运行代码时,可以通过 -Xss 指定栈内存大小。默认大小为1024 KB(Windows 根据虚拟内存来定)
栈内存越大,最大运行的线程数反而会更少,因此运行速度不一定快。只是能够进行更多次的方法递归调用。
方法内的局部变量是否是线程安全的?
需要看局部变量是否逃离方法的作用范围
-
如果方法内的局部变量没有逃离方法的作用范围(比如没有作为返回值),则是线程安全的。
因为一个线程对应一个栈,线程内的每次方法调用都会产生一个新的栈帧,栈帧内的局部变量是私有的,所以线程安全。
如果是 static 关键字修饰的则不是线程安全。因为static 变量会被存放在方法区,而非栈内存,属于共享内存区域,因此不是线程安全的。
-
如果逃离了方法的作用范围(比如作为参数/作为返回值),需要考虑线程安全问题。
1.2.2 栈内存溢出
-
原因:
-
栈帧过多(方法递归太多次)
-
栈帧过大
1.2.3 线程诊断常见问题:
CPU 占用过高怎么排查?
首先通过 top 命令找到 CPU占用很高的那个进程ID
然后通过 ps H -eo pid,tid,%CPU |grep + 端口号 查看某个进程的进程id、线程id、CPU占用率,看到底是哪个线程占用CPU很高,记录下这个线程ID。
然后通过 jstack + 进程ID 打印这个进程的所有线程。(注意:打印出来的线程ID为十六进制,需要进行换算!),找到对应的线程,再定位到对应的代码行数进行 debug。
jstack + 进程ID 也能够排查死锁问题!
1.3 本地方法栈
Java虚拟机调用本地方法的时候需要为这些方法分配内存空间。
本地方法:那些不是由Java代码编写的,由C/C++编写的本地方法(有时需要和操作系统打交道)。
1.4 堆
-
定义:通过 new 关键字创建的对象都会使用堆内存。
-
特点:
-
它是线程共享的,堆中对象都需要考虑线程安全的问题
-
有垃圾回收机制
-
1.4.1 堆内存溢出
(OutOfMemoryError)
可以通过 -Xmx 修改堆内存大小(默认为 4G)
1.4.2 堆内存诊断
-
jmap
首先通过 jps 命令查看当前运行的进程
然后通过 jmap -heap + 进程 ID 打印出来当前 Java 进程的堆内存使用情况
-
jconsole
可以直观地查看某个进程的堆内存使用情况
1.5 方法区
-
定义:在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存(由操作系统管理的内存空间,不由 JVM 管理内存了)。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
1.5.1 StringTable
-
本质:哈希表
String s1= "a" 过程:
会先从 StringTable 中查找是否有 "a",如果有,拿来直接用,没有的话则将 "a" 变为 String 对象,并且放入 StringTable 中。每个字符串在 StringTable 中是唯一的。
判断两个字符串对象是否相等:
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder()
System.out.println( s3 == s4 );
}
答案:false
s3 存储在 StringTable (串池)当中
s4 是通过StringBuilder 拼接 s1 和 s2 得到的对象,存储在堆里面
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder()
String s5="a"+"b";
System.out.println( s3 == s5 );
}
答案: true
"a"+"b" 直接在编译期间确定为 “ab” 因此 s5 是直接从 StringTable 中找到的,所以 s3==s5
-
JDK1.8 后可以通过字符串对象的 intern( ) 方法尝试将某个字符串对象放入 StringTable 中,如果有则不会放入,但仍然会返回 StringTable 中的字符串。
-
JDK1.6之前则是复制一个新的字符串对象再把新的对象放入 StringTable 。
JDK 1.8后:
public static void main(String[] args) {
// StringTable: ["ab","a","b"]
String s1=new String("a")+new String("b"); // new String("ab") 堆中
String s2=s1.intern();
System.out.println(s2=="ab");
System.out.println(s1=="ab");
}
运行结果:
true
true
s1.intern() 将 s1 放入了 StringTable中
因此 s1=="ab" s2=="ab"
如果是 JDK 1.6 则运行结果为:
true
false
public static void main(String[] args) {
// StringTable: ["ab","a","b"]
String x="ab";
String s1=new String("a")+new String("b"); // new String("ab") 堆中
String s2=s1.intern();
System.out.println(s2==x);
System.out.println(s1==x);
}
运行结果:
true
false
因为 "ab" 在一开始已经被放入 StringTable 中了
s1.intern() 的时候发现 StringTable 中已经存在 "ab" ,所以不会将 s1 放入 StringTable,s1 仍然是放在堆内存中的。
虽然 StringTable 中已经存在 "ab" ,但 s1.intern() 仍然会返回 StringTable 中的字符串对象,所以 s2==x 返回为 true