JVM内存结构图:

1.程序计数器:
程序计数器是JVM运行时数据区中唯一一个不会发生OOM的区域,它是线程私有的,容量极小的"地址内存指示器",核心作用的记录下一条JVM运行时的执行地址。

特征:
1.每个线程都有一个独立的程序计数器,互不干扰。确保线程切换后可以恢复到正确执行的位置。
2.程序计数器不会抛出内存溢出的问题。
2.虚拟机栈:

虚拟机栈:JVM运行时数据区的重要组成部分,线程私有的,用于储存java方法执行时的栈帧 ,遵循先进后出的原则。决定了方法的调用链和局部变量的生命周期 。(线程运行时所需要的内存空间)
栈帧:每个方法调用时所需要的内存(局部变量,操作数,方法调用链路等)。
每个线程只有一个活动栈帧,就是正在运行时的栈帧。
问题:方法内的局部变量是否会有线程安全?
分析:因为方法在运行时,是储存到虚拟机栈中,而虚拟机栈在运行时是线程私有的,所以正常来说是不会有线程安全的问题。但是如果当局部变量引入了对象,并且这个对象还在其他的方法中调用了,就会产生线程安全问题。
总结:
1.如果该局部变量没有逃离方法的作用范围,他就是线程安全的。
2.如果该局部变量引用了对象,并且逃离了方法使用,就要考虑线程安全问题了。
问题2:栈内存溢出:
出现这种可能的情况:
1.栈帧过多:比如使用了递归,并且判断语句写错了,就会导致方法套方法,直到栈内存溢出。
2.栈帧过大,也会导致(概率很小)
虚拟机栈的核心参数:
-Xss:可以设置每个线程的虚拟机栈的大小。
3.本地方法栈:
本地方法栈:JVM运行时数据区的一部分,线程私有,专门用于本地方法(Native Method)的执行----非java语言编写的,如C和C++编写的方法。类似虚拟机栈,服务的不是java方法而是本地方法方法。
4.堆:

JDK1.8之前堆中还存在一个区域叫永久代,现在被元空间所代替,元空间不占用堆的内存区域,永久代中储存的类信息,静态变量,常量等信息,在堆中内存不可控,容易随着项目的扩大,内存消耗更多,最后导致内存溢出
堆(heap):是JVM内存中最大的区域,线程共享的,储存对象和数组,以及成员变量等,堆中的的对象都需要考虑线程安全问题。
堆内存诊断:
1.jps命令:查看有哪些java进程。
jmap -heap 进程号:查看堆内存的占用。
jconsole命令:可以连续监控到堆内存的占用。
堆核心参数设置:
-Xms:最大堆内存。
-Xmn:新生代的大小。
5.方法区:
方法区:JVM规范定义的逻辑内存区域,用于储存类的"模版信息",支持类加载和字节码执行。它的物理实现不同JDK版本实现不同。线程共享的。
储存内容:
1.类元信息:类名,父类名,接口,访问修饰符等,类加载器引用。
2.方法与字段信息:方法的参数列表,方法的返回类型,字节码指令,字段的类型和名字。
3.运行时常量池(每个类都会独一份):是根据.class文件的常量池转换而来,储存字面量,符号引用,直接引用。
JDk7及之前:采用的是永久代实现(上面也提到过),是存在堆中。
JDK8及之后:采用元空间实现,存在本地内存中,JVM是无权管理的。
不同阶段常量储存方式不同:
第一步:.class文件常量池(编译后静态储存)
java文件编译为.class文件时,会生成静态常量池,采用紧凑的"索引+类型标记"结构储存。
编译后的"常量清单",避免重复储存相同变量。
第二步:运行时常量池(类加载后动态储存)
1.类加载阶段:此阶段,将.class文件逐一生成运行时常量池 的条目。然后此时储存字符串和类引用等是以"符号引用"形式储存(如类全限定名字符串,方法索引)
储存位置:元空间(本地内存),无堆实例,stringtable不参与。
2.解析阶段:JVM将"符号引用 "转换为"直接引用 ",创建堆实例,解析阶段时(首次使用字符串时懒解析)
过程:JVM检查堆内存是否已存在该字符串,不存在,就在堆中创建String实例,其底层是字符数组。若存在,就直接复用已存在的实例。
解析完成后,JVM隐式执行String.intern()将字符串的哈希值,以哈希值为key,查询stringtable中是否存在该key,如果不存在,就会将该字符串中的堆地址作为value,存入stringtable 中,如果存在,则忽略(确保同一字面量仅缓存一次地址)
stringtable(串池):
stringtable特点:
1.常量池中的字符串仅是符号,第一次解析时,就会变成对象。
2.利用串池机制,来避免重复创建字符串对象。
3.字符串变量拼接原理是:stringbuilder中的append。
4.字符串变量拼接的原理是编译器优化,例如String a="a"+"b"+"c",那么编译器优化,优化为"abc"储存在堆中。
5.可以使用intern方法,可以主动将串池没有的字符串对象放入到串池。
intern方法:
jdk1.8:当执行intern方法时,将这个对象尝试放入串池,如有串池有该值,则不放,返回地址。如果没有,就会主动放入到串池中,返回地址,同时执行intern方法的对象的内存地址也会改变。
jdk1.6:只有一点不同,在调用intern的方法时,如果串池中没有,会被拷贝一份新的放入串池。和调用该方法的对象的内存地址指向不一样,但是jdk1.8是一样的。
stringtable位置:
1.6的时候,在方法区中,会导致stringtable回收不及时。
1.8的时候,就存在堆中。
stringtable底层是哈希表,垃圾回收的性能调优:
1.哈希桶的个数,个数越多,哈希冲突就会得到缓解,性能也能变高。
2.考虑对象是否入池。
频繁创建短期存活的字符串,如果被存入stringtable中,称为GC root的一部分,就不会被回收,堆积占用堆的内存。
及时清理长期无效的缓存。
直接内存:

JVM直接内存也叫堆外内存,不在JVM运行时的堆栈内,所以直接内存不是由JVM直接控制的。由操作系统直接管理的内存区域。
申请方式:
通过NIO包ByteBuffer.allocateDirect(int capacity)来申请,而非普通的堆内存(通过new来申请)。
性能优势:
读写效率高------避免了JVM堆区和操作系统本地内存之间的数据拷贝(即零拷贝),适合高频IO场景。(网络通信,大文件读写)
成本特点:
分配/释放的开销比较大,因此通常用于长期复用的内存快
内存管理:
直接内存是不由JVM管理的,但java的DirectByteBuffer会通过虚引用(Cleaner)来关联直接内存,当后续DirectByteBuffer被GC回收后,Cleaner会触发操作系统(调用unsafe)回收对应的直接内存。
**注意:**当直接内存使用不当,如果有大量创建DirectByteBuffer对象,就会导致直接内存溢出。(OOM)