3)问题辨析
- 垃圾回收是否涉及栈内存?
不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
- 栈内存的分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
- 方法内的局部变量是否是线程安全的?
如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
4)内存溢出
Java.lang.stackOverflowError 栈内存溢出
发生原因
-
虚拟机栈中,栈帧过多(无限递归)
-
每个栈帧所占用过大
5)线程运行诊断
案例一、CPU占用过高
- Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换
案例二、程序运行很长时间没有结果
可能发生了死锁
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
1)定义
Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
特点
-
它是线程共享的,堆中对象都需要考虑线程安全的问题
-
有垃圾回收机制
2)堆内存溢出
java.lang.OutofMemoryError :java heap space. 堆内存溢出
3)堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程
- jmap 工具
查看堆内存占用情况 jmap - heap 进程id
- jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
jconsole 工具图示:
4)案例
- 垃圾回收后,内存占用仍然很高
我们可以使用jvirsalvm命令调用出来的工具来分析
图示
5.1)定义
Java虚拟机有一个在所有Java虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法。
方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能选择不垃圾收集或压缩它。此规范不强制指定方法区域的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。
5.2)组成
Hotspot 虚拟机 jdk1.6 1.7 1.8 内存结构图
5.3)方法区内存溢出
- 1.8 之前会导致永久代内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError: PermGen space
使用 -XX:MaxPermSize=8m 指定永久代内存大小
- 1.8 之后会导致元空间内存溢出
演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
使用 -XX:MaxMetaspaceSize=8m 指定元空间大小
5.4)运行时常量池
二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)
首先看看常量池是什么,编译如下代码:
public class Test {
public static void main(String[] args) {
System.out.println(“Hello World!”);
}
}
然后使用 javap -v Test.class 命令反编译查看结果。
![在这里插入图片描述](https://img-blog.csdnimg.cn/b28a2163af2d4a6d9d9887c8efa82e5a.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNzQ0NDg4,size_16,color_FFFF
【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
FF,t_70)
每条指令都会对应常量池表中一个地址,常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。
常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5)StringTable
5.6)StringTable特性
-
常量池中的字符串仅是符号,只有在被用到时才会转化为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是StringBuilder
-
字符串常量拼接的原理是编译器优化
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
以下代码信息包含:
-
常量池与串池的关系
-
字符串变量拼接底层实现原理
-
编译期优化
public class Demo01 {
//1、当程序运行时候常量池中的信息都会被加载到运行时常量池中,这时a b ab 都还是符号,还没有变为java字符串对象
//2、当程序运行到了String s1 = “a”;这一行时jvm就会把a 符号变为"a"字符串对象
//3、再将"a"字符串对象放到stringTable[“a”,“b”](串池)中,stringTable是hashTable结构的不能扩容,且不能重复
//4、下一次再有引用字符串a时候就直接去stringTable中找,找不到才会创建字符串对象
public static void main(String[] args) {
String s1 = “a”;
String s2 = “b”;
String s3 = “ab”;
//相当于new StringBuilder().append(“a”).append(“b”).toString()也相当于new String(“ab”)
//但是这种new是在堆内存中它和方法区中的常量池中的"ab"地址不同,也不会去常量池中寻找
String s4 = s1 + s2;
System.out.println(s3==s4); //false
//这一种的s5是在编译期间已经确定为"ab"了所以,这行代码会在常量池中寻找"ab"
String s5 = “a” + “b”;
System.out.println(s3==s5);//true
}
}
5.7) intern方法 1.8
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中
-
如果串池中没有该字符串对象,则放入成功
-
如果有该字符串对象,则放入失败
-
无论放入是否成功,都会返回串池中的字符串对象
注意:
-
jdk1.8此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
-
jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,
放入串池, 会把串池中的对象返回
例1:
public class Demo02 {
public static void main(String[] args) {
//此时串池中有了[“a”,“b”]
//堆空间中有了new String(“ab”)对象
String s = new String(“a”) + new String(“b”);
//将字符串new String(“ab”)对象尝试放入到串池中,如果串池有就不会放入,没有就会放入;返回串池中的对象
//此时串池中[“a”,“b”,“ab”]
String s1 = s.intern();
String s2 = “ab”;
System.out.println(s == s1);//true
System.out.println(s2 == s);//true
}
}
例2:
public class Demo02 {
public static void main(String[] args) {
String s2 = “ab”;
//此时串池中有了[“a”,“b”,“ab”]
//堆空间中有了new String(“ab”)对象
String s = new String(“a”) + new String(“b”);
//将字符串new String(“ab”)对象尝试放入到串池中,如果串池有就不会放入,没有就会放入;返回串池中的对象
//此时串池中[“a”,“b”,“ab”]
String s1 = s.intern();
System.out.println(s == s1);//false
System.out.println(s2 == s);//false
}
}
5.8) 面试题
/**
- 演示字符串相关面试题
*/
public class Demo1_21 {
public static void main(String[] args) {
String s1 = “a”;
String s2 = “b”;
String s3 = “a” + “b”; // ab
String s4 = s1 + s2; // new String(“ab”)
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”); // new String(“cd”)
x2.intern();
String x1 = “cd”;
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
}
}
5.9)StringTable 的位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
图示
5.10)StringTable 垃圾回收
-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息
/**
-
演示 StringTable 垃圾回收
-
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Code_05_StringTableTest {
public static void main(String[] args) {
int i = 0;
try {
for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
//将堆中对象放入到串池中,串池里的值是唯一的
String.valueOf(j).intern();
i++;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
考虑是否需要将字符串对象入池
可以通过 intern 方法减少重复入池
1)定义
Direct Memory
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受 JVM 内存回收管理
2)使用直接内存的好处
文件读写流程:
因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。
使用了 DirectBuffer 文件读取流程
直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。
3)直接内存回收原理
直接内存的回收不是通过 JVM 的垃圾回收来释放的,而是通过unsafe.freeMemory 来手动释放。
底层是创建了一个 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为虚引用的实际对象, 第二个参数是一个回调,实现了 runnable 接口,run 方法中通过 unsafe 释放内存。
att = null;
}
这里调用了一个 Cleaner 的 create 方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是 DirectByteBuffer )被回收以后,就会调用 Cleaner 的 clean 方法,来清除直接内存中占用的内存。
直接内存的回收机制总结
-
使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
-
ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory来释放内存