学习于B站视频:https://www.bilibili.com/video/BV1PJ411n7xZ
语言热度排行榜:https://www.tiobe.com/tiobe-index/
JVM 8官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html,若使用其他版本,步骤:
Java8 命令行可用参数:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html,其他版本:
一台虚拟的计算机,两类:
作用:二进制字节码的运行环境
jvm:装在操作系统上,与硬件没有直接交互
基于栈式架构的特点:
基于寄存器架构的特点:
java的指令是根据栈来设计的,跨平台、指令集小、指令多;执行性能比寄存器差
启动:引导类加载器创建初始类完成,这个类是由虚拟机具体实现决定的
执行:执行一个叫java虚拟机的进程
退出:
加载
链接:
验证:
准备:
解析
初始化
类变量有两次初始化机会,一次是在链接的准备阶段,赋0值,另一次是在初始化阶段,赋予我们定义的值
类加载器分类(按照java虚拟机规范)
虚拟机自带的加载器:
1、引导类加载器:Bootstrap ClassLoader
//通过Launcher.getBootstrapClassPath().getURLs()获取:
file:/C:/Java/jdk1.8.0_271/jre/lib/resources.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/rt.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/sunrsasign.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/jsse.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/jce.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/charsets.jar
file:/C:/Java/jdk1.8.0_271/jre/lib/jfr.jar
file:/C:/Java/jdk1.8.0_271/jre/classes
2、扩展类加载器
//通过System.getProperty("java.ext.dirs")获取:
C:\Java\jdk1.8.0_271\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
3、应用程序类加载器(系统类加载器,appClassLoader)
用户自定义类加载器
为什么要自定义类加载器?
如何自定义?
继承ClassLoader
jdk1.2之前,要重写loadClass方法,jdk1.2之后不建议覆盖loadClass方法,而是建议把自定义类加载逻辑写在findClass方法中
获ClassLoader方法:
ClassLoader classLoader1 = Class.forName("java.lang.String").getClassLoader();
ClassLoader classLoader2 = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader3 = ClassLoader.getSystemClassLoader();
System.out.println("classLoader1 = " + classLoader1);
System.out.println("classLoader2 = " + classLoader2);
System.out.println("classLoader3 = " + classLoader3);
//classLoader1 = null
// classLoader2 = sun.misc.Launcher$AppClassLoader@18b4aac2
// classLoader3 = sun.misc.Launcher$AppClassLoader@18b4aac2
java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将该类的class文件加载到内存生成class对象,而且加载某个类的class文件时,java虚拟机采用的时双亲委派模式,即把请求交由父类处理,他是一种任务委派模式
原理:(向上委托到最顶层之后,依次尝试加载:引导类加载器 -> 扩展类加载器 -> 系统类加载器)
例子:


优点:
如图:自定义java.lang.String类,自其中写main方法尝试运行

运行结果:

说明使用的不是我们自定义的String
结论:
1、自定义String类,在加载的时候使用引导类加载器进行加载
↓
2、引导类加载器在加载的时候会加载jdk自带的文件中的String类
↓
3、自带jdk中的String没有main方法,导致报错
这样可以保证对java核心源代码的保护,这就是沙箱安全机制
在jvm中判断两个class对象是否为同一个类存在的两个必要条件:
jvm必须知道一个类型是由启动类加载器还是用户类加载器加载,如果是用户类加载器,会将其一个应用保存在jvm的方法区中;当解析一个类型到另一个类型时,jvm必须保证类加载器是相同的
主动使用:
除了以上七种情况,其他使用java类的方式都时对类的被动使用,不会导致类的初始化
方法信息:

字节码:




内存是硬盘和cpu的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。jvm内存布局规定了java在运行过程种内存申请、分配、管理的策略,保证jvm高效稳定运行。不同jvm对于内存的划分方式和管理机制存在着部分差异

阿里的图:

有些和虚拟机生命周期一致,有些和线程生命周期一致
一个jvm对应一个Runtime
线程是一个程序里的运行单元,jvm允许一个应用由多个线程并行的执行
在Hotspot JVM里,每个线程与本地线程直接映射
操作系统负责所有线程安排调度到一个可用的cpu上,一旦本地线程初始化完成,他会调用java线程中的run()方法
用于存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取吓一条指令。

