初探JVM

什么是 JVM?

JVM (Java Virtual Machine) - Java 二进制字节码的运行环境

JVM 有什么好处?

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收
  • 数组下标越界检查
  • 多态(虚方法表)

jvm jre jdk javase javaee 有什么关系
jvm jre jdk 关系图
学习 jvm 有什么用?

  • 面试
  • 理解底层的实现原理
  • 中高级程序员的必备技能

学习路线

jvm

内存结构

程序计数器:记住下一条 jvm 指令的执行地址

特点:

  • 是线程私有的
  • 不会存在内存溢出的问题

: 线程运行的内存空间

栈帧:每个方法运行需要的内存空间

  • 每个线程只能有一个活动栈帧,对应当前在执行的方法

面试问题:

1、 垃圾回收是否涉及栈内存?
	不会,因为方法调用结束后栈帧内存会被自动释放,线程结束后,栈内存也会被释放, 所以不会存在垃圾
2、栈内存分配越大越好吗?
	不是,栈内存越大,也就意味着最多能创建的线程数就越少,程序的执行效率可能会降低
	默认情况下 linux 的栈内存空间为 1024kb (-Xss 1m)
3、方法内的局部变量是否是线程安全的?
	不一定,要看该变量是否有机会被多个线程共享,具体问题具体分析(逃逸分析)

栈内存溢出

什么情况下会存在栈内存溢出?
	- 栈帧过多
	- 栈帧过大

栈帧过多

public class Demo {
	
	public static void main(String[] args) {
		// java.lang.StackOverflowError
		demo();
	}
	
	public static void demo() {
		demo();
	}

}

线程诊断

1、cpu 占用过多
	- top 命令查看cpu占用过高的进程 id
	- ps H -eo pid, tid, %cpu | grep 进程id (ps命令查看cpu 占用过高的进程 id 和线程 id)
	- jstack 进程id 查看进程中的 Java 线程状态和异常堆栈信息(信息中的线程id是用16进制表示的)

2、程序运行很长时间没有结果
	- jstack 进程 id 查看线程状态,查看是否出现死锁

本地方法栈

通过本地方法接口调用操作系统的底层方法的内存结构,为本地方法的调用提供内存资源

heap 堆:通过 new 关键字,创建对象都会使用堆内存

特点:

  • 它是线程共享的,堆中对象都需要考虑线程安全的问题
  • 它有垃圾回收机制

堆内存溢出

public static void main(String[] args) {
		
		// java.lang.OutOfMemoryError: Java heap space
		List<String> list = new ArrayList<>();
		String str = "hello";
		while (true) {
			list.add(str);
			str += str;
		}

	}
-Xmx size // vm参数可以设置堆内存空间的大小

堆内存诊断

1、jps 工具
	查看当前系统中有哪些 Java 进程
2、jmap 工具
	jmap -heap 线程id
	查看某一时刻堆内存的占用情况
3、jconsole 工具
	图形界面的,多功能的检测工具,可以连续监测
4、jvisualvm 工具
	查看堆快照信息

jmap 工具使用示例代码

public static void main(String[] args) throws Exception {
	System.out.println("1...");
	Thread.sleep(30000);
	byte[] bytes = new byte[1024 * 1024 * 10];
	System.out.println("2....");
	Thread.sleep(30000);
	bytes = null;
	System.gc();
	System.out.println("3...");
	Thread.sleep(1000000);
}
方法区

定义

方法区被所有 Java 虚拟机线程所共享,它存储了每一个类的结构,如运行时常量池,字段,方法数据
方法和构造器的字节码,包括使用类,实例,接口加载的特殊方法,方法区在 Java 虚拟机启动时被创建
尽管方法区在逻辑上是堆的一部分, 原则上方法区并不是必须的,他只是用来管理字节码

官方定义

The Java Virtual Machine has a method area that is shared among all Java Virtual
Machine threads. The method area is analogous to the storage area for compiled code
of a conventional language or analogous to the "text" segment in an operating 
system process. It stores per-class structures such as the run-time constant pool, 
field andmethod data, and the code for methods and constructors, including the special
methods (S2.9) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is
logically part of the heap, simple implementations may choose not to either garbage
collect or compact it. This specification does not mandate the location of the method
area or the policies used to manage compiled code. The method area may be of a
fixed size or may be expanded as required by the computation and may be contracted
if a larger method area becomes unnecessary. The memory for the method area does
not need to be contiguous.

jdk1.6 的方法区

1.6jdk
1.8的方法区

1.8jdk
1.8 的 StringTable 还是在堆中

方法区的内存溢出

1.8 之前会导致永久代的内存溢出
-XX:MaxPermSize=8m // 设置永久代的空间大小为8m
1.8 之后会导致元空间的内存溢出
-XX:MaxMetaspaceSize=8m // 设置元空间的大小为8m

字节码中的常量池

.class 字节码中包含了类基本信息,常量池,类方法信息,虚拟机指令等信息

