JVM概述和内存结构
一、JVM概述
定义:
Java Virtual Machine - java程序的运行环境 (java二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界越界检查
- 多态 (使用虚方法调用的方式实现多态)
比较:
jre jdk jvm
常见的JVM:
二、内存结构
2.1 程序计数器 (Program Counter Register PCR)
- 定义:物理上用寄存器
- 作用:记住下一条jvm指令的执行地址
- 特点:
- 线程私有,每个线程维护一个程序计数器,随着线程创建而创建,销毁而销毁。
- 不会存在内存溢出,是一块较小的内存空间
2.2 虚拟机栈
-Xss定义栈大小
定义:
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)(包含参数,局部变量,返回地址)组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题:
- 垃圾回收是否涉及栈内存?
垃圾回收只涉及堆内存,不涉及栈内存
- 栈内存分配越大越好吗?
并不是越大越好,内存空间是固定的,栈内存越大,可用线程数则越少
方法内的局部变量是否线程安全?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
// 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全
//线程安全
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//线程不安全,可以将StringBuilder改为StringBuffer
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//线程不安全,因为它作为返回值逃离方法的作用范围。
public static StringBuilder m2() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
2.2.1 栈内存溢出
- 栈帧过多导致内存溢出(错误的递归调用)
- 栈帧过大导致内存溢出(不常见)
2.2.2 线程运行诊断
案例1:cpu占用过多
定位
- 用top定位哪个进程对cpu的占用过高
- ps H -eo pid,tid,%cpu|grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
- jstack 进程id
- 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行数
案例2:程序运行很长时间没有结果
2.3 本地方法栈
调用本地方法时,给本地方法的运行提供内存空间
native方法:不是由java代码编写的方法。因为java代码存在限制,有时候不能够直接和操作系统进行交互。
举例:clone(), hashCode(), notify(), notifyall(), wait()
2.4 堆
定义
Heap 堆
- 通过new关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
展开:对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,比如String s = new String(“william”);会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。
堆内存溢出
-Xmx定义堆内存大小
堆内存诊断
jps
- 查看当前系统有哪些java进程
jmap工具
- 查看堆内存占用情况 jmap -heap 进程id
jconsole工具
- 图形界面的,多功能的检测工具,可以连续检测
jvirsualvm
2.5 方法区
方法区定义:
- 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是所有Java虚拟机线程共享的区域。当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
- 保存在着被加载过的每一个类的信息;这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中。
- 方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
- 方法区在虚拟机开启时创建。
2.5.1 组成部分
2.5.2 方法区内存溢出
- 1.8以前会导致永久代内存溢出
- 1.8以后会导致元空间内存溢出
场景
- Spring
- Mybatis
2.5.3运行时常量池
- 常量池:一张表,虚拟机指令根据这张常量表找到要执行的而类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是*.class文件中的,当该类被加载时,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。
2.5.4 StringTable
1 | // StringTable ["a", "b", "ab"] hashtable结构,不能扩容 |
2.5.5 StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(1.8)
- 字符串常量拼接的原理是编译期间优化
- 可以用intern方法,主动将串池中还没有的字符串对象放入串池,会把串池的对象返回
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池中,会把串池中的对象返回
1 | //1.8 |
2.5.6 StringTable位置
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
2.5.7 StringTable 性能调优
- -XX:StringTableSize=20000 调整桶个数 -XX:+PrintStringTableStatistics
StringTable 底层使用哈希表实现的,增加bucket的数量,就可以减少哈希碰撞,从而实现性能调优。
- 考虑将字符串对象是否入池
2.6 直接内存
定义:
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制
2.6.1 分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
传统io
使用直接内存
参考:
install_url
to use ShareThis. Please set it in _config.yml
.