一、程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
1.2 作用
- 程序计数器作用: 记录下一条jvm指令的执行地址
编译:
- java 源代码 (经过一次编译得到) -> 二进制字节码 (jvm指令)
执行流程:
- jvm指令 -> 解释器(翻译) -> 机器码 - > CPU(执行)
- 通过程序计数器记录jvm指令的地址,解释器就可以通过程序计数器来获取下一次需要翻译成机器码的代码地址
寄存器:
- 整个CPU读取速度最快的一个单元
在物理上:
- 程序计数器是通过寄存器实现的
- 程序计数器是java对物理硬件的屏蔽与抽象
1.3 特点
时间片:
- 1、给线程1分配一个时间片,如果在时间片内没有执行完成,那么就会将线程1的状态进行一个暂存,然后切换到线程2去,然后等到线程2执行的时间片结束之后进行暂存之后在切换到线程1去执行接下来的代码。
- 2、暂存也是使用到程序计数器, 因为程序计数器是线程私有的,所以每个线程获取到的是自己上一次暂存的位置,互不影响
多个线程同时运行的时候,CPU会有一个调度器组件,给他们分配时间片
总结:
- 1、线程私有的
- 2、不会存在内存溢出(其他堆、栈、方法区都会)
二、虚拟机栈
栈 - 线程运行需要的内存空间
栈帧 - 每个方法运行时需要的内存
2.1 定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 1、每个线程运行时所需要的内存, 称为虚拟机栈
- 2、每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 3、每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法
问题辨析:
1、垃圾回收是否涉及栈内存?
答: 否,垃圾回收器只会回收堆内存中的内存占用,栈的空间执行完之后会弹出,出栈,所以不需要垃圾回收器回收
2、栈内存分配越大越好吗?
答:一般通过一个参数来指定。
-Xss size (linux 、 macOS、Oracle Solaris 默认是1024KB, windows根据windows的虚拟内存)
栈内存分配的越大,反而会让线程数变少
例:
如果总内存有500m,分配了栈内存分配了1m,那么一共可以有500个线程同时运行
如果栈内存分配了2m,那么就只能有250个线程同时执行了。
栈内存分配的越大,不会增快我们运行的效率,只能让我们进行更多次方法的递归调用。
所以采用系统默认占内存大小就可以了
3、方法内的局部变量是否是线程安全?
答:
局部变量是线程安全的
全局静态变量不是线程安全的
1、如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
2、如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全的
2.2 栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
2.5线程运行诊断
- 1、cpu占用高
后台运行java代码: nohup java com.liziing.jvm.Demo1 &
top 命令: 查看进程的所有信息 (用来定位哪个进程对cpu的占用过高)
ps H(打印进程数) -eo pid tid %cpu(感兴趣的进程 线程 cpu信息)
线程数太多 可以使用管道 过滤 | grep 进程id (用来定位是哪个线程引起的cpu占用过高)
使用jdk提供的工具 jstack
命令 jstack 进程id (这个进程中所有java进程列出来) 进程id -> 十进制转十六进制
根据上面通过ps定位出来的线程id找到有问题的线程,进一步定位到问题代码的源码行号
- 2、 程序运行了很长时间没有结果
死锁:
四个必要条件: 互斥、不可抢占、请求且占有、循环等待
可以用jstack 进程id 来排查
三、本地方法栈
java虚拟机调用一些本地方法时,需要给这些本地方法提供的一些内存空间。
本地方法: 使用c/c++编写的代码。(java代码有一些限制,有时候不能很好的跟操作系统底层进行打交道)
例如:关键字 native
- Object.clone()
- Object.wait();
所以java代码可以间接通过本地方法来跟底层打交道
本地方法使用的时候所使用的空间就是本地方法栈
四、堆
4.1 定义
Heap堆
- 通过new关键字,创建对象都会使用堆内存
特点:
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
-Xmx size 可以设置堆内存大小
4.2 堆内存诊断
1、jps工具 jps
- 查看当前系统中有哪些java进程
2、jmap工具 jmap -heap 进程id
- 查看堆内存中占用情况
3、jconsole工具
- 图形界面的,多功能的监测工具,可以连续监测
案例:
- 垃圾回收后,内存占用仍然很高
解决: 使用jvisualvm
堆转储 dump
五、方法区
5.1 定义
- 所有虚拟机线程共享的
- 存在类的信息 (方法,属性 ,构造器方法...)
- 虚拟机启动时创建的
- 逻辑上是堆的组成部分,但是不限制方法区的位置
Oracle hostfor
在jdk1.8之前使用的永久代,使用堆当作方法区
1.8之后,使用的是元空间,使用的是本地内存(操作系统的内存)
- 方法区也会导致堆内存溢出
5.2 内存溢出
永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
最大元空间:-XX:MaxMetaspaceSize=8m
元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
最大永久代空间: -XX:MaxPermSize=8m
场景
spring使用了cglib技术,生成一些代理类,aop中的核心
mybaits也使用了cglib技术,生成一些mapper实现类
都是生成类加载到方法区,都会导致内存溢出的情况
5.3 常量池
类要运行需要先编译成二进制字节码文件(类的基本信息、类的常量池、类中方法定义、包含了虚拟机指令)
5.4 运行时常量池
- 常量池, 就是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(字符串,整数,布尔类型)等信息
- 运行时常量池,常量池是存储在 *.class文件中的,当该类被加载时,它的常量池信息就会放入到运行时常量池,并把里面的符号地址变为真实地址,由此可见,运行时常量池也是每一个类都有一个。
5.5 StringTable[串池]
逻辑上属于方法区,物理上存放在堆中
数据结构上是一个哈希表
只有执行到对应的代码,才开始创建,才会将a在运行时常量池中的符号变成"a"的字符串对象,然后去串池中找,如果存在就跳过,如果不存在,就将生成的字符串放入到串池,行为上是懒惰的。
5.5.1面试题1
1、问s3 == s4
答:false
- s3是存放在串池
- s4拼接两个变量s1 + s2,底层是通过创建一个新的字符串,是通过new关键字创建,所以是存放在堆中的,所以两个的地址,位置是不一样的,是两个对象,所以不相等。
2、问s3 == s5
答:true
- 因为是两个常量的拼接,所以javac在编译期间优化,结果已经在编译期间变成ab,所以是存在串池中的,所以跟s3引用的是同一个串,也就是同一个对象。
5.5.2 StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变成对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder (1.8)
- 字符串常量拼接的原理是编译器优化
- 可以使用intern() 方法,主动将串池中还没有的字符串对象复制一份放入串池 (JDK 1.6及以前)
- 可以使用intern() 方法,主动将串池中还没有的字符串对象放入串池 (JDK 1.7/1.8以后的)
intern()使用1
答案:true true (JDK 1.7/1.8以后的)
true false (JDK 1.6及以前)
解析 (JDK 1.7/1.8以后的):
1、获取常量"a" 放入到串池,然后new 创建了一个字符串a,放到堆中
2、获取常量"b"放入到串池,然后new创建了一个字符串b,放到堆中
3、因为两个是new出来的,也就是变量拼接,底层使用的是StringBuilder拼接,最后会创建(new)一个新的字符串ab也就是s表示,放到堆中。
4、s调用intern方法,尝试将ab这个字符串对象放入到串池中,如果串池中存在,那么就不会放入,如果没有则会将该对象放入串池------最后会把串池中的对象返回。
5、这里s为什么也会等于,因为串池中没有该串,所以将s这个对象放到串池中了,放进去,也就是从堆中移到了串池中,所以s指向也从指向堆,变成了指向串池中的ab。
intern()使用2
答案:true false
6、这里s为什么也会等于,因为串池中有该串,所以将s这个对象不会放到串池中了,也就是s还是指向原来的堆中的地址,所以是false
但是为什么s2还是true,因为intern方法不管放入成功与否,都会返回串池中的对象,所以s2还是true。
5.5.3 面试题2
答案: false true true false true(调换位置) false(jdk1.6)
5.5.4 StringTable的位置
- jdk 1.6 StringTable是在永久代的常量池里面,缺点是只有Full GC的时候才会回收,整个老年代的空间不足时候才会回收,触发时机有点晚,StringTable回收效率不高。
- jdk1.7以后StringTable转移到堆中,因为堆中有垃圾回收器minorGC(新生代)就会自动回收,回收效率高。
jdk1.6空间不足(java.lang.OutOfMemoryError: PermGen space)
jdk1.8 (java.lang.OutOfMemoryError: GC overhead limit exceeded)
为什么这里不是报heap space 异常呢,因为1.8以后,会有一个机制,如果GC花费了百分之98的时间来回收堆空间,但是只回收了不到百分之2堆空间,说明没救了,就会报这个错误,只要设置参数 (- 表示去掉 + 表示 显示)
去掉之后就会显示回正常的错误
jdk1.8空间不足(java.lang.OutOfMemoryError: Java heap space)
5.5.5 垃圾回收机制
- 串池中的字符串常量也是会被垃圾回收的
5.5.6 StringTable 性能调优
底层是一个哈希表
桶的概念:哈希表底层的数组长度
桶变大,哈希分布均匀,效率高
桶变小,哈希分布冲突,效率低
结论1:如果系统里字符串常量池中个数较多,建议调高桶的大小,有一个更好的哈希分布,减少哈希冲突,增加串池的效率
六、直接内存
属于操作系统的内存
6.1 定义
Direct Memory
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
数据需要存两份,所以效率不是很高
使用了direct Buffer得到的效果(java代码可以直接访问的内存区域,少了一次缓冲区复制,效率成倍增加)
这是nio的基本操作,需要深入,感兴趣可以参考一些nio的相关知识
6.2 直接内存溢出
示例:
6.3 直接内存释放原理
垃圾回收只能释放java的内存,
直接内存需要主动调用Unsafe对象的freeMemory方法来进行释放(Unsafe需要通过反射的方式来获取)
6.4 分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收时候需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法来释放直接内存
-XX:+DisableExplicitGC
调用System.gc() //显示的垃圾回收,Full GC 不光回收新生代, 老年代 会影响性能
防止程序员多次调用System.gc() 那么加上这个参数设置,就能够使得这段显示垃圾回收失效
问题:那么直接内存不会自动释放?
解决:可以根据原理,手动管理调用unsafe的freeMemory方法来释放直接内存