显示Demo.java的字节码信息

javap -v Demo.class
public class Demo{
	public static void main(String[] args) {
		System.out.println("hello world");
	}
}

虚拟机指令
虚拟机指令
运行时常量池

- 常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
- 运行时常量池,常量池是 .class 文件中的,当该类被加载时,他的常量池信息就会放入运行时常量池,
- 并把里面的符号地址变为真实地址

.class 常量池
字节码中的常量池

StringTable

StringTable 是一个hashTable结构, 不能扩容, 每一个字符串对象在 StringTable 中都是唯一的

public class Demo{
	// stringTable: [ "a", "b", "ab" ]
	public static void main(String[] args) {
		String s1 = "a";
		String s2 = "b";
		String s3 = "ab";
	}
}

虚拟机指令

虚拟机指令

- ldc指令表示加载常量池中 #16 的常量 "a"
- astore_1 指令表示将该常量放入局部变量表的 slot 为 1 的位置

局部变量表
局部变量表
字符串变量拼接

String s4 = s1 + s2; // new StringBuilder();

对应的虚拟机指令
虚拟机指令

- aload_1 加载局部变量表中 solt 为 1 的局部变量
- invokestatic 静态方法调用
- invokespecial 构造器方法调用
- invokevirtual 实例方法调用

局部变量表
局部变量表
字符串的字面量拼接

// javac 在编译时会对字面量拼接的字符串进行优化,优化成 "ab"
// 而 "ab" 在字节码的常量池中已经存在,所以会将 s3 -> s5
String s5 = "a" + "b";

虚拟机指令
虚拟机指令
局部变量表
在这里插入图片描述

public class Demo{
	public static void main(String[] args) {
		String s1 = "a";
		String s2 = "b";
		String s3 = "ab";
		String s4 = s1 + s2;
		String s5 = "a" + "b";
		System.out.println(s3 == s4); // false
		System.out.println(s3 == s5); // true
	}
}

字符串中的字面量也是延迟成为对象的

StringTable 的特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复的字符串对象
- 字符串变量拼接的原理是 StringBilder()
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern() 方法,主动将串池中还没有的字符串对象放入串池
public class Demo{
	public static void main(String[] args) {
		// stringTable: ["a", "b"]
		// "ab" 不会被放入串池中,因为 "ab"串没有显式的声明
		String str = new String("a") + new String("b");

		// 尝试将 "ab" 串放入串池中,如果有不会放入
		// 如果没有则放入,并将放入的串返回
		String s = str.intern();
		System.out.println(s); // ab
	}
}
直接内存

操作系统的内存

  • 常见于 nio 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

直接内存基本使用
在这里插入图片描述

垃圾回收

如何判断对象可以回收?

  • 引用计数法
  • 可达性分析算法
  • 四种引用
1、引用计数法
	在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,
	则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,
	当该对象的引用计数为0时,那么该对象就会被回收。
缺点:
	引用计数算法有一个比较大的问题,那就是它不能处理环形数据 - 即如果有两个对象相互引用,
	那么这两个对象就不能被回收,因为它们的引用计数始终为1。这也就是我们常说的“内存泄漏”问题
	因此在Java中并没有采用这种方式
2、可达性分析算法
	在Java中采取了 可达性分析法, 通过一系列称为“GC Roots”的对象作为起始点,
	从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时
	(从 GC Roots 到这个对象不可达)时,证明此对象不可用。
	

GC Root 对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
1、强引用
	只有所有 GC Root 对象都不通过强引用引用该对象,该对象才能被垃圾回收
2、软引用
	仅有软引用引用该对象时,在垃圾回收后,内存不足会再次发出垃圾回收
	回收软引用对象,可以配合引用队列来释放软引用自身
3、弱引用
	仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
	可以配合引用队列来释放弱引用自身
4、虚引用
	必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队
	由 Refernce Handler 线程调用虚引用方法释放直接内存
5、终结器引用
	无需手动编码,但其内部配合引用队列使用,在垃圾回收是,终结器引用入队(被引用对象展示没有被回收)
	再由 Finalizer 线程通过终结器引用找到被引用对象并调用他的 finalize 方法,第二次 GC 时
	才能回收被引用对象
	

垃圾回收算法

  • 标记清除
  • 标记整理
  • 标记复制

堆区的划分模型
堆区划分模型
分代垃圾回收

对象首先分配在伊甸园区,伊甸园区空间不足时,触发 minor gc, 伊甸园和 from 存活的对象使用 cope
复制到 to 中,存活的对象年龄加一,并且交换 from to.

minor gc 会触发 stop the world, 暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值时,会晋升到老年代,最大寿命是 15 (4bit -> 1111(2) --> 15(10))

当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc, STW 的时间更长

相关 VM 参数
在这里插入图片描述
垃圾回收器的分类

