JVM笔记-JVM内存结构
1、JDK JRE JVM?
JDK:
- Java程序的运行环境-JRE
- JVM java虚拟机
- jvm有自己完善的硬件架构,如处理器、栈区、寄存器等。
- jvm是一个虚拟的中间平台,只负责将编译后的字节码文件转换成当前计算机能理解并执行的指令,其他都不关心。jvm是java“一次编译,到处执行”的原因。
- 核心类库 java.lang java.utils 等
- JVM java虚拟机
- Java的基础类库(Java API)重要的语法结构和基本的线程、图形和IO等
- Java的一些工具包(JDK的bin目录下)(其中包含了javac源码编译器,还有一些其他的命令:jdb,javah,jmp等)
2、java程序的执行过程
编译阶段: java 文件 编译器把 .java 源代码文件编译成 .class 字节码(二进制)文件
运行阶段:Java类加载器将 .class 字节码文件加载到内存,在 JVM 中进行解释并生成可执行代码
运行阶段最重要:
类加载器讲字节码文件加载到 JVM 内存中运行
- 类放在放在方法区
- 类的实例对象放在 堆内存
队中的对象调用方法是会用到 虚拟机栈、程序计数器、本地方法栈中
方法执行时,每行代码由执行引擎中的解释器逐行执行,方法中的热点代码由JIT 编译器进行优化 执行引擎中的GC模块用于垃圾回收
- Java 代码不方便实现的功能需要借助 本地方法接口调用系统接口
3、JVM内存结构
3.1 PC 程序计数器(寄存器)
作用:1 是流程控制 记住下一条jvm指令的执行地址(执行引擎的代码解释器需要知道下一条代码才能)2 线程切换 上下文切换
特点
是线程私有的 (随着线程的创建而创建销毁而销毁)
不会存在内存溢出
3.2 Java 虚拟机栈
每个线程运行时所需要的内存,称为虚拟机栈(线程私有)
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存(其大小由方法的参数,局部局部变量所决定)
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问:
- 垃圾回收是否涉及栈内存?弹栈就是垃圾回收过程,GC仅限于堆内存
- 栈内存分配越大越好吗?通过JVM参数xss分配一般为1M。不是,栈内存越大反而使支持线程数变少,变大只会让递归调用次数变多
- 方法内的局部变量是否线程安全? 看线程共有的还是私有的
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,或者静态成员变量,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出的原因?StackOverflowError
栈帧过多导致栈内存溢出
- 递归调用过多
栈帧过大导致栈内存溢出
- 循环引用
3.3 本地方法栈
本地方法,无法通过java代码实现的功能,通过本地方法进行与操作系统接口交互,这种本地方法使用的内存叫做本地方法栈
举例,object类的 native
方法(C实现) 包括 clone
,notify
,wait
等
3.3.1 线程运行诊断
案例1、cpu 占用过多
用
top
定位哪个进程对cpu的占用过高ps H -eo **pid**,**tid**,%cpu | grep 进程id
(用ps命令进一步定位是哪个线程引起的cpu占用过高)jstack
进程id可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
案例2:程序运行很长时间没有结果 同理
3.4 Heap 堆
- 通过 new 关键字,创建对象都会使用堆内存
特点
它是线程共享的,堆中对象都需要考虑线程安全的问题
OutOfMemoryError
有垃圾回收机制
3.4.1 堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程
- jmap 工具
查看堆内存占用情况 jmap - heap 进程id
- jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
3.5 方法区
- 首先方法去使JVM中所有线程共享的一块区域
- 存储了 类的结构有关的方法 ,包括方法 构造器等代码
- 方法其在虚拟机启动时建立,逻辑上属于堆的一部分
- 如 Oracle Hotspot 虚拟机 在1.8之前 实现叫做永久代使用了对的内存 1.8之后叫做元空间使用了系统内存一部分
- 可以这样理解: 方法区只是一个概念,或者标准。永久代和原空间属于方法区的不同实现
- 方法区内存溢出也会导致
out of memory
- 1.6 之前 设置
-XX:MaxPermSize=10m
1.8 后设置 :Xmx10m -XX:-UseGCOverheadLimit
3.5.1 java1.6 到 1.8 的方法区 区别
1.6的方法区(永久代) 与堆内存共享JVM内存结构,宽泛意义上来说,方法去占用了或者属于堆内存区域),其中方法去包含 class classloader(可以加载类的二进制字节码) 常量池(常量池中包含StringTable)
1.8 相比于1.6 把方法区(元空间)移出了JVM内存,放在了本地内存, 并且把StringTable放到了堆内存
所以
1.8之前会有以前会导致永久代内存溢出
OutOfMemoryError: PermGen space
1.8之后会导致元空间内存溢出
java.lang.OutOfMemoryError: Metaspace
- 1.8 之后会元空间有自己的垃圾回收机制,效率会高一些
场景:
Spring 用CGlib 技术来生成代理类是AOP技术的核心
MyBatis CGlib 产生的Mapper接口的实现类
CGlib 用于运行期间动态生成二进制字节码实现动态字节码
这些类的动态加载使用不当就会导致内存溢出
3.5.2 运行时常量池
什么是常量池?
javac 编译为二进制的字节码:
包含三部分:类基本信息、常量池、类方法定义 都包含了虚拟机指令
- PS: 通过 javac 编译 java -v 反编译(反汇编)查看人能理解的”字节码“
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
3.5.3 StringTable:
经典面试问题
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);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
1、 上述前两行代码都是采用 惰加载的方式创建的(基本上所有java对象都是惰加载的方式),只有执行到这样代码时,才会讲字符串对应的字符串对象放入”串池“(才算真正创建这个对象);同时注意,只有”串池“中没有这个数据才会向其添加,有的话只会将引用指向它
2、
String s3 = "a" + "b"
; 编译器进行优化 直接优化为String s3 = "ab"
所以System.out.println(s3 == s5)
结果为true;3、
String s4 = s1 + s2
; 首先创建一个`StringBulider对象 在调用 append方法 把 s1 和 s2 拼起来 变成StringBuilder 类型的”ab“,然后toString 转成String 等价于 new String(“ab”) 这个对象不会放在常量池,而会放在堆内存
所以
System.out.println(s3 == s4);
结果为false
因为一个在常量池一个在堆内存肯定地址不一样4、
String x2 = new String("c") + new String("d");
一共创建了几个“字符串对象”?
- “c”、”d” 作为常量被放入了串池中 (两个对象)
- new String(“c”) 、new String(“d”) 放入了堆中 (两个对象)
- new String(“c”) + new String(“d”) 调用 StringBulider对象拼接 并返回字符串类型 “cd” 注意这个字符串对象不会放进串池,只有字符串常量才会放入串池,拼接的只会放进堆里(一个对象,字符串对象)
- 共计五个字符串对象
5、
System.out.println(s3 == s6);
结果为ture;String s6 = s4.intern(); 会试图s4放入常量池中,但是发现常量池中已经有了,所以返回的是常量值中的地址。
关于 intern 方法,会将尝试这个字符串对象放入串池,如果串池不会放入,没有就会放入,这个例子中,有就不放入串池,就不放入串池了,所以
System.out.println(x1 == x2);
结果为false,因为x2虽然调用了intern 但是返回值才是串池;如果调换了【最后两行代码】的位置呢
为true ,可以理解为串池不是一个物理概念,而是一个逻辑概念;x2就地变为串池的内容
即
x2 = x2.intern();
就地变串池内容(或者说,是串池主动添加的他,他自己的地址不用变)如果是jdk1.6呢(如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回) 优惠常见一个对象
jdk1.6 串池像是一个物理概念一样,放入串池指的是,在串池中新建这个对象
即
x2!=x2.intern()
; x2.intern() 的返回地址是重新在串池中复制的字符串的地址。
总结:
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
StringTable 位置
1.7之前,放在永久代(方法区)中的常量池中
1.7,1.8 开始 将其移到了堆中
- 原因:
- 永久代内存效率低,只有Full GC 才会触发垃圾回收,之后老年代内存不足才会触发
- 而StringTable 比较活跃,如果回收效率不高会占用大量内存
- 证明 :大量制造 字符串将其存在StringTable 1.7之前报 永久代的内存溢出 1.8 会报 对空间 内存溢出
StringTable 性能调优
- StringTable 底层类似HashMap的实现机制,底层数组个数叫做桶(bucket)
- 调整 -XX:StringTableSize=桶个数 变大后减少hash冲突
- 考虑将字符串对象是否入池,入池的好处:重复多的话节约内存占用
3.6 直接内存
Direct Memory
定义
- 属于操作系统的内存
- 常见于 NIO 操作时,用于数据缓冲区 (ByteBuffer)
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
3.6.1 ByteBuffer为什么块?
- 先说普通的java代码读写为什么慢?
可以看到由于java内存属于系统外部内存,为了读写,至少进行两次不必要的复制(从系统缓存区到java缓存区,再从Java缓存区到系统缓存区)
而ByteBuffer 用allocateDirect(_100Mb) 方法分配的区域,两段内存可以共享,少了中间不必要的复制
3.6.2 直接内存分配和回收原理
上面说直接内存不受 JVM 内存回收管理,那么怎么操作才能实现回收,不使内存泄漏呢?
- 底层 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦
- ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
- 用 freeMemory 来释放直接内存 借助了虚引用的机制
4、分析工具总结
命令 | 平台 | 功能 |
---|---|---|
top | linux | 定位哪个进程对cpu的占用过高 |
ps H -eo pid,tid,%cpu | | grep 进程id | linux | 进一步定位是哪个线程引起的cpu占用过高 |
jstack [] | java自带 | 进一步定位那个进程 线程快照信息 |
jps | java | 查看当前系统中有哪些 java 进程 |
jmap [] | java | 用于生产堆转存快照打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量,GC算法等)。 |
jconsole 图形化工具 | java | 多功能的监测工具,可以连续监测 |