文章目录
JVM内存结构包括 程序计数器,本地方法栈,虚拟机栈,堆,方法区,
程序计数器
java源代码----->编译器----->二进制字节码 .class文件---->解释器----->机器码------>cpu执行
定义
是线程私有的,一块很小的内存区域,作为当前线程的行号指示器,用于记录当前执行的线程的下一条指令地址
作用
程序计数器在指令的执行过程中记住下一条指令执行的地址,解释器通过读取程序计数器中的地址来执行下一条指令,通过寄存器实现,寄存器是cpu读取最快的组件,读取操作比较频繁
特点
- 线程私有,随着线程的创建而创建,线程销毁而销毁
- 唯一一个不会存在内存溢出的区域
虚拟机栈
定义
java虚拟机栈,线程运行时需要的虚拟空间:每个栈内有每个栈帧组成,
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,每一个栈帧对应着一个方法的调用,也就是说每个方法运行时需要的内存,栈帧内容包括方法参数,局部变量,返回地址
- 每个线程只能有一个活动栈帧,对应着当前执行的方法
面试问题
- 垃圾回收是否设计栈内存?
不涉及,因为栈内存就是方法一次次调用产生的栈帧内存,而栈帧内存在方法每次调用后自动释放内存,垃圾回收是回收堆内存中的无用对象
2.内存分配越大越好吗?
栈内存 -Xss ,栈内存越大,反而会使线程数变少,因为物理内存是一定的,比如说一个线程使用的是栈内存,一个线程使用了1m内存,物理内存有500m,理论上可以有500个线程同时运行,但是如果每个栈内存时2m内存,只有250个线程同时运行
调大了只是进行更多次的方法递归调用,而不会增强运行效率
3.方法内的局部变量是否是线程安全?
看一个变量是否线程安全,其实就是看这个变量是线程共享的还是私有的,
- 如果方法局部变量作用范围没有逃离方法作用范围,每个线程有自己私有的帧内存,局部变量在每个帧中的栈帧存储,所以是线程私有的,即线程安全就是线程安全
- 但是局部变量引用了对象,并且为返回值返回,就需要考虑线程安全问题,就是线程不安全
栈内存溢出
- 栈帧过多,导致栈溢出,比如方法递归调用中,没有设置终止条件,就会导致栈溢出
- 栈帧过大,导致栈溢出,一个栈帧大小超过栈内存大小
线程运行诊断
Cpu占用过多
linux命令:
- 定位:先用top 实时检测cpu运行情况定位哪个进程对cpu占用过高
- 用ps H -eo pid,tid,%cpu | grep 进程号 定位哪个线程占用过高
- jstack进程id 定位线程来查看具体出现问题,定位到源代码行数
程序运行时间很长没有结果(死锁现象)
tips:产生死锁的四个条件—>互斥,不可剥夺,请求和保持,循环等待
本地方法栈
定义
线程私有,java在调用本地方法时用到的内存区,当JVM创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是动态链接并直接调用该方法
本地方法:不是使用java语言写的方法
堆
jps查看进程id
jmap命令可以查看堆内存情况
定义
java虚拟机中内存区域最大的一块,几乎所有的实例对象都在这里分配内存
程序计数器,虚拟机栈,本地方法栈都是线程私有区域,堆和方法区为线程共享区域
- 通过new关键字,创建的对象都会使用堆
- 有垃圾回收机制
- 线程共享,考虑线程安全问题
堆内存溢出
public static void main(String[] args) {
int i = 0;
try{ List<String> list = new ArrayList();
String s = "hello";
while (true) {
list.add(s);
s =s+s;
i++;
}}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
产生异常
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.lsy.Jvm.HeadDemo.main(HeadDemo.java:14)
堆空间虚拟机参数**-Xmx**
堆内存诊断
- jps工具
- 查看当前系统中有哪些java进程
- jmap工具
- 查看对内存占用情况 jmap -heap 进程数
- jconsole工具
- 图形界面的,多功能的检测工具,可以连续加测
案例 垃圾回收后,内存占用依旧很高
方法区
定义
-
存储了类的相关信息,比如类的成员变量,成员方法,以及构造器方法,在虚拟机启动时被创建
-
在JDK1.6之前,使用永久代作为方法区的实现,永久代包含存储类的信息,类加载器.运行时常量池(包括StringTable)
-
在JDK1.8之后,永久代废弃,使用元空间 元空间里存储类的信息,类加载器,常量池(不包含StringTable),不占用堆内存,jvm不管理它的内存结构,由本地内存管理,StringTable不再放到元空间部分,被移到heap堆里
方法区内存溢出
- JDK1.8之前会产生永久代溢出
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize = 8m
- JDK1.8之后会产生元空间内存溢出
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspaceSize = 8m
场景:
- spring框架
- mybatis
运行时常量池
程序的运行先编译成二进制字节码,有类的基本信息,类的常量池,类的方法定义
- 常量池就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等
- 运行时常量池,常量池是*.Class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
StringTable
-
常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是StringBuilder(1.8)
-
字符串常量拼接的原理是编译期优化
package com.lsy.Jvm; public class StringTable { //常量池中的信息,都会被加载到运行时常量池中,这是a ,b ,ab 都是常量池中的符号,还没有变为java字符串对象 //ldc #2 会把a符号变为"a"的字符串对象,准备好一块空间StringTable[],变为a字符串对象后作为key去StringTable去找,如果没找到就把a字符串对象放入串池 // ldc #3 会把"b"变为字符串对象,先去StringTable中去找, //ldc #4 会把"a,b"变为字符串对象 public static void main(String[] args) { String s1 = "a";//用到了才会创建 String s2 = "b"; String s3 = "ab"; /*变量字符串拼接,s1和s2为变量,在运行时有可能被修改,使用StringBuilder.append进行动态拼接 * 先创建了一个StringBuilder,然后调用StringBuiler无参构造,先把s1调用进来,调用append方法,把s2调用进来,调用append方法,之后再toString * new StringBuilder.append().append().toString() new String() * */ String s4 = s1+s2; //System.out.println(s3 == s4);//false s3是串池中的对象 ,s4是new出来的在堆里面的 /*常量字符串拼接 在编译期 * 先在串池中寻找,如果串池中没有就创建 * s3 =s5 javac在编译期间的优化,结果已经在编译期确定为ab * */ String s5 = "a" + "b"; } }
-
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
-
1.8将这个字符串对象尝试放入串池中,如果有就不会放入,如果没有就会放入串池,就会把串池中的对象返回
String s = new String("a") + new String("b"); String intern = s.intern(); System.out.println(intern==x );//true System.out.println(s==x );//true
String x = "ab"; String s = new String("a") + new String("b"); String intern = s.intern(); System.out.println(intern==x );//true System.out.println(s==x );//false
-
1.6将这个字符串对象尝试放入串池中,如果有就不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
-
StringTable位置
JDK1.6之前在永久代,永久代回收效率很低,永久代在fullGC时才会垃圾回收,但是fullGC等到老年代不足时才会触发,触发的实际有点晚
StringTable使用的频率有点晚,如果回收效率不高,就会导致永久代内存不足.
所以JDK1.8之后转变到堆中,在堆中miniGC就会触发垃圾回收,把一些用不到的字符串常量回收
StringTable垃圾回收
当内存空间不足时,StringTable没有被引用的字符串常量也会被垃圾回收
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1737 = 41688 bytes, avg 24.000 //存储字符串对象
Number of literals : 1737 = 156464 bytes, avg 90.077//字符串常量
Total footprint : = 678256 bytes
Average bucket size : 0.029
Variance of bucket size : 0.029
Std. dev. of bucket size: 0.171
Maximum bucket size : 3
stringTable发生垃圾回收
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->696K(9728K), 0.0036188 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2744K->704K(9728K), 0.0016845 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2752K->704K(9728K), 0.0041612 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
StringTable性能调优
- 如果字符串常量个数比较大,可以调试StringTable桶的个数 -XX:StringTableSize 有更好的hash分布,减少hash冲突
- 考虑将字符串对象入池
直接内存
定义
- 主要用作NIO(ByteBuffer)操作,用于数据缓冲区
- 分配回收成本较高,但是读写性能会非常高
- 不属于jvm内存管理,属于操作系统内存,不受jvm内存回收管理
- 也会产生内存溢出,oom:direct buffer memory
java本身不提供磁盘读写,要调用磁盘读写必须调用操作系统中的函数,调用本地方法native
从用户态转变为内核态,在内核态cpu读取磁盘内容,磁盘内容先分次读取到系统缓存区,系统缓冲中的数据java不能够运行,所以在堆内存中分一块java缓存区,从系统内存中把数据间接的读取到java缓冲区,反复进行读写操作
不使用直接内存所在问题:有两块缓存区,系统内存和java堆内存,读取的时候涉及到数据要存两份,从系统内存中把数据间接的读取到java缓冲区,反复进行读写操作,这样造成不必要的数据复制,效率不是很高