JVM = 类加载器(classloader) + 运行时数据区域(runtime data area) + 执行引擎(execution engine)
类加载器:通过过全限定名加载二进制数据class文件到Jvm内存中,具体原理见类加载文章。
Jvm虚拟机在Java程序执行时,会把Jvm内存划分为若干不同区域。这些区域各有用途,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
JDK1.8 中,永久代(方法区)已完全被元数据区(Meatspace)所取代。
直接内存/本地内存(Direct Memory):并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,属于堆外内存。这部分内存也被频繁地使用,如 NIO 类,当本地内存不足时也会导致OutOfMemoryError 异常出现。
为什么JDK1.8要移除永久代 ,使用metaspace代替?
官方说明:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代。
Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的 时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中
实际应用:
即Java内存模型(Java Memory Model),
对象是存储在堆中,线程栈(虚拟机栈)中存储的是对象的引用,对象不也是在堆中?
当线程运行到引用对象数据时会根据局部表中的对象引用地址去找到主存中的真实对象,然后将对象拷贝到自己的工作内存再操作,当所操作的对象是一个大对象时(1MB+)并不会完全拷贝,而是将自己操作和需要的那部分成员拷贝到自己的线程栈中。
JMM与JVM内存区域的划分区别?
JVM内存(JVM运行时数据区)分为五大区域:
存放当前线程执行到的字节码的行号。
为什么需要程序计数器?为什么程序计数器为线程私有呢?
对于一个处理器,是通过不断切换CPU来实现同时运行的效果。在一个确定的时刻都只会执行一个线程中的指令,一个线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
Java方法执行和调用的内存模型,存储Java方法执行的相关数据。
栈帧的大小是什么时候确定的?
编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。所以调用一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,不会受到运行期数据的影响。
Java虚拟机栈可能出现两种类型的异常:
虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法(C/C++语言)服务。本地方法栈中栈帧和Java虚拟机的栈帧、以及栈的指针是不一样的,所以单独开辟一块空间,供本地方法使用。
用来存放对象实例,几乎所有的对象实例都在这里分配内存
Java虚堆可能出现的异常:
为什么堆并不是存储着全部对象实例?
虽然JVM虚拟机规范对堆的描述是:所有对象实例及数组都要在堆上分配内存。但随着JIT编译器的发展和逃逸分析技术的成熟,所以并不全是这样。
用于存放已被加载的类信息(名称、修饰符、类中的Field信息、类中的方法信息等)、常量池(类中定义为final类型)、静态变量(类中定义的static类型)、即时编译器编译后的代码等数据。
运行时常量池(Runtime Constant Pool)
用于存储编译期Class文件就生成的字面常量、符号引用、翻译出来的直接引用,会在类加载后被放入这个区域;除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern(),这部分常量也会被放入运行时常量池。
// 可以通过javap -v 类名查看类文件的常量池
public class ShowJOL {
// a为常量,在javac编译时就将常量
public static final int a = 3;
public static int b = 4;
public static void main(String[] args) {}
}
// javap -v showJOL
Classfile /Users/shaotuo/java/java/target/classes/edward/com/ShowJOL.class
Last modified 2022-3-16; size 507 bytes
MD5 checksum 127d5263043487cdb8f7d956d5eef3f3
Compiled from "ShowJOL.java"
public class edward.com.ShowJOL
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 常量池部分
Constant pool:
#1 = Methodref #4.#24 // java/lang/Object."":()V
#2 = Fieldref #3.#25 // edward/com/ShowJOL.b:I
#3 = Class #26 // edward/com/ShowJOL
#4 = Class #27 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 ConstantValue
// 常量a的值,直接放入常量池
#8 = Integer 3
#9 = Utf8 b
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Ledward/com/ShowJOL;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8
#22 = Utf8 SourceFile
#23 = Utf8 ShowJOL.java
#24 = NameAndType #10:#11 // "":()V
#25 = NameAndType #9:#6 // b:I
#26 = Utf8 edward/com/ShowJOL
#27 = Utf8 java/lang/Object
{
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 3
public static int b;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public edward.com.ShowJOL();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ledward/com/ShowJOL;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
// 类的构造器,clinit<>初始化函数,也不会将常量a的赋值代码放入Code,不会参与类的初始化
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_4
1: putstatic #2 // Field b:I
4: return
LineNumberTable:
line 6: 0
}
SourceFile: "ShowJOL.java"
方法区的各版本变动:
测试JDK1.8中的方法区中的字符串常量是在什么内存中?
在JDK1.7前,字符串常量是存在永久代中;但JDK1.7开始,字符串常量和类引用被移动到 Java Heap中。所以可用通过测试常量池的内存溢出来判断,是堆溢出还是永久代溢出。
// String类下面的原生方法
// 作用是如果字符串常量池已经包含一个等于这个String对象的字符串,则返回代表池中的这个字符串的String对象
public native String intern();
// 测试程序
public class MethodAreaTest {
public static void main(String[] args) {
// list数组为了保持着引用 防止full gc回收常量池的String对象
List list = new ArrayList();
int i = 0;
while(true){
System.out.println(i);
// String.valueOf(),将i字符串化,切会放入常量池中
list.add(String.valueOf(i++).intern());
}
}
}
// 运行main方法设置相关JVM内存参数
// 设置Java堆的大小为1m,不可扩容
// UseGCOverheadLimit关闭GC
-Xmx1m -Xms1m -XX:-UseGCOverheadLimit
执行结果为Java Heap OOM异常:
JMM内存占用图:
新生代与老年代默认比例为Young:Old = 1 : 2
新生代中默认Eden : from : to = 8 : 1 : 1
相关JVM参数配置:
-Xmx3550m -Xms3550m -Xmn2g -Xss128k
// 设置JVM最大可用内存为3550M
// 设置JVM最小内存也为3550M,-Xms与-Xmx相同,保证JVM堆内存不可变,可避免每次垃圾回收完成后JVM重新分配内存
// 设置年轻代大小为2G
// JDK1.7之前 堆大小=年轻代大小+年老代大小+永久代大小,永久代一般固定大小为64m,
// JDK1.7之后,永久代完全被元空间(Meatspace)所取代,放在本地内存中
// 设置每个线程的堆栈大小128K。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K;
// 操作系统对一个进程内的线程数是有限制的,在3000~5000 左右
内存泄漏:分配出去的内存回收不了;
内存溢出:指系统内存不够用了;
可以分为:内存泄漏和内存溢出,这两种情况都会抛出OutOfMemoryError:java heap space异常。
public class OOMTest {
public static void main(String[] args) {
List list = new ArrayList<>();
while (true) {
list.add(UUID.randomUUID());
}
}
}
// -Xms200m -Xmx200m -Xmn200m
public class OOMTest {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(new byte[1024*1024*1000]);
}
}
要解决这个异常,一般先通过内存映像分析工具对堆转储快照分析,确定内存的对象是否是必要的,即判断是 内存泄露 还是 内存溢出。如果是内存泄露,可以进一步通过工具查看泄露对象到GC Roots的引用链,比较准确地定位出泄露代码的位置。如果是内存溢出,可以调大虚拟机堆参数,或者从代码上检查是否存在某些对象生命周期过长的情况。
虚拟机栈(VM Stack)存放主要是栈帧( 局部变量表、操作数栈、动态链接、方法出口信息 ),无论虚拟机栈还是本地方法栈都为线程私有,所以栈溢出和线程内存大小相关,其中有两类:
SOF(StackOverflowError异常)
线程内方法调用层次太深,内存不够新建栈帧。抛出java.lang.StackOverflowError错误。一般由于方法的嵌套调用层次太多,比如递归调用,使得虚拟机栈中不断创建栈帧导致,线程栈中的所有栈帧的大小的总和大于-Xss设置的值,从而产生StackOverflowError溢出异常。
public class SOFTest {
public static void method() {
method();
}
public static void main(String[] args) {
method();
}
}
OOM
当Java 程序启动一个新线程时,若没有足够的空间为该线程分配Java栈(一个线程Java栈的大小由-Xss设置决定),JVM将抛出OutOfMemoryError异常。
public class OOMTest {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
try {
Thread.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}