0
点赞
收藏
分享

微信扫一扫

JVM的内存结构


JVM入门

jvm基础

什么是jvm

定义:Java Virtual Machine -java 程序的运行环境(java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行的基石
  • 自动内存管理机制,垃圾回收
  • 数组下标越界检查
  • 多态

内存结构

程序计数器 PC Register

定义

Program Counter Register 程序计数器(寄存器)

  • 在程序执行过程中,记录下一次jvm指令的执行地址
  • 在代码运行过程中,java代码会被编译成有序的jvm指令,再由解释器解释成程序能够识别的二进制指令
  • 在上一条指令执行完成后,程序会从程序计数器中获取下一条指令的地址
  • 特点:
  • 线程私有
  • 不会存在内存溢出的区

虚拟机栈

  • 先进后出结构
  • 线程运行时需要的内存空间(线程私有)
  • 虚拟机栈由多个栈帧组成(每个方法运行时需要的内存-》参数、局部变量、返回地址)
  • 每个线程只能有一个活动栈帧,对应着正在执行的那个方法

public static void main(String[] args) {
method1();
}

private static void method1() {

method2(1,2);
}

private static int method2(int a ,int b) {

int c = a + b;

return c;
}

备注:debugger运行,可通过Frames变化查看当前线程对应虚拟机栈的内存变化,入栈操作为main-》method1-》method2,实际的执行顺序为反序

问题:

  1. 垃圾回收是否涉及栈内存?
    不会:栈内存在每次方法执行结束后都会被出栈,释放内存,所以不需要垃圾回收
  2. 栈内存分配越大越好吗?
    栈内存的默认大小都是1024kb(mac,linux…)
    在windows环境下的默认大小是根据内存大小进行分配的
    在相同内存大小下,如果栈分配的内存大小不一致,栈内存大的程序所能调用的线程数会比栈内存小的程序少
  3. 方法内的局部变量是否线程安全?
    线程安全,方法内的局部变量对每个线程都会分配一份单独的内存空间,每个栈内的内存空间互不影响,如果变量是共享的,会存在线程安全问题。
    变量是否线程安全:
  • 是否是方法内的局部变量
  • 局部变量是否逃离方法的作用范围(参数或返回)
  • 如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
  1. 栈内存溢出
  • 栈内栈帧过多(方法的递归调用)
  • 栈帧过大(一个栈帧大小就超过了栈内存大小)

private static int count;

public static void main(String[] args) {

try {
method3();
} catch (Exception e) {
e.printStackTrace();
} finally {

System.out.println(count);
}

}

private static void method3() throws Exception {
count++;
method3();
}

默认栈内存大小下,递归发生了23522次

修改VM option栈内存大小参数 Xss256k 后:-递归次数变为3558

java.lang.StackOverflowError

  1. 线程运行诊断
  • 案例1:cpu占用过多
    定位:
    top命令查看当前系统中进程占用资源情况
    ps H(线程中的进程数) -eo(显示的字段) pid,tid,%cpu -展示进程中线程的属性展示
    jstack 进程id 查看进程中的线程信息
    根据进程id找到有问题的线程,进一步定位到问题代码的源代码行数
  • 案例2:程序运行很长时间没有结果
    jstack 进程id 查看进程中的线程信息
    找到线程死锁代码
    本地方法栈-Native method stack
    给本地方法的运用提供内存空间(java代码存在局限,无法直接和操作系统进行交互,需要通过C等代码编写的native方法 和操作系统形成交互)

堆-Heap

通过new关键字,创建对象都会使用堆内存

  • 特点:
  • 线程共享,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制
  • 堆内存溢出

堆内的对象都存在引用,且一直存在对象的新增

public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
Java.lang.OutOfMemoryError: Java heap space

-Xmx8m:设置堆内存大小

  • 堆内存诊断
    jps工具
    查看当前系统中有那些java进程

60064 RemoteJdbcServer
7424 RemoteMavenServer
10728
1544 Test3
33400 ApiApplication
53148 Launcher
6940 Jps

jmap工具

查看堆内存占用情况:jmap -heap 进程id

  • 堆配置

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4261412864 (4064.0MB)#最大内存配置
NewSize = 88604672 (84.5MB)#新生代内存
MaxNewSize = 1420296192 (1354.5MB)#最大新生代内存
OldSize = 177733632 (169.5MB)#老年代内存
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB #元空间大小
G1HeapRegionSize = 0 (0.0MB)

  • Eden区内存使用情况

Eden Space:
capacity = 66584576 (63.5MB)
used = 1331712 (1.27001953125MB)
free = 65252864 (62.22998046875MB)
2.0000307578740157% used

  • jconsole工具
    图形界面的,多功能的监测工具,可以连续监测:jconsole
  • 案例:垃圾回收后,内存依旧占用很高
    jvisualvm 可视化工具工具
    查看jvm参数等:

方法区

所有java虚拟机进程共享的区域

  • 存储类结构相关信息:成员变量、方法数据、成员方法、构造方法、特殊方法
  • 运行时常量池
  • 在虚拟机启动时被创建
  • 逻辑上是堆的存储部分
  • 方法区内存溢出

hotspot 在jdk1.8之前在堆空间中划分永久代保存方法区内的数据

1.8以后,永久代被替代为元空间,且使用系统物理内存作为数据存储

一般不会出现内存溢出问题,需要修改相关jvm参数

-XX:MaxMetaspaceSize=8m:修改元空间最大内存为8m

public static void main(String[] args) {
int j = 0;
try {
Test4 test4 = new Test4();
for (int i = 0; i < 10000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter wa = new ClassWriter(0);
//版本号,public,类名,包名,父类,接口
wa.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回byte
byte[] code = wa.toByteArray();
//执行了类的加载
test4.defineClass("Class"+i,code,0,code.length);
}

} finally {
System.out.println(j);
}
}

结果:

Error occurred during initialization of VM
MaxMetaspaceSize is too small.

场景:

  • spring
  • mybatis

运行时常量池

  1. 静态常量池:.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念;
  2. 运行时常量池:则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池,除此外,运行期间也可以将新的常量放入池中,如String类的intern()方法,会在常量池查找是否存在一份equal相等的字符串,如果有则返回该字符串的引用,否则自己添加字符串进入常量池。

优点:

  • 实现了对象的共享,避免了频繁的创建和销毁对象而影响系统性能
  • 节省内存空间,如字符串常量池
  • 合并相同字符串只占用一个空间
  • 节省运行时间,进行比较时==比equals快,只判断引用是否相等就可以判断实际值是否相等
    如:

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;

System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true

s1 == s2 作为直接赋值,使用的字符串字面量,在编译时会直接加载到class文件的常量池中,内容被合并且指向一致

s1==s3 s3对象为拼接对象,但是作为拼接的两个字符串也是字面量,在编译时会被优化为s1 = “Hello”,等同于s1和s2

s1==s4 s4中存在new的新对象,其地址为堆中地址,与常量池地址不一致,需要在等到运行时才会知道地址

s1==s9 s9为拼接对象,虽然s7,s8中使用的是字符串字面量,但是拼接s9时,s7,s8作为两个变量,地址不可预料,不能在编译时确定,所有不存在优化

s4==s5 两个都是new出来的堆对象,地址不可能一致

s1==s6 s6使用intern()方法在常量池中寻找,如果有直接调用常量池中的地址

特例:static修饰符

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}

