一、JVM深入浅出
1.什么是JVM
定义:Java Virtual Machine----java程序的运行环境(java二进制字节码的运行环境)
好处:
- 一次编译,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
比较JVM、JRE、JDK 的关系 逐级向上包含的关系
基础类库:java.lang.*,集合类,线程类,io类,日期类
tips:
jvm是运行环境,是个空壳,结合了基础类库,才能构成完整意义上的java运行时环境
2.学习JVM有什么用
1. 面试:简单面试题衡量不出java程序员之间的差异,提高面试中的竞争力
2. 理解底层的实现原理:往长远发展,理解底层必备,比如,自动装拆箱,for增强,动态代理
3. 中高级程序员必备技能:内存溢出,响应缓慢,快速定位解决实际生产问题
3.常见的JVM
tips:
jvm是一套规范,只要遵从这套规范,你自己就可以开发一套jvm的实现,很多公司有自己的jvm。
我们主要学习HotSpot为准,其它的实现不尽相同
4.JVM的学习路线
jvm主要模块
一、类加载器ClassLoader
类从java源代码编译成java二进制字节码文件,还需要经过类加载器,才能够被加载到JVM里去运行
二、内存结构
- 类放在方法区
- 类创建的实例即对象放在堆中
- 堆里面的对象在调用方法时会用到虚拟机栈,程序计数器,本地方法栈
三、执行引擎
方法执行时每行代码是由执行引擎中的解释器逐行进行执行
方法里的热点代码(被频繁调用的代码)会被即使编译器进行优化后的执行
GC对堆中一些不再被引用的对象进行垃圾回收
四、本地方法接口
java不方便实现的功能会调用底层操作系统的功能,通过本地方法接口(常见Object类带有native修饰的方法,没有方法体)调用操作系统提供的功能方法
二、JVM内存结构
1、程序计数器(寄存器)
英文全称:Program Counter Register
作用
记录这下一条JVM指令的执行地址
JVM指定执行流程:
1、JVM指令通过解释器(java执行引擎的解释器)翻译成一条机器码,然后机器码交给CPU运行,但与此同时,会把下一条指定地址放入程序计数器。
2、当第一条指定执行完后,解释器会到程序计数器去取下一条指定地址找到对应指令,重复1的步骤
因此:程序计数器是记录这下一条JVM指定的执行地址,如果没有程序计数器,不指定,接下来不知道该执行哪条命令
tips:物理上,程序计数器是通过寄存器来实现的,寄存器是CUP组件里读取最快的一个单元
程序计数器是java对物理硬件的屏蔽和抽象
特点
-
线程私有的
java程序支持多线程,多线程运行是存在CPU分时调度,由于程序计数器是每个线程所私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
-
java虚拟机规范中,唯一一个不会存在内存溢出的区域
2、本地方法栈
本地方法提供的内存空间,叫本地方法栈
本地方法:java代码有一定的限制,不能直接和操作系统底层打交道,需要用c,c++编写的本地方法来真正的与操作系统更底层的打交道,java可以通过本地方法来调用底层功能
3、虚拟机栈
3.1 栈演示
栈结构图解:类似生活中的子弹夹
入栈:
出栈
特点:
1、栈是先进后出(FILO – First In Last Out)有序列表,
2、栈是限制线性表中元素的插入和删除只能在同一端进行的一种特殊线性表。
3、允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
4、最先放入栈中元素在栈底,最后放入的元素在栈顶, 而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
Java Virtual Machine Stacks 定义
1、每个线程运行时所需要的内存,称为虚拟机栈,多个线程运行就有多个虚拟机栈
2、每个栈由多个栈帧(Frame)组成,每个栈帧对应着每次方法调用时所需要的内存(由于线程最终是需要去执行代码的,而代码是由一个个方法组成,因此每个方法运行时需要的内存叫栈帧,.每调用一次方法时,给每个方法划分栈帧空间,并压入栈内,当方法执行完毕,会把方法对应的栈帧出栈即释放方法所占用的内存)
3、每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
public class StacksTest {
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;
}
}
断点看方法执行过程,第一步
继续走
3.2 栈问题辨析
3.2.1 垃圾回收是否涉及栈内存
不需要,栈是每个个方法调用产生的栈帧内存,而栈帧内存在每次方法调用结束后会自己弹出栈,会自动被回收掉。垃圾回收只是去堆内存无用对象
3.2.2 栈内存分配越大越好吗?
栈内存越大,线程数越少,
因为物理内存大小一定,线程使用的是栈内存
假设总共物理内存500M, 1个线程用1M内存,理论上有同时有500个线程可以同时运行
如果每个线程的栈内存设置成2M内存,那么同时最多只能有250个线程同时运行,
划分大只是为了更多次的递归调用,并不能增强运行效率,一般采用系统默认的栈内存就可以
3.2.3 方法内的局部变量是否线程安全
判断依据该变量多个线程共享的,.还是每个线程私有的
以上代码x的值不会混乱,因为x是方法的局部变量,一个线程对应一个栈,线程内每一次方法调用都会产生一个新的栈帧,每个线程都有自己一个私有的局部变量X,互不干扰,每个线程最后得出的值一样
再看一段代码
package com.jt.test;
/**
* 局部变量的线程安全问题
*/
public class ThreadTest2 {
public static void main(String[] args) {
//主线程修改sb对象
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
//新线程也在修改sb对象
new Thread(() -> m2(sb)).start();
}
public static void m1() {
// sb 作为方法m1()内部的局部变量,是线程私有的 ---> 线程安全
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
/**
* sb 作为参数传进来,有可能有其它的线程访问到它,不在是线程私有的
*/
public static void m2(StringBuilder sb) {
// sb 作为方法m2()外部的传递来的参数,sb 不在方法m2()的作用范围内
// 不是线程私有的 ---> 非线程安全
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
/**
* 返回sb,其它线程可能拿到了sb,逃离了方法作用范围,不是线程安全的
*/
public static StringBuilder m3() {
// sb 作为方法m3()内部的局部变量,是线程私有的
StringBuilder sb = new StringBuilder();// sb 为引用类型的变量
sb.append(1);
sb.append(2);
sb.append(3);
return sb;// 然而方法m3()将sb返回,sb逃离了方法m3()的作用范围,且sb是引用类型的变量
// 其他线程也可以拿到该变量的 ---> 非线程安全
// 如果sb是非引用类型,即基本类型(int/char/float...)变量的话,逃离m3()作用范围后,则不会存在线程安全
}
}
结论:
如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
3.3 栈内存溢出
StackOverflowError
3.3.1 栈帧过多导致栈内存溢出
3.3.2 栈帧过大导致内存异常
4、线程运行诊断
线程运行与虚拟机栈息息相关
4.1 CPU占用过多
定位:
用top定位哪个进程对CPU占用过高
由上面top中cpu占用最高的进程id过滤哪个线程的问题 ps H -eo pid, tid, %cpu | grep 1896
ps H -eo pid, tid(线程id), %cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的CPU占用过高)
jstack 进程id 可以根据线程id找到有问题的线程,进一步定位到代码的源代码行数
输出的线程编号是16进制的,需要10进制转换为16进制
4.2 程序运行很长时间没有结果
等很久没有输出结果,可能多个线程造成死锁
jstack命令 jstack 1863
代码:
package com.jt.test;
public class DeadLock {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = new Object();
new Thread(() -> {
synchronized (o1){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName()+"获得锁o1,o2");
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (o2){
synchronized (o1){
System.out.println(Thread.currentThread().getName()+"获得锁o1,o2");
}
}
}).start();
}
}
总结:程序计数器,虚拟机栈,本地方法栈都是线程私有的
5 堆
Heap:通过new 关键字创建的对象都会使用堆内存
特点:
1.线程共享的,堆中的对象都需要考虑线程安全问题
2.有垃圾回收机制(不在被引用的对象)
5.1 堆内存溢出
Java heap space
疑问:堆里有垃圾回收机制,当对象不在被使用,垃圾会回收即对象占用的内存会释放掉,为什么还会溢出呢
解惑:不断产生对象,而且这对象一直有人使用,不能被回收,这样的对象达到一定的数量就会导致堆内存溢出
5.2 堆内存诊断
- jps工具:查看当前系统中有哪些进程
- jmap工具:查看堆内存占用情况 -heap 进程id
- jconsole工具:图形界面,多功能的监测工具,可以连续监测
- jvirsalvm
案例:垃圾回收后,内存仍然占用很高
6 方法区
6.1 定义
- 方法区在虚拟机启动时创建,是java虚拟机线程共享的区域,存储着类的结构相关信息(成员变量,方法数据,成员方法,构造器方法,特殊方法,类的构造器,运行时常量池)
- 方法区逻辑上是堆的一个组成部分,但是在不同版本的虚拟机里实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)
- (注意:方法区只是概念上的东西,是一种规范,而永久代和元空间是它的一种实现方式)
1.6 中,用永久代作为方法区的实现,存储类的信息,类加载器,运行时常量池,里面的StringTable俗称串池
1.8中,用元空间作为方法区的实现,同样存储着class,ClassLoader,常量池
区别:1.8不占用堆内存,换句话不是由JVM管理内存结构,移出到本地内存(操作系统内存,还会跑其它进程),串池StringTable不在放在元空间,方法区实现的内存部分,被移到了堆Heap中
6.2 方法区内存溢出
疑问:方法区中存储着类的一些数据,类能有多少呢,怎么导致方法区内存溢出呢
- 1.8 以前会导致永久代内存溢出
- 1.8以后会导致元空间内存异常
元空间内存溢出代码
package com.jt;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出
* ClassLoader的子类,意味着本类是个类加载器,可以加载类的二进制字节码
* 说明:1.8后方法区的实现是元空间的实现,默认使用的是系统内存,,没有设置上线,系统内存跟物理内存有关,很难演示出内存溢出
* 所以加个虚拟机参数-XX:MaxMetaspaceSize=8m ,才容易演示出内存溢出
*/
public class MethodOutOfMerroryTest extends ClassLoader{
public void main(String[] args) {
int j = 0;
try {
MethodOutOfMerroryTest test = new MethodOutOfMerroryTest();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 类的父类, 类实现接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
动态加载类,动态生成类的场景,spring框架,mybatis,用到cglib技术产生大量class
6.3 常量池
- 常量池就是一张表Constant pool,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池:常量池是存在*.class文件中的,当类被加载到虚拟机后,它的常量池信息会放到内存中,称为运行时常量池,并且把里面的符号地址变成真实地址
下面看一段代码来分析class文件包含内容
/**
* 编译成二进制字节码(类的基本信息,常量池,类方法定义包含了虚拟机指令)
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
jdk提供的工具将class做一次反编译,我们来一起分析反编译后的结果 javap -v HelloWorld.class
常量池后面的代码继续分析
完整常量池信息
Constant pool:
#1 = Methodref #6.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #24 // hello world
#4 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #27 // HelloWorld
#6 = Class #28 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LHelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 MethodParameters
#19 = Utf8 SourceFile
#20 = Utf8 HelloWorld.java
#21 = NameAndType #7:#8 // "<init>":()V
#22 = Class #29 // java/lang/System
#23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#24 = Utf8 hello world
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#27 = Utf8 HelloWorld
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
下面我们来分析以下main方法变成下面4条指令,执行过程
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
注:java解释器翻译虚拟机指令时是没有后面注释的,解释器执行时会把#2 #3 #4进行查表翻译
当执行第一条指令时,
#2 代表去查常量池中的表 ,引用了成员变量,引用了哪个成员变量呢?又去找#22,#23中去找,
#22是个类名 ,然后又去从#29找,即我们找的静态成员变量所在的类是 java/lang/System
#2还有个#23,#23又引用了#30 即System中的out变量 和#31说的是类型,类型是java.io包下PrintStream;
总结:#2是想找java/lang/System类下的叫out的成员变量,它的类型是java/io/PrintStream,即getstatic指的是要找某个类的静态成员变量
接下来的
ldc #3 代表加载一个参数,hello world
invokevirtual #4 执行虚拟机方法调用,调用System.out的println方法
return 表示方法结束
依次类推,这里不详细叙述过程啦