一、初始JVM
1. JVM是什么
2. JVM的三大核心功能是什么?
3. 常见的JVM虚拟机有哪些?
二、字节码文件详解
1. Java虚拟机的组成
2. 字节码文件的组成
(1)基本信息
Magic魔数
主副版本号
(2)常量池
(3)字段
(4)方法
操作数栈是用来存放临时数据的内容,是一个栈式的结构,先进后出。
局部变量表是存放方法中的局部变量,包含方法的参数、方法中定义的局部变量,在编译期就已经可以确定方法有多少个局部变量。
1、iconst_0,将常量0放入操作数栈。此时栈上只有0。
2、istore_1会从操作数栈中,将栈顶的元素弹出来,此时0会被弹出,放入局部变量表的1号位置。局部变量表中的1号位置,在编译时就已经确定是局部变量i使用的位置。完成了对局部变量i的赋值操作。
3、iload_1将局部变量表1号位置的数据放入操作数栈中,此时栈中会放入0。
4、iconst_1会将常量1放入操作数栈中。
5、iadd会将操作数栈顶部的两个数据相加,现在操作数栈上有两个数0和1,相加之后结果为1放入操作数栈中,此时栈上只有一个数也就是相加的结果1。
6、istore_2从操作数栈中将1弹出,并放入局部变量表的2号位置,2号位置是j在使用。完成了对局部变量j的赋值操作。
7、return语句执行,方法结束并返回。
同理,同学们可以自行分析下i++和++i的字节码指令执行的步骤。
i++的字节码指令如下,其中iinc 1 by 1指令指的是将局部变量表1号位置增加1,其实就实现了i++的操作。
而++i只是对两个字节码指令的顺序进行了更改:
(5)属性
3. 类的生命周期
加载 -> 连接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载
(1)加载阶段
(2)连接阶段
(2-1)验证
(2-2)准备
(2-3)解析
(3)初始化
4. 类加载器
(1)类加载器的分类
(2)双亲委派机制
向上查找是否加载过
向下尝试加载
面试题 - 类的双亲委派机制是什么?
(3)打破双亲委派机制
(3-1)自定义类加载器
自定义类加载器默认的父类加载器:应用程序类加载器
package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;
/**
* 打破双亲委派机制 - 自定义类加载器
*/
public class BreakClassLoader1 extends ClassLoader {
private String basePath;
private final static String FILE_EXT = ".class";
//设置加载目录
public void setBasePath(String basePath) {
this.basePath = basePath;
}
//使用commons io 从指定目录下加载文件
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
try {
return IOUtils.toByteArray(fis);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Exception e) {
System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
return null;
}
}
//重写loadClass方法
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//如果是java包下,还是走双亲委派机制
if(name.startsWith("java.")){
return super.loadClass(name);
}
//从磁盘中指定目录下加载
byte[] data = loadClassData(name);
//调用虚拟机底层方法,方法区和堆区创建对象
return defineClass(name, data, 0, data.length);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
//第一个自定义类加载器对象
BreakClassLoader1 classLoader1 = new BreakClassLoader1();
classLoader1.setBasePath("D:\\lib\\");
Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
//第二个自定义类加载器对象
BreakClassLoader1 classLoader2 = new BreakClassLoader1();
classLoader2.setBasePath("D:\\lib\\");
Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");
System.out.println(clazz1 == clazz2);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
}
(3-2)线程上下文类加载器
总结:
(4)JDK8之后的类加载器
(5)总结
1. 类加载器的作用是什么?
2. 有几种类加载器?
3. 什么是双亲委派机制?
4. 怎么打破双亲委派机制?
三、JVM的内存区域
运行时数据区 - 总览
1. 程序计数器
2. 栈
(1)Java虚拟机栈
栈帧的组成
操作数栈
帧数据
帧数据主要包含动态链接、方法出口、异常表的引用。
动态链接:
方法出口:
异常表:
栈内存溢出:
(2)本地方法栈
3. 堆
4. 方法区
(1)类的元信息
(2)运行时常量池
(3)字符串常量池
字符串常量池存放在Java堆内存(heap)中。
静态变量存储在哪里?
5. 直接内存
总结:
1. 运行时数据区分为哪几个部分,每一部分的作用是什么?
程序计数器:每个线程会通过程序计数器记录当前要执行的字节码指令的地址,程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑。
Java虚拟机栈:采用栈的数据结构赖管理方法调用中的基本数据(局部变量、操作数、帧数据),每一个方法的调用使用一个栈帧来保存。
本地方法栈:本地方法栈存储的是native本地方法的栈帧。
堆:存放中的是创建出来的对象,这也是最容易产生内存溢出的位置。
方法区:主要存放的是类的元信息,同时还保存了常量池。
2. 不同JDK版本之间运行时数据区域的区别是什么?JDK6
(在JDK8中,类的元信息和运行时常量池存放在元空间中,使用本地内存。字符串常量池存放在Java堆内存中。)
四、JVM的垃圾回收
C/C++的内存管理
Java的内存管理
1. 方法区的回收
2. 堆回收
(1)引用计数法和可达性分析法
如何判断堆上的对象可以回收?
引用计数法
可达性分析算法
哪些对象被称之为GC Root对象呢?
查看GC Root对象
(2)五种对象引用
软引用
代码:
/**
* 软引用案例3 - 引用队列使用
*/
public class SoftReferenceDemo3 {
public static void main(String[] args) throws IOException {
ArrayList<SoftReference> softReferences = new ArrayList<>();
ReferenceQueue<byte[]> queues = new ReferenceQueue<byte[]>();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[1024 * 1024 * 100];
SoftReference studentRef = new SoftReference<byte[]>(bytes,queues);
softReferences.add(studentRef);
}
SoftReference<byte[]> ref = null;
int count = 0;
while ((ref = (SoftReference<byte[]>) queues.poll()) != null) {
count++;
}
System.out.println(count);
}
}
代码:
package chapter04.soft;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
/**
* 软引用案例4 - 学生信息的缓存
*/
public class StudentCache {
private static StudentCache cache = new StudentCache();
public static void main(String[] args) {
for (int i = 0; ; i++) {
StudentCache.getInstance().cacheStudent(new Student(i, String.valueOf(i)));
}
}
private Map<Integer, StudentRef> StudentRefs;// 用于Cache内容的存储
private ReferenceQueue<Student> q;// 垃圾Reference的队列
// 继承SoftReference,使得每一个实例都具有可识别的标识。
// 并且该标识与其在HashMap内的key相同。
private class StudentRef extends SoftReference<Student> {
private Integer _key = null;
public StudentRef(Student em, ReferenceQueue<Student> q) {
super(em, q);
_key = em.getId();
}
}
// 构建一个缓存器实例
private StudentCache() {
StudentRefs = new HashMap<Integer, StudentRef>();
q = new ReferenceQueue<Student>();
}
// 取得缓存器实例
public static StudentCache getInstance() {
return cache;
}
// 以软引用的方式对一个Student对象的实例进行引用并保存该引用
private void cacheStudent(Student em) {
cleanCache();// 清除垃圾引用
StudentRef ref = new StudentRef(em, q);
StudentRefs.put(em.getId(), ref);
System.out.println(StudentRefs.size());
}
// 依据所指定的ID号,重新获取相应Student对象的实例
public Student getStudent(Integer id) {
Student em = null;
// 缓存中是否有该Student实例的软引用,如果有,从软引用中取得。
if (StudentRefs.containsKey(id)) {
StudentRef ref = StudentRefs.get(id);
em = ref.get();
}
// 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,
// 并保存对这个新建实例的软引用
if (em == null) {
em = new Student(id, String.valueOf(id));
System.out.println("Retrieve From StudentInfoCenter. ID=" + id);
this.cacheStudent(em);
}
return em;
}
// 清除那些所软引用的Student对象已经被回收的StudentRef对象
private void cleanCache() {
StudentRef ref = null;
while ((ref = (StudentRef) q.poll()) != null) {
StudentRefs.remove(ref._key);
}
}
// // 清除Cache内的全部内容
// public void clearCache() {
// cleanCache();
// StudentRefs.clear();
// //System.gc();
// //System.runFinalization();
// }
}
class Student {
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
弱引用
package chapter04.weak;
import java.io.IOException;
import java.lang.ref.WeakReference;
/**
* 弱引用案例 - 基本使用
*/
public class WeakReferenceDemo2 {
public static void main(String[] args) throws IOException {
byte[] bytes = new byte[1024 * 1024 * 100];
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes);
bytes = null;
System.out.println(weakReference.get());
System.gc();
System.out.println(weakReference.get());
}
}
虚引用和终结器引用
终结器引用案例
package chapter04.finalreference;
/**
* 终结器引用案例
*/
public class FinalizeReferenceDemo {
public static FinalizeReferenceDemo reference = null;
public void alive() {
System.out.println("当前对象还存活");
}
@Override
protected void finalize() throws Throwable {
try{
System.out.println("finalize()执行了...");
//设置强引用自救
reference = this;
}finally {
super.finalize();
}
}
public static void main(String[] args) throws Throwable {
reference = new FinalizeReferenceDemo();
test();
test();
}
private static void test() throws InterruptedException {
reference = null;
//回收对象
System.gc();
//执行finalize方法的优先级比较低,休眠500ms等待一下
Thread.sleep(500);
if (reference != null) {
reference.alive();
} else {
System.out.println("对象已被回收");
}
}
}
(3)垃圾回收算法
垃圾回收算法的评价标准
标记—清除算法
复制算法
标记—整理算法
分代垃圾回收算法
为什么分代GC算法要把堆分成年轻代和老年代?
(4)垃圾回收器
①年轻代 - Serial垃圾回收器 -> 复制算法
老年代 - SerialOld垃圾回收器 -> 标记整理算法
②年轻代 - ParNew垃圾回收器 -> 复制算法
老年代 - CMS(Concurrent Mark Sweep)垃圾回收器 -> 标记清除算法
③年轻代 - Parallel Scavenge垃圾回收器 -> 复制算法
老年代 - Paralell Old垃圾回收器 -> 标记整理算法
④G1垃圾回收器
总结:
1. Java中有哪几块内存需要进行垃圾回收?
线程不共享:跟随线程的生命周期随着线程回收而回收;
方法区:一般不需要回收,JSP等技术会通过回收类加载器去回收方法区中的类
堆:由垃圾回收器进行回收
2. 有哪几种常见的引用类型?
3. 有哪几种常见的垃圾回收算法?
4. 常见的垃圾回收器有哪些?