cpu时间片:
串行:一个线程执行
并行:多个线程同时执行
并发:多个线程交替执行
1、使用pc寄存器存储字节码指令地址由什么用?
答:因为cpu需要不停的切换各个线程,切换回来的时候需要知道从哪开始执行
2、为什么使用pc寄存器记录当前线程的执行地址?
答:JVM字节码解释器通过改变pc寄存器的值来明确下一天执行什么样的字节码指令
出现背景:
优点:
缺点:
内存中的栈与堆:
java虚拟机栈:
1、是什么:
2、生命周期:
和线程一致
3、作用
主管java程序运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
栈的特点:
栈中可能出现的异常:
java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
设置栈内存的大小:
可以使用-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
栈中存什么?
栈运行原理
栈帧的内部结构:每个栈帧中存储着:
成员变量与局部变量
成员变量分为类变量(静态变量)和实例变量,有两次初始化机会:准备阶段(赋零值)、初始化阶段(赋我们定义的值)
局部变量,不存在系统初始化过程,意味着使用前必须人为初始化,否则无法使用
局部变量表:
在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
每一个独立的栈帧中除了包含局部变量表意外,还包含一个后进先出的操作数栈,也可以称之为表达式栈
栈有两种实现方式:基于数组、基于链表,操作数栈基于数组实现
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的占深度用于存储数值,其所需的最大的深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
栈中的任何一个元素都可以是任意的java数据类型
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
如果调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
另外,我们说java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
栈顶缓存技术

常量池的使用就是为了提供一些符合和常量便于指令识别
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将放回值压入调用者栈帧的操作数栈、设置pc寄存器的值等,让调用者方法继续执行下去。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何返回值
异常表示例:



栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息
也可以没有附加信息
参数值的存放总是在局部变量数组的index0开始,到长度-1的索引结束
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表存储编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
在局部变量表中,32位以内的只占用一个slot(包括返回值类型),64位的类型(long和double)占用两个slot
jvm会位每个slot分配一个访问索引,通过索引可以访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法内定义的变量会按照顺序被复制到局部变量的每一个slot上
如果需要访问局部变量表中一个64bit的局部变量值时,只需使用前一个索引即可(64位的占用两个slot,访问起始索引即可)
如果当前帧时由构造方法或者实例方法创建的(非静态的),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数顺粗继续排列,例如:

存在一个局部变量:

局部变量表中的槽位是可重复利用。如果一个局部变量过了作用域,新的变量可以服用该局部变量的槽位,达到节省资源的目的,例如:

发现上图中变量d存储在变量l存储的位置:

将符号引用转化为调用方法的直接引用,两种方式:
对应的绑定机制为早期绑定和晚期绑定:
虚方法与非虚方法
虚拟机中提供了以下几条方法调用指令:
其中,invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法(因为可能被子类重写,不知道调用的是自己的还是子类的方法)
动态类型语言和静态类型语言:
方法重写的本质:
虚方法表:(每个虚方法调用哪个类的)

注意:以下答案可能不完整
举例栈溢出的情况:
解决:
- 使用参数 -Xss 去调整JVM栈的大小
- 动态分配
调整栈大小,就能保证不出现溢出吗?
分配的栈内存越大越好吗?
垃圾回收是否会涉及到虚拟机栈?
方法中定义的局部变量是否线程安全?
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的),如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个 stackoverflowError异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError 异常。
本地方法是使用c语言实现的。
它的具体做法是Native Method stack中登记natilve方法,在Execution Engine执行时加载本地方法库。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
并不是所有的JVM都支持本地方法。
在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如c。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在c++中,你可以用extern "c"告知C++编译器去调用一个c的函数。
"A native method is a Java method whose implementation isprovided by non-java code . "
在定义一个native method时,并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
标识符native可以与所有其它的java标识符(public、private等等)连用,但是abstract除外。
为什么要使用Native Method ?
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
与Java环境外交互:
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
与操作系统交互:
通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。
Sun 's Java
sun的解释器是用c实现的,这使得它能像一些普通的c一样与外部交互。
现状:目前该方法使用的越来越少了,除非是与硬件有关的应用
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
-Xms10m -Xmx10m表示初始堆空间10M,最大堆空间10M《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer,TLAB)。
对象实例、数组在堆分配

堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆是GC(garbage collection,垃圾收集器)执行垃圾回收的重点区域


1.设置堆空间大小的参数
-Xms:用来设置堆空间(年轻代+老年代)的初始内存大小
-Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
2.默认堆空间的大小
物理电脑内存大小 / 64物理电脑内存大小 / 43.手动设置:-Xms60m -Xmx60m,开发中建议将初始堆内存和最大的堆内存设置成相同的值,防止频繁扩容。
4、查看设置的参数:
方式一:
jpsjstat -gc 进程id
方式二:添加运行时参数:-XX:+PrintGCDetails

运行结果:

java.lang.OutOfMemoryError,堆内存被占满,想办法一直往堆里面添加东西即可。我们知道对象、数组放在堆中,因此,可用这么干:
public static void main(String[] args) {
List<Date> list = new ArrayList<>();
while (true){
list.add(new Date());
}
}
最终:就oom了


参数:
-XX:NewRatio=2是指:老年代 / 新生代 = 2,默认2
-XX:SurvivorRatio=4:表示eden / survivor = 4,默认8
-XX:+UseAdaptiveSizePolicy,开启自适应的内存分配策略(默认开启),如果是-XX:-UseAdaptiveSizePolicy表示关闭-Xmn256m,设置新生代的大小未256M,一般不指定
一般过程:
-XX:MaxTenuringThreshold=进行设置。幸存者区满了,不会触发Minor GC;幸存者区只是伴随着Eden区进行Minor GC




Jprofiler:

年轻代

老年代

Full GC:





对象分配过程:

官网:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html


关于分配担保:
JDK7前后HandlerPromotionFailure参数的变化
栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法(不会在方法之外使用)的话,那么就可能被优化成栈上分配。-XX:+DoEscapeAnalysis开启逃逸分析,jdk8之后默认开启结论:
开发中能使用局部变量的,就不要使用在方法外定义。
代码优化:





-XX:MetaspaceSize=21m-XX:MaxMetaspaceSize=256mjps查看进程idjinfo -flag MaxMetaspaceSize 进程id
JDK8以后:
JDK7之前:
如何解决OOM?

类型信息、常量、静态变量、即时编译器编译后的代码缓存等。







为什么用常量池:







jdk7中将stringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。
这就导致stringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存
JDK7及之后:

JDK 7及其以后版本的HotSpot虚拟机选择把静态变量 与 类型在Java语言一端的映射class对象存放在一起,存储于Java堆之中,例如上图中的staticObj








两种执行方式:解释器、即时(Just In Time,JIT)编译器,即时编译器又分C1和C2
优缺点









解释器与即时编译器的切换:

优缺点








汇编语言

高级语言




例如jdk8中查看默认值:

例如再jdk8中设置1000,有如下错误:



字符串常量池不会存放相容内容的字符串:例如,如下代码,通过debug调试,看内存中String的个数变化:
@Test
public void testSameString(){
String s1 = "abc";//6201个String
String s2 = "abcdef";//6202个String
String s3 = "abc";//6202个String,因为已经有abc了
}


5. 字符串拼接不一定使用StringBuilder,当两边都是final变量时,也会再编译期优化
例子:
@Test
public void testStringAdd() {
String s1 = "abc";
final String finalS1 = "abc";
String s2 = "def";
final String finalS2 = "def";
String s3 = "abcdef";
String add = "abc" + "def";//常量拼接
String finalAdd = finalS1 + finalS2;//常量拼接
String s1AddS2 = s1 + s2;//含变量的拼接
System.out.println(s3 == add);//true
System.out.println(s3 == finalAdd);//true
System.out.println(s3 == s1AddS2);//false
}

问题一:

答案:
解析:
重点:字符串常量池没有“11”;那么下一步s3.intern方法就会在常量池创建“11”;在jdk6中,直接在永久代常量池放一个"11";而jdk7/8是在堆的常量池创建一个 指向刚刚new String(“11”)地址 的量问题二:

答案及解析:

扩展:

问题三:

答案及解析:

对于程序中大量存在存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间。


什么是垃圾:


好处

坏处



循环应用:A的同学有B,B的同学有A,循环引用