结果是一致对象。

原因:static修饰方法在编译时直接定死了AB的值和地址,所以s值也为定值

特例2:静态代码块

public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}

并不是同一个对象

原因:AB为变量,但是没有直接赋值,程序不知道它们何时赋值和赋予什么样的值,那么s值就不能作为一个定时被初始化,只能在运行时创建。

注意:

  • 运行时常量池中的常量基本来源于class文件中的常量池
  • 程序运行时,除非手动向常量池中添加常量(intern方法),否则jvm不会自动添加常量到常量池中

基本数据类型和其包装类中除了Float和Double外都实现了常量池技术,但是数值类型的常量池不能手动添加常量,程序启 动时常量池中的常量就已经确定了.

如整形常量池中的常量范围:-128-127,(Byte,Short,Integer,Long,Character,Boolean)这五种包装类默认创建了数值【-128,127】之间的相应类型缓存数据,超过此范围后都会创建新的对象到堆中。

  • StringTable 串池
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池机制,避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(jdk1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
  • 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

位置:

1.6时,是跟随常量池放在永久代空间中

1.7,1.8后在堆中单独开辟一个空间存放StringTable

原因:

  • 永久代的回收效率不高

直接内存-Direct Memory

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不受jvm内存回收管理
  • 相较于原始的io操作,原始io在进行文件读写时需要先由操作系统划分内存空间,再由java程序划分内存空间,进行内容的复制,最后交由内核进行内容写入

直接内存同样也是系统开辟内存空间,但是这块内存空间java程序可以直接访问,减少了java自己开辟空间以及进行内容拷贝的时间

  • 内存溢出

static int defaultMember = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(defaultMember);
list.add(byteBuffer);
i ++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

-XX:+DisableExplicitGC:禁用显示的垃圾回收

System.gc:显示的垃圾回收,FUll GC,导致程序出现较长时间的停滞


举报

相关推荐

jvm的内存结构

JVM内存结构

JVM 内存结构

jvm内存结构

java JVM - jvm内存结构

JVM详解【三】JVM的内存结构

JVM-JVM内存结构(二)

JVM----内存结构

JVM之内存结构

0 条评论