目录
应该如何看招聘信息,直通年薪50万+?
- 参与现有系统的性能优化,重构,保证平台性能和稳定性
- 根据业务场景和需求,决定技术方向,做技术选型
- 能够独立架构和设计海量数据下高并发分布式解决方案,满足功能和非功能需求
- 解决各类潜在系统风险,核心功能的架构与代码编写
- 分析系统瓶颈,解决各种疑难杂症,性能调优等
我们为什么要学习JVM?
- 面试的需要(BATJ、TMD、PKQ等面试都爱问)
- 中高级程序员必备技能,项目管理、性能调优的需要
- 追求极客精神,垃圾回收算法、JIT、底层原理
JVM代码执行流程
JVM的生命周期
虚拟机的启动
- Java虚拟机的启动是通过引导类加载器(bootstarp class loder)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行
- 一个运行中的Java虚拟机有着一个清晰的任务:执行java程序。
- 程序开始执行时他才运行,程序结束时他就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫Java虚拟机的进程。
虚拟机的退出
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Jaa安全管理器也允许这次exit或halt操作
- 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况
为什么要自定义类的加载器?
- 隔离类加载:中间件会有自己的引用jar包,我们程序框架也会有jar包,会出现某个类引用一样 路径一样 引起jar包冲突,需要做类的仲裁,自定义加载类避免jar包冲突
- 修改类的加载方式:bootstarp是必须的,其他的加载器非必须,可以根据情况动态加载需要的加载器
- 扩展加载源:本地的物理磁盘,网络中的jar包,可以扩展加载渠道
- 防止源码泄露:class源码很容易被反编译篡改,对class进行加解密
用户自定义类加载器实现步骤:
- 集成抽象类java.lang.ClasLoader类,重写findClass()方法,把业务逻辑写在里面。
- 如果没有太过复杂需求,可以直接继承URLClasLoader类,这样可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
双亲委派机制:
Java虚拟机对class文件采用的是按需加载的方式,也就是说档需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
工作原理:
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
- 如果福类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载,这就是双亲委派模式
系统类加载器 AppClassLoader:
- 负责加载环境变量classpath或系统属性,java.class.path指定路劲下的类库
- 该类加载是程序中默认的类加载器,一般来说Java应用的类是由它来完成加载
- 通过ClassLoader.getSystemClassLoader()方法获取到该类加载器
扩展类加载器Extension ClassLoader:
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为引导类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载
引导类加载器Bootstrap ClassLoader:
- 这个类加载使用C/C++语言实现的,镶嵌在JVM内部
- 他用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容)用于提供JVM自身需要的类
- 不继承类,没有父类加载器,属于顶层加载器
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器之加载包名为java、javax、sun等开头的类
// 获取加载器
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取applitation ClassLoader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取Extension ClassLoader
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
// 获取Bootstrap ClassLoader
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
}
}
// 加载自定义java.lang.String类
package java.lang;
public class String {
static {
System.out.println("自定义String加载");
}
public static void main(String[] args) {
java.lang.String string = new java.lang.String();
System.out.println(string);
/**
* AppClassLoader找父类 Extension ClassLoader 继续找 Bootstrap ClassLoader 加载,
* 由于Bootstrap ClassLoader 只加载java包下的类,所以运行java包下的main找不到此方法
*
* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args)
* 否则 JavaFX 应用程序类必须扩展javafx.application.Application
*/
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
java.lang.String string = new java.lang.String();
System.out.println(string); // 打印出 java自带类库里的string
}
}
双亲委派优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
沙箱安全机制:
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中java/lang/String.class),报错信息说没有main方法就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
其他补充:
在JVM中表示两个class对象是否为同一个类存在的两个必要条件
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
换句话说,在JVM中即使这两个类对象(Class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
对类加载器的引用:
JVM必须知道一个类型是由启动加载器的还是由用户类加载器加载的,如果一个类型是由用户类加载器加载,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的时候,JVM需要保证这两个类型的类加载器是相同的。
JDK1.7内存模型
程序计数器:线程私有,可以看做当前程序执行的行号指令器。
Java虚拟机栈:线程私有,生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行时会形成一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,一个方法从调用到执行完毕,就是一个栈帧从进栈到出栈的过程
本地方法栈:线程私有,作用于Java虚拟机栈类似,只不过Java虚拟机栈执行Java方法,而本地方法栈运行本地的Native方法。
堆:Java虚拟机管理的最大的一块内存区域,Java堆是线程共享的,用于存放对象实例。也就是说对象的出生和回收都是在这个区域进行的。
方法区:线程共享,用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,这里要说一下方法区和永久代到底是什么关系,永久代是HotSpot虚拟机对于方法区的实现,方法区的实现是不受虚拟机规范约束的,这里只是HotSpot虚拟机团队是这样实现的。
运行时常量池:在JDK1.7中,是运行时常量池是方法区的一部分,用于存放编译期生成的各种字符变量和符号引用。其实除了运行时常量池,还有字符串常量池,class常量池
JDK1.8内存模型
JDK1.8与1.7最大的区别是1.8将永久代取消,取而代之的是元空间,既然方法区是由永久代实现的,取消了永久代,那么方法区由谁来实现呢,在1.8中方法区是由元空间来实现,所以原来属于方法区的运行时常量池就属于元空间了。元空间属于本地内存,所以元空间的大小仅受本地内存限制,但是可以通过-XX:MaxMetaspaceSize进行增长上限的最大值设置,默认值为4G,元空间的初始空间大小可以通过-XX:MetaspaceSize进行设置,默认值为20.8M,还有一些其他参数可以进行设置,元空间大小会自动进行调整。
- 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
- 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
- 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
1、PC寄存器/程序计数器(Program Counter Register)
PC寄存器用来存储指令向下一条指令的地址,也即将要执行的指令代码。将指令地址存到PC寄存器中,由执行引擎读取指令内容,将指令内容翻译成机器指令,由CPU执行。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有,生命周期与线程的生命周期保持一致。
它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
常见问题:
- 使用PC寄存器存储字节码指令地址有什么用?
- 为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从那开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
- PC寄存器为什么会被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内会执行其中某一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,如何保证分号误差呢?为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现互相干扰的情况。
由于CPU时间片论限制,众多线程在并发过程中任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分号误差呢?在每个线程创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
2、Java虚拟机栈
虚拟机栈出现的背景:
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多指令。
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
生命周期
生命周期和线程一致,随着线程的创建而创建,销毁而销毁。
作用
主管Java程序的运行,它保存的局部变量(8种基本数据类型、对象的引用地址)、部分效果,并参与方法的调用和返回。
栈是运行时单位,而堆是存储的单位。
栈的优点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
JVM对Java栈的操作只有两个,方法被执行=>入栈,方法结束=>出栈
对于栈来说不存在垃圾回收问题
栈种可能出现的异常:
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError(栈溢出)异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存空间,或者在创建新的线城时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
栈溢出 java.lang.StackOverflowError
static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
设置栈内存大小:
可以使用参数-Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
默认大小:1024k
栈种存储什么:
- 每个线程都有自己的栈,栈种的数据都是以栈帧(Stack Frame)的格式存在。
- 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理:
JVM直接对Java的栈操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出、“后进先出”原则。
寄存器 | 栈 | ||||
GC | 否 | 否 | |||
OOM | 是 | 是 | |||
异常 | 否 | 是 | |||
速度 | 1 | 2 |
栈 https://my.oschina.net/wangsifangyuan/blog/711329?_sm_byp=iVVTVRV1gvgqVfWQ
CPU时间片
访问速度
程序计数器 > 栈 > 堆
配置 idea javap
一、内存与垃圾回收
二、字节码与类的加载器
三、性能监控与调优
四、大厂面试题
常见的垃圾回收算法:
引用计数
复制
标记清除
标记整理