1、串行
- 单线程
- 堆内存较小,适合个人电脑
2、吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,stop the world 的时间最短
3、响应时间优先
- 堆内存较大,多核 cpu
- 多线程
- 尽可能让单次 stop the world 的时间最短

G1 垃圾回收器

适用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是 200ms
- 超大堆内存,会将堆划分为多个大小相等的 region
- 整体上是标记整理算法,两个区域之间是复制算法

相关JVM参数
-XX:+UseG1GC // 开启 G1 垃圾回收器
-XX:G1HeapReginSize=size // 堆区域 Regin 空间的大小
-XX:MaxGCPauseMillis=time // 最大的垃圾回收暂停时间,默认 200 ms

G1 垃圾回收阶段
在这里插入图片描述
Young Collection

  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定
  • -XX:InitiatingHeapOccupancyPercent=percent (默认 45%)

Full GC

SerialGC
	- 新生代内存不足发生的垃圾收集- minor gc
	- 老年代内存不足发生的垃圾收集- full gc
ParallelGC
	- 新生代内存不足发生的垃圾收集- minor gc
	- 老年代内存不足发生的垃圾收集- full gc
CMS
	- 新生代内存不足发生的垃圾收集- minor gc
	- 老年代内存不足
G1
	- 新生代内存不足发生的垃圾收集- minor gc
	- 老年代内存不足

类加载

多态的原理

**invokvirtual** 指令的执行
	- 先通过栈帧中的对象引用找到对象
	- 分析对象头,找到对象的实际 Class
	- Class 结构中有虚方法表,它在类加载的链接阶段就已经根据方法的重写规则生成好了
	- 查表得到方法的具体地址
	- 执行方法的字节码

类加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 Java 类
instanceKlass 中的属性 _java_mirror 表示Java类的镜像,例如对 String 来说就是 String.class
_super
_fields
_methods
_constants
_class_loader
_vtable // 虚方法表
_itable // 接口方法表

instanceKlass 中这样的元数据是存储在方法区(1.8之后的元空间内)
但 _java_mirror 是存放在堆中

在这里插入图片描述
链接-验证

验证字节码的格式是否正确

链接-准备

为 static 变量分配空间,设置默认值
- static 变量在 jdk7 之前存储在 instanceKlass 末尾,从jdk7 开始,存储在 _java_mirror 末尾
- static 分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final的引用类型,那么赋值在初始化阶段完成

链接-解析

将常量池中的符号引用解析为直接引用

类的初始化

初始化调用 cinit() 方法,虚拟机会保证这个类的*构造方法*的线程安全

类初始化是懒惰的
类初始化发生的时机
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类没有初始化,会触发父类的初始化
- Class.forName()
- new 会导致初始化

不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName() 的第二个参数为 false 时

类加载器

Bootstrap ClassLoader
	启动类加载器,加载 JAVA_HOME/jre/lib/rt.jar 中的类,无法直接访问
	java -Xbootclasspath/a:. cn.liuweiwei.Hello // 指明用启动类加载器加载该类

Extension ClassLoader 
	扩展类加载器:加载 JAVA_HOME/jre/lib/ext/ 下的类,上级为启动类加载器,显示为 null

Application ClassLoader 
	应用类加载器:加载 classpath 下的类,上级为扩展类加载器

自定义类加载器
	加载自定义位置的类,上级为应用类加载器

自定义类加载器

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Demo {
	
	public static void main(String[] args) throws Exception {
		MyClassLoader loader = new MyClassLoader();
		Class<?> class1 = loader.loadClass("Hello");
		class1.newInstance();
		System.out.println(class1);
	}
	
}

class MyClassLoader extends ClassLoader{

	@SuppressWarnings("deprecation")
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String path = "F:\\eclipse\\" + name + ".class";
		try {
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			Files.copy(Paths.get(path), os);
			byte[] bytes = os.toByteArray();
			return defineClass(bytes, 0, bytes.length);
		} catch (IOException e) {
			e.printStackTrace();
			throw new ClassNotFoundException(e.getMessage());
		}
	}
	
}

即时编译器(JIT)与解释器的区别

- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会重复的解释
- 即时编译器是将一些字节码编译为机器码,并存入 Code Cache, 下次遇到相同的代码,直接执行无需再编译
- 解释器是将字节码解释为针对所有平台都通用的机器码
- 即时编译器会根据平台类型,生成平台特定的机器码

对于大部分不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行

另一方面,对于仅占据小部分的热点代码,我们则可以将其编译为机器码,以达到理想的

运行速度,总的目标是发现热点代码,优化之。

运行时优化

1、逃逸分析
	对象没有逃逸出栈时,对象分配在栈上而不是在堆上,极大的降低了 GC 的次数,提升了程序的执行效率
2、方法内联
	如果发现方法是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝,粘贴到
	调用者的位置,减少了方法调用的栈开销,如果方法的执行结果是一个常量,也会进行常量折叠
3、字段优化
4、反射优化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值