Program Counter Register——程序计数器,在字节码执行过程中,记录下一条jvm指令执行的地址
Java Vitual Machine Stacks——Java虚拟机栈
public class Main {
public static void main(String[] args) {
A();
B();
C();
}
private static void A(){
}
private static void B(){
A();
}
private static void C(){
A();
}
}
垃圾回收不涉及栈内存,因为栈内存对应者方法的执行,当一个方法执行结束后,会从栈中pop,不会出现多余的方法在栈中,所以不需要回收
栈内存不是越大越好,栈内存越大,对应一个线程的内存占用就越大,线程数量就会减少,栈内存划分过大会导致并发场景下的性能问题
只存在于方法内部的局部变量是线程安全的,栈内存是线程私有的,不同线程都有自己的方法栈,对局部变量的修改不会影响其他线程
/**
* 线程安全
*/
public static void a(){
StringBuilder sb = new StringBuilder();
sb.append(1).append(2).append(3);
System.out.println(sb);
}
/**
* 线程不安全,sb对象是方法外部的对象,其他线程可能对这个外部对象存在访问
* @param sb
*/
public static void b(StringBuilder sb){
sb.append(1).append(2).append(3);
System.out.println(sb);
}
/**
* 线程不安全,sb对象作为返回值返回了,其他线程可能对这个对象进行访问
* @return
*/
public static StringBuilder c(){
StringBuilder sb = new StringBuilder();
sb.append(1).append(2).append(3);
System.out.println(sb);
return sb;
}
判断局部变量是否线程安全,需要保证这个局部变量没有逃离这个方法,对于逃离方法的局部变量存在被其他线程访问的可能性
由于方法的递归调用,不断有方法入栈而没有方法出栈,所以最终会出现栈内存溢出
开发中可能出现的stackOverflowError
public class Main {
public static void main(String[] args) throws JsonProcessingException {
Emp e1 = new Emp();
e1.setName("张三");
Emp e2 = new Emp();
e2.setName("李四");
Dept dept = new Dept();
e1.setDept(dept);
e2.setDept(dept);
List<Emp> list = new ArrayList<>();
list.add(e1);
list.add(e2);
dept.setEmpList(list);
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(dept));
}
}
class Emp{
private String name;
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept{
private String name;
private List<Emp> empList;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmpList() {
return empList;
}
public void setEmpList(List<Emp> empList) {
this.empList = empList;
}
}
由于循环依赖,导致在json转换时不断递归转换,最终栈溢出
使用jstack工具,在最后可以找到Found one Java-level deadlock
,查到出现死锁位置
本地方法栈和虚拟机栈基本一致,区别在于,JVM栈用于为Java方法提供内存,而本地方法栈,是通过本地接口调用的方法(如C、C++方法)占有的内存
例如:Object.wait等方法,就有native修饰,该方法会调用本地的方法接口
通过new关键字创建的对象都会使用堆内存,它是线程共享的,需要垃圾回收
通过-Xmx8m虚拟机参数,将堆内存改为8m,然后不断通过字符串拼接创建新的字符串对象,导致堆内存溢出
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
while(1!=in.nextInt()){
}
//占用10m的堆空间
byte[] arr = new byte[1024*1024*10];
while(2!=in.nextInt()){
}
arr = null;
while(3!= in.nextInt()){
}
System.gc();
while(4!=in.nextInt()){
}
}
}
通过jps查看运行的Java进程,jps
查看最初的堆内存情况,jmap -heap 12608
控制台输入1后,创建一个占用10m内存空间的数组
控制台输入2后,将对象的引用置空
控制台输入3后,触发垃圾回收
通过jps获取进程id,然后通过jconsole 进程id
打开jconsole图形界面
输入1,占用10m堆内存
输入2,将引用置空
输入3,触发垃圾回收
jconsole除了检测堆内存,还可以查看线程、cpu等各项指标
通过jvisualvm可以进一步定位到具体占用大的对象
JVM规范定义了方法区存储和类相关的一些数据,如类本身、类加载器、常量池等
通过-XX:MaxMetaspaceSize=10m
来指定元空间大小
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/***
* @author shaofan
* @Description
*/
public class Main extends ClassLoader{
public static void main(String[] args){
int j = 0;
try{
Main main = new Main();
for (int i = 0; i < 10000; i++,j++) {
//cw用来生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号、访问控制、类名、包名、父类、接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
byte[] code = cw.toByteArray();
//加载类
main.defineClass("Class"+i,code,0,code.length);
}
}finally {
System.out.println(j);
}
}
}
在3331个类的创建后,出现了元空间的内存溢出
二进制字节码包含:类基本信息、常量池、类方法定义、虚拟机指令
通过javap -v class文件
来反编译字节码文件并显示详细信息
类基本信息
常量池
类方法和方法内部的虚拟机指令
工作流程