例子:
public class Kill {
public static Kill kill;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("刀下留人...");
kill = this;//自救
}
public static void main(String[] args) {
kill = new Kill();
System.out.println("斩!");
// ----------- 第一次自救,成功 ---------------
//垃圾回收器回收
kill = null;
System.out.println("准备GC...");
System.gc();
// finalizer线程优先级低,等待几秒
try {
Thread.sleep(2_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (kill != null) {
System.out.println("没有被杀死!");
}else {
System.out.println("已经被斩杀!");
}
// -------------- 第二次自救,失败,finalize方法只会执行一次 ----------
// 流程同上
kill = null;
System.out.println("准备第二次GC...");
System.gc();
if (kill != null) {
System.out.println("没有被杀死!");
}else {
System.out.println("已经被斩杀!");
}
}
}
结果:


标记的可达对象,想象一下,如果要标记不可达对象,为何不直接清除?!


将存活对象复制到另一个内存块中



标记压缩算法后,可以使用指针碰撞分配新的地址;而标记清除算法之后,要用空闲列表来分配新地址















并发:

并行:





(了解)
安全点:


安全区域:




- 强引用可以直接访问目标对象。
- 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出
OOM异常,也不会回收强引用所指向对象。- 强引用可能导致内存泄漏。

public static void main(String[] args) throws InterruptedException {
SoftReference<Object> objectSoftReference = new SoftReference<>(new Object());
// 第一次打印
System.out.println(objectSoftReference.get());
// 进行一次gc后打印
System.gc();
Thread.sleep(1000 * 2);
System.out.println(objectSoftReference.get());
//制造OOM,设置参数:-Xms1m -Xmx1m,再打印
try {
// 需要1M空间,于是会OOM
byte[] bytes = new byte[1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(objectSoftReference.get());//null
}
}
结果是:前两次都能获取到对象,最后一次获取到为null

public static void main(String[] args) throws InterruptedException {
WeakReference<Object> objectSoftReference = new WeakReference<>(new Object());
// 第一次打印
System.out.println(objectSoftReference.get());
// 进行一次gc后打印
System.gc();
Thread.sleep(1000 * 2);
System.out.println(objectSoftReference.get());//null
}
结果:第一次能获取到对象,第二次为null



按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。





吞吐量、暂停时间、内存占用

吞吐量、暂停时间

吞吐量:

暂停时间

对比

总结:在最大吞吐量优先的情况下,降低暂停时间



即:

























小结


为什么叫G1?


















如何选择?


使用:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

-XX:+PrintGCDetails 输出参数及解释:

例如有如下输出结果:
[GC (System.gc()) [PSYoungGen: 2663K->840K(38400K)] 2663K->848K(125952K), 0.0035334 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 840K->0K(38400K)] [ParOldGen: 8K->706K(87552K)] 848K->706K(125952K), [Metaspace: 3328K->3328K(1056768K)], 0.0055251 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 998K [0x00000000d5b80000, 0x00000000d8600000, 0x0000000100000000)
eden space 33280K, 3% used [0x00000000d5b80000,0x00000000d5c79b70,0x00000000d7c00000)
from space 5120K, 0% used [0x00000000d7c00000,0x00000000d7c00000,0x00000000d8100000)
to space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
ParOldGen total 87552K, used 706K [0x0000000081200000, 0x0000000086780000, 0x00000000d5b80000)
object space 87552K, 0% used [0x0000000081200000,0x00000000812b0b90,0x0000000086780000)
Metaspace used 3341K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
参数解释:
GC、Full GC 等 : GC的类型System.gc()、Allocation Failure (内存分配失败) 等 : GC原因PSYoungGen : 使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化ParoldGen : 使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化Metaspace : 元数据区cc前后大小的变化,JDK1.8中引入了元数据区以替代永久代xxx secs : 指GC花费的时间Times:
user : 指的是垃圾收集器花费的所有CPU时间sys : 花费在等待系统调用或系统事件的时间real : GC从开始到结束的时间,包括其他进程占用时间片的实际时间通过添加参数-Xloggc:路径来指定导出GC日志,如果有多级目录需要事先创建目录。
例如:-Xloggc:./gc.log,表示导出GC日志到当前项目下的gc.log

GCEasy是在线分析的:
