视频链接: 黑马程序员JVM p1-p47
定义: Java Virtual Machine - java程序的运行环境 (java二进制字节码的运行环境)
好处:
作用: 记录下一条jvm指令的执行地址
实现: jvm在物理上是通过寄存器实现程序计数器的。寄存器是cup中读取速度最快的组件,而读取指令地址是非常频繁的操作。
特点:
虚拟机栈: 一个线程有一个栈,栈是每个线程运行需要的内存空间,里面存放的是栈帧。一个栈由多个栈帧组成
栈帧 每个方法调用时需要的内存,例如:参数、局部变量、返回地址等
只能有一个活动栈帧
,对应着当前正在执行的那个方法但并不是越大越好
。线程栈内存越大,线程数反而变少
。因为物理内存大小是一定的,假如一个线程使用1M内存,物理内存假设有500MB,理论上可以有500个线程同时运行。假如线程栈内存调为2M,那么理论就有250个线程同时运行。个人理解:
每个线程都有自己的栈,其中栈内保存的是该线程中每个方法调用时需要的内存
物理内存假如固定500MB,每个线程的栈内存大小为1MB,则可以有500个线程同时运行
方法内的局部变量是否线程安全?
局部变量是引用类型
异常: java.lang.StackoverflowError
出现原因:
1. cpu占用高
Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
top
查看进程对cpu、内存的使用情况。PID: 进程号ps H -eo pid, tid, %cpu, | grep 32655
查看32655这个进程下的进程号、线程号、cpu占用情况jstack 进程id
使用java提供的命令,查看对应进程下的所有线程ps H -eo pid, tid, %cpu, | grep 32655
输出的线程号是十进制, jstack 进程id
输出的线程号是十六进制。定义:
本地方法:
标识: native
关键字
例如: Object类中的 wait()
, finalize()
, hashCode()
, notify()
等都是native方法
虚拟机栈、程序计数器、本地方法栈 都是线程私有
的。
堆是线程共享
的
定义:
new
关键字,创建的对象都会使用堆内存特点:
异常: java.lang.OutOfMemoryError :java heap space. 堆内存溢出 (OOM)
实例演示
堆内存的设置 -Xmx8m : 设置堆内存为8mb。如果堆内存设置的过大,则不容易发现堆内存溢出情况。所以可以把堆内存设置小一点,提前发现堆内存溢出情况。
jmap -heap 进程id
定义
所有jvm线程共享的一个区域
方法区是规范,永久代(jdk1.6)和元空间(jdk1.8,占用的是系统的内存)只是实现
Xx: MaxMetaspaceSize=8m
设置元空间内存为8mOutOfMemoryError: Metaspace
-XX:MaxPermSize=8m
指定永久代内存大小-XX:MaxMetaspaceSize=8m
指定元空间大小方法区内存溢出可能出现的场景
cglib
,用它来生成代理类,它是Aop的核心。cglib
,用它来生成mapper接口实现类。上图的中的常量池就是运行时常量池
,图中少写了运行时
三个字
.java文件编译后生成.class二进制字节码。二进制字节码包含:类的基本信息
、常量池
、类方法定义
(包含了虚拟机指令)
idea打开.class文件时,能够显示与.java相似文件,是因为IDEA自带了反编译插件 Java Bytecode Decompiler。
它可以反编译 .class 文件、.jar 文件。它内部实际使用了 Fernflower 来反编译
常量池
运行时常量池
案例:反编译Demo.java文件
public class Demo{
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
可以利用JDK提供的javap反编译工具,查看字节码信息
javap -v Demo.class
常量池
类的方法定义
public class Test{
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true 编译器优化
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2); // false 因为串池已经有"cd"了。
// 补充:如果调换了x1, x2位置, 以下输出什么。如果是jdk1.6呢
System.out.println(x1 == x2); // 1.8 true, 成功把"cd"放入串池,x1指向串池中的对象。 1.6 false, 因为会复制一份"cd"放到串池,x1还是指向堆中对象
}
}
StringTable
的数据结构是哈希表
,不能扩容。字符串变量拼接
s3 == s4 输出false
编译器优化
StringTable特性
String s1 = "a";
才在堆中创建s1对象。String s4 = s1 + s2;
String s5 = "a" + "b";
intern的返回永远是串池中的对象
。
public class Demo{
public static void main(String[] args) {
String s = new String("a") + new String(""b); // new StringBuilder().append("a").append("b").toString() => new String("ab")
//将s这个字符串对象尝试放入串池
// 如果串池已经存在了,则不会放入,s还是指向堆中的对象。
// 如果串池不存在,则会把s对象放入串池,此时s指向的是串池中的对象。
//intern()方法返回的结果最后一定是串池中的对象。
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true
}
}
可以适当增加HashTable桶的个数
,来减少字符串放入串池所需要的时间。HashTable是由数组+链表实现,桶对应着数组中的每一个位置。如果桶的个数过少,hash碰撞的几率会增加。-XX:StringTableSize=xxxx //最低为1009
通过intern方法减少重复入池
,保证相同字符串对象在StringTable中只存储一份传统读取文件方式:文件首先被读取到系统缓冲区,之后再分块读取到Java堆内存中。
直接内存:是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
直接内存导致内存溢出案例
public class Main {
static int _100MB = 1024 * 1024 * 100;
public static void main(String[] args) throws IOException {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100MB);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
}
}
//输出:
2
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at main.Main.main(Main.java:19)
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory
来手动释放
通过申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
allocateDirect的实现
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer类
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size); //申请内存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
att = null;
}
这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被垃圾回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
public void clean() {
if (remove(this)) {
try {
this.thunk.run(); //调用run方法
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address); //释放直接内存中占用的内存
address = 0;
Bits.unreserveMemory(size, capacity);
}
Unsafe
对象的allocateDirect()
完成直接内存的分配,回收需要主动调用 Unsafe
对象的freeMemory()
ByteBuffer
的实现类内部,使用了 Cleaner
(虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收
,那么就会由ReferenceHandler线程通过 Cleaner 的 clean 方法调用 freeMemory
来释放直接内存