JVM
- JVM/JRE/JDK
- 示例
- JVM内存管理
- JVM整体架构
- 程序计数器
- 虚拟机栈
- 栈内存溢出
- 线程诊断 top/ps -H/jstack
- 案例一:CPU占用过多
- 案例二: 程序运行很长时间没结果
- 本地方法栈
- 堆
- 堆内存溢出
- 堆内存诊断
- 案例一:jps/jmap/jconsole工具使用
- 案例二:垃圾回收后,内存占用仍然很高
- 方法区
- 方法区内存溢出
- 常量池
- StringTable
- 示例一:字面量创建字符串
- 示例二:字符串变量拼接
- 示例三:字符串常量拼接
- 示例三:intern方法
- StringTable特性
- StringTable优化
- 直接内存
- IO/DirectBuffer拷贝文件
- 直接内存溢出
- JVM参数
JVM/JRE/JDK
- JVM: Java Virtual Machine的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算
机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM 保正确执行 .class 文
件,实现在 Linux、Windows、MacOS 等平台上运行。 - JRE Java Runtime Environment的缩写,JVM 标准加上实现的一大堆基础类库就组成了Java的运行环境
提供运行Java应用程序所必须的软件环境等。
JDK Java Development Kit的缩写,提供了一些非常好用的小工具,比如 javac、java、jar 等。
提供Java开发工具包
JVM好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查
- 多态
示例
简单看下Java程序的执行过程:
运行下面代码:遵循的就是 Java 语言规范。其中,调用了System.out 等模块,也就是 JRE 里提供的类库。
public class HelloTest {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
使用 JDK 的工具 javac 进行编译后,会产生 HelloWorld 的字节码。javap查看下字节码内容:
控制端输入:javap -v HelloTest
javap 将class反编译
v显示基本信息
查看main方法内:
0 getstatic #2 <java/lang/System.out> // getstatic 获取静态字段的值
3 ldc #3 <Hello World> // ldc 常量池中的常量值入栈
5 invokevirtual #4 <java/io/PrintStream.println> // invokevirtual 运行时方法绑定调用方法
8 return //void 函数返回
Java 虚拟机采用基于栈的架构,其指令由操作码和操作数组成。这些字节码指令
,就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode。
JVM 就是靠解析 opcode 和操作数来完成程序的执行的。当使用 Java 命令运行 .class 文件的时候,实际上就相当于启动了一个 JVM 进程。
然后 JVM 会翻译这些字节码,它有两种执行方式。
- 常见的就是解释执行,将 opcode + 操作数翻译成机器代码;
- 另外一种执行方式就是 JIT,即时编译,会在一定条件下将字节码编译成机器码之后再执行。
JVM内存管理
JVM整体架构
JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
程序计数器
Program Counter Register 程序计数器记住下一条jvm指令要执行的地址。(Java对硬件的抽象物理上通过寄存器实现(读取速度快))
当前指令所在的内存地址+当前指令所占用的字节数=下一条指令所在的内存地址
虚拟机指令的执行流程: 拿到一条指令,通过解释器翻译成机器码,才能交给CPU运行,程序计数器就是记住下一条jvm指令的执行地址。
观察这张图,不难发现每个指令前都有一个数字,这是指令的偏移地址,简称为行号,程序计数器的作用就是存储下一条指令的行号。
以上图为例,当第一条指令执行时,JVM会将下一条指令的行号“3”放入程序计数器中。当第一条指令执行完时,解释器会根据程序计数器中的行号拿到下一条指令,继续运行。
为什么要多此一举先把下一条指令的行号放入程序计数器中,直接取指令然后执行不是更简单吗?
JVM是支持多个线程同时运行的,这就涉及到CPU的调度问题了。线程甲正执行的好好的, 大哥CPU告诉甲说,你累了,我陪会儿乙,甲只好乖乖休息。一段时间后,大哥回来了,这时甲就可以根据程序计数器中的行号取到下一条指令接着执行。
这里有一点要注意,因为程序计数器记录的是行号,是会重复的,所以多个线程不能同时用一个,不然就乱了。所以程序计数器是线程私有的。
特点:
- 是线程私有的(每个线程执行的指令地址不一样)
- 不会存在内存溢,程序计数器中的行号永远只会有一个,当前指令执行时,会拿下一条指令的行号替换当前的行号。因此就不存在内存溢出问题。
查看二进制字节码:
1.安装插件:
2.查看
虚拟机栈
Java Virtual Machine Stacks 虚拟机栈,线程运行时所需要的内存空间。
一个线程对应一个虚拟机栈,多个线程对应多个虚拟机栈。
一个虚拟机栈,由多个栈帧(Frame)组成。一个栈帧对应着一次方法调用,方法调用时所需要的内存为栈帧,存储参数,局部变量,返回地址等。
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
垃圾回收是否涉及栈内存?
不涉及,栈内存是一次次方法调用时所产生的栈帧内存,栈帧内存在每次方法调用结束后会弹出栈,自动回收掉。
垃圾回收只会回收掉对内存中的内存占用,栈内存不需要。
栈内存分配越大越好吗?
-Xss 设置栈帧大小
操作系统默认栈大小:
每个线程在创建的时候都会创建一个虚拟机栈,而物理内存是固定的,栈内存划分的越大, 可分配的线程数就越少。
方法内的局部遍历是否线程安全?
如果方法内部局部变量没有逃离方法的作用范围它就是安全的,是线程私有的 (栈帧独有),不会产生在多个线程下产生线程干扰。
如果局部变量引用了对象,并逃离方法的作用范围,能被其他线程访问到,就不是线程安全的。
栈内存溢出
1.栈帧过多导致栈内存溢出(递归调用)
2.栈帧过大导致栈内存溢出
栈帧设置为256k,只循环3237次
线程诊断 top/ps -H/jstack
案例一:CPU占用过多
nohup java cc.java &
nohup 是 no hang up 的缩写,意思是不挂断运行,一直运行下去
& 是在后台运行的意思
top 命令检测进程对CPU的使用,定位哪个进程对cpu占用过高
ps H -eo pid,tid,%cpu
ps 查看线程情况
H 打印进程数
eo 输出要查看的内容 pid(进程ID) tid (线程id) %cpu
ps H -eo pid,tid,%cpu | grep 32655 定位哪个线程对CPU占位过高
jstack 32655(进程id) 会输出相关堆栈信息
jstack输出的线程信息是16进制的,我们将tid 32665 用计算器转为10进制可以看到是 7f99
线程的状态是runnable,显示第8行有问题,可以定位到Java源代码
案例二: 程序运行很长时间没结果
运行一段程序,想输出一个结果,但是一直不输出,可以查看相关堆栈信息
jstack pid
jstack最后一段输出中,发现Found one Java-Level deadlock,找到死锁,定位到Java源码的Thread-1的29行和Thread-0的21行。
本地方法栈
Native Method Stacks:java虚拟机调用本地方法时,所提供的内存空间。
本地方法定义:不是由java代码写的方法,由C或C++所写(因为c/c++方便和操作系统打交道,Java间接的通过调用本地方法来进行工作)。
本地方法例如:Object类,native修饰符等,Java的基础类库或执行引擎里都会调用本地方法。
堆
Heap:通过 new 关键字创建对象都会使用堆内存。
特点
- 是线程共享的,所以堆中的对象都需要考虑线程安全的问题
虚拟机栈中的局部变量都是线程私有的,只要它的局部变量不逃逸出方法的作用范围,就是线程安全的,但是堆中的对象,要考虑线程安全,因为它们是线程共享的。当然也有例外,后面讲解。 - 堆里面有一个垃圾回收机制
堆中不再被引用的对象,就会当成垃圾进行回收,以释放空闲的内存,不至于内存被创建的对象给撑爆。
堆内存溢出
当一个对象不被使用时,就可以成为所谓垃圾被回收掉,也就是它占用的内存会被释放掉。怎么还可能出现堆内存耗尽呢?
可以被回收的一个对象的条件是没人在使用它,但是如果不断的产生对象,而产生的这些新对象,仍然被使用,就意味着这些对象不能作为垃圾,这样的对象达到一定的数量,就会导致堆内存被耗尽,即堆内存溢出。
代码示例:
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a); // hello, hellohello, hellohellohellohello ...
a = a + a; // hellohellohellohello
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
初始的时候,a变量引用"hello",第一次变量,会把hello放入List集合中,接下来做一个字符串的拼接,变成了两个hello,组成了一个新的字符串,再循环的时候,会把两个hello的字符串对象。再追加到list集合,再做一次的拼接。以此类推,不断地把拼接的Hello对象加入list集合中,list的作用范围是从声明开始,一直都是有效范围,所以不能被垃圾回收掉,随着这个字符串就越来越多,它就会把堆空间占满。
Xmx 设置堆内存大小
控制堆空间的一个最大值,改小之后只循环17次。
堆内存诊断
案例一:jps/jmap/jconsole工具使用
代码示例:
public class Demo1_4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000); //停留时间敲命令
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null; //变量数组不再引用
System.gc();//进行垃圾回收
System.out.println("3...");
Thread.sleep(1000000L);
}
}
jps 查看当前系统中有哪些java进程,并展示进程号
jmap 查看进程在堆内存占用情况,只能查询某个时刻
新创建的对象,都会使用eden Space区,used代表使用内存,capacity代表总容量
执行到1,查看堆内存使用情况:
执行到2,查看堆内存使用情况:增加了10M,就是创建的byte数组
执行到3,查看堆内存使用情况:包含一些不用的信息都回收掉了
jconsole工具
图形界面,多功能检测工具,可以连续监测,还可以查看内存占用,线程情况,类加载数量等信息。
启动代码,控制台输入jconsole
选择进程,直接连接
可以看到堆内存的使用情况:
还可以检测线程死锁
案例二:垃圾回收后,内存占用仍然很高
程序运行起来之后,并不知道程序代码怎么编写的,可以借助一些工具来查看一下。
jps 查看进程id
jmap - heap 13556 查看堆内存使用情况
jconsole 执行jc,但是堆内存还是200MB空间
是不是有一些由于我们的一些编程失误,导致了很一些对象始终被引用而无法释放他们的内存呢
使用 jvisualvm 工具
与jconsole类似,也可以监测一个内存的占用,执行垃圾回收,检测线程,查看堆各个组成成分。
堆dump工具,抓取堆的当前快照,可以进一步对里面的一些详细内容进行分析。这是jmap,jconsole 工具所不具备的。
查找保留前20个内存最大的对象,点进去查询细节:
可以看到student对象中big属性,是一个byte数组占用空间大约是一兆左右。每个student 对象占用一兆的内存,两百个对象占内存两百多兆。
排查出问题:student对象以及list导致内存占用比较高。而且这个对象是长时间使用的,导致垃圾回收没办法回收他们的内存。
查看Java源码:
student对象的属性的大小是1兆。循环了两百次,都加到了一个list 中,而这个list 由于一直在main方法里,而main方法一直没结束,所以都是在它的生存范围内,所以一直没能被回收,就导致内存占用居高不下。
/**
* 演示查看对象个数 堆转储 dump
*/
public class Demo1_13 {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}
在实际的生产环境下分析的手段和排查的方式也是类似。通过堆转储功能(堆dump)把内存快照抓下来。再分析对内存占用最大的那些对象,就可以得出一些有用的结论,再去分析和排查你的原始代码就可以了。
方法区
jvm中对方法区的定义:Chapter 2. The Structure of the Java Virtual Machine
方法区是JVM所有线程共享的区域,它存储了与类结构相关的信息:运行时常量池,成员变量,方法数据,成员方法、构造方法的代码。它的大小就决定了系统可以保存多少种类。
方法区在虚拟机启动时创建,它在逻辑上是堆的组成部分(具体实现上是否为堆的一部分视厂商而定,如永久代,元空间都是其实现)。JVM规范并不强制方法区的位置。
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类会出现OOM错误。
在JDK 7以前,习惯上把方法区称为永久代(习惯上),而到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替。这两个最大的区别就是:元空间不在虚拟机设置的内存中,而是使用本地内存。
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区内存溢出
代码示例:
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 类加载器可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
运行之后并没有出现溢出,因为Java8之后使用元空间,使用的是系统内存,所以设置元空间初始大小为1.8
-XX:MaxMetaspaceSize=8m 设置原空间初始大小 1.8
1.8环境,Metaspace代表元空间导致的OOM
-XX:MaxMetaspaceSize=8m 设置元空间初始大小
1.6环境,PermGen代表是永久代导致的OOM。
XX:MaxPermSize=8m 设置永久代初始大小
常量池
JVM指令参考:https://blog.csdn.net/A598853607/article/details/125026953
通过一段代码认识常量池:
示例代码:
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("hello world");
}
}
hello world要运行,需要先编译成二进制字节码。
字节码由三部分组成:类的基本信息,类的常量池,类中的一些方法定义(包含虚拟机指令)。
javap -v helloWord 查看二进制反编译后内容
PS D:\java\idea\IdeaProject2\jvm\out\production\jvm\cn\itcast\jvm\t5> javap -v HelloWorld
警告: 二进制文件HelloWorld包含cn.itcast.jvm.t5.HelloWorld
Classfile /D:/java/idea/IdeaProject2/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
Last modified 2023-2-15; size 567 bytes
MD5 checksum 8efebdac91aa496515fa1c161184e354
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 cn/itcast/jvm/t5/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
类的基本信息:
常量池:
方法定义:
查看虚拟机指令:虚拟机指令后的注释是java程序帮忙加上的。解释器在翻译虚拟机指令的时候,只看到没加注释的这几行指令。
常量池就是一张常量表,jvm虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(字符串和基本数据类型)等信息。
运行时常量池就是存在 *.class 文件中的,当该类被加载到虚拟机以后,它的常量池中的信息会被加载到运行时常量池中(在内存中的位置)。并把符号地址变为内存中的真实地址。
StringTable
在运行时常量池中,有一块逻辑区域是StringTable(串池),用于存放字面量,串池实际上是一个hash表,不能扩容。当jvm指令在执行时,当执行到字面量信息时,会去串池中查找是否有该字面量,如果没有该字面量将其放入到串池中。
示例一:字面量创建字符串
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
}
}
常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
当运行到此方法,执行到对应的代码时(jvm执行 ldc #2) 才会把 a 符号变为 “a” 字符串对象,并将对象放入串池(StringTable[]) 中,StringTable是一个哈希表,长度固定,“a”就是哈希表的key。一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]。执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]。同样地,后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]。
小结一下:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象。
普通的java对象在类加载的时候就会生成并放入堆中,而这种方式生成的String不同,只有当执行到新建String的代码时才会生成字符串对象。
astore_0 将引用类型或returnAddress类型值存入局部变量0
示例二:字符串变量拼接
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;
System.out.println(s3 == s4); // false
}
}
通过虚拟机指令明白:
行号为9的指令是个new,就说明s4的创建方式和s1、s2、s3不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。通过后面的注释明白new了一个StringBuilder对象。接着看17,21,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。最后看24,调用了toString方法生成了新的字符串对象。
所以:s1+s2的执行流程:new StringBuilder().append(“a”).append(“b”).toString()
StringBuilder.toString(),创建一个新的String对象,存入ab
说明:当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。
最后输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。
示例三:字符串常量拼接
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b";
System.out.println(s3 == s5);//true
}
}
行号为6时,串池中没有ab对象,就创建ab对象放入串池,执行到29行时要去常量池中找4号位置的ab符号,发现串池中已存在,就不会创建新的对象,直接引用即可。
为什么会以这种方式创建s5,其实是编译期的优化。y因为编译器发现这是两个常量a,b相加,结果在编译期间就可以确定为ab,不会再变。s4 = s1+s2,因为s1和s2是变量,在运行时被引用的值可能被修改,结果不能确定,所以要在运行时使用StringBuilder动态拼接。
示例三:intern方法
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
System.out.println( s2 == x);//true
System.out.println( s == x );//false
}
通过虚拟机指令明白:
第一行代码看行号为0的指令,往串池中添加ab字符,
第二行代码从行号为3的指令new StringBuilder对象,调用他的空参构造器,行号10new String对象,行号14将a字符添加到串池中,实例化String对象,值为a,调用StringBilder的append方法,再new一个String对象,实例化值为b,StringBuilder拼接ab,调用toString方法。
intern方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”对象并返回,并且会让s指向串池中的“ab”对象。
在jdk1.6,当一个String调用intern方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回串池中刚加入的对象。
StringTable特性
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,可以避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(jdk1.8)
字符串常量拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
StringTable优化
- -XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
- 如果有大量字符串且有重复。使用intern方法,将字符串入池减少字符串个数,节约堆内存的使用
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line.intern());
//address.add(line);
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
未入池之前内存对比:
入池之后内存对比:
直接内存
直接内存:Direct Memory,不属于Java虚拟机的内存管理,属于操作系统的内存。
- 常见于 NIO 操作时,用于数据读取时缓冲区内存,
- 分配回收成本较高,但读写性能高(Java使用系统内存,分配成本高些)
- 不受 JVM 内存回收管理()
IO/DirectBuffer拷贝文件
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "E:\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io使用传统阻塞IO文件拷贝 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 使用缓冲区 用时:479.295165 702.291454 562.56592
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
Java并不具备磁盘读写的能力,要调用操作系统的提供的方法。涉及到CPU要从用户态,切换到内核态。
切换到内存态后,就可以由CPU的函数读取磁盘文件的内容,读取数据后内核态会在操作系统内存中画出一块缓冲区,称为系统缓存区,磁盘内容会先读入到系统缓冲区中,系统缓冲区Java代码是不能运行的,Java会在堆内存中分配一块Java的缓冲区,对应Java中new byte[_1Mb],Java代码要能读取到流中的数据,必须要从系统缓存区中的数据间接的读入到Java缓冲区,再到用户态,Java调用输出流的写入操作,重复读写操作,将文件赋值到目标位置。
因为Java堆内存中有块缓冲区,系统内存中由块系统缓存区,数据必然要存两份,造成不必要的数据复制,效率不是很高。
使用ByteBuffer之后,allocateDirect方法会分配一块直接内存,会在操作系统中画出一块缓冲区,Java代码和系统都可以直接访问,磁盘文件读取会读取到直接内存,Java代码也可以直接访问直接内存,减少了一次复制操作。
直接内存溢出
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}
显式的垃圾回收gc对内存占用的影响:
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
/*
* -XX:+DisableExplicitGC 禁用掉显式的垃圾回收之后,直接内存不能通过显示代码回收掉的话,只能等到真正的垃圾回收发生时才会被清理,对应的直接内存才会被清理。
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,不仅要回收新生代,还会回收老年代,导致程序暂定的时间较长 Full GC
System.in.read();
}
}
分配完毕。。。
gc垃圾回收后。。。
直接内存分配的底层原理:Unsafe
ByteBuffer底层分配和释放直接内存的类型为Unsafe类,jdk内部使用Unsafe类,不建议程序员使用。
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存,返回base代表刚分配的内存地址
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存,通过内存地址释放
unsafe.freeMemory(base);
System.in.read();
}
//通过反射获取Unsafe对象
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
分配和回收原理
使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用类型)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存。
垃圾回收对Java中无用的对象是自动释放的,不需手动调用方法,直接内存必须主动调用freeMemory()方法,才能完成对直接内存的释放。
JVM参数
-XX:+PrintStringTableStatistics 打印字符串表的统计信息,可以精准看到串池中实例个数和占用大小信息
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收信息,次数已经花费时间
-XX:StringTableSize=1009 调整StringTable桶个数,最小为1009,如果系统内字符串较多,可以适当增大该值
-XX:+DisableExplicitGC 禁用掉显式的垃圾回收
在jdk8下设置-XX:MaxMetaspaceSize=8m 设置元空间初始大小
在jdk6下设置 -XX:MaxPermSize=10m 设置永久代初始大小
-Xmn10M 设置年轻代大小。整个堆大小=年轻代大小 + 年老代大小 + 常量池。
-Xsx500m
-Xms500m 初始堆内存大小,设定程序启动时占用内存大小
-Xmx500m 最大堆内存,设定程序运行期间最大可占用的内存大小
-Xss256k 设置单个线程栈大小,一般默认512~1024kb,设置栈帧大小
-XX:+UseSerialGC
JVM参数参考:https://blog.csdn.net/dsydly/article/details/106303058