对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++ 程序开发程序员这样为每一个操作去写对应的 delete / free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序把内new存控制权利交给JVM虚拟机。一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
JVM 虚拟机在执行 java 程序的过程中,会把它管理的内存划分成若干个不同的区域,每个区域有各自的不同的用途、创建方式及管理方式。有些区域随着虚拟机的启动一直存在,有些区域则随着用户线程的启动和结束而建立和销毁,这些共同组成了 Java 虚拟机的运行时数据区域,也被称为 JVM 内存模型
JVM虚拟机在执行JAVA程序的过程中会把它管理的内存划分成若干个不同的数据区域,由方法区,堆区,虚拟机栈,本地方法栈,程序计数器五部分组成
版本的差异
其中虚拟机栈、本地方法栈、程序计数器是线程私有的区域,所以随着线程消亡而结束。而线程共享的 Heap 堆区、MetaSpace 元空间会随着虚拟机的启动,一直存在。
Program Counter Register‘
程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器
字节码解释器在解释执行字节码文件工作时,每当需要执行一条字节码指令时,就通过改变程序计数器的值来完成.程序中的分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
程序执行过程中,会不断的切换当前执行线程,切换后,为了能让当前线程恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且各线程之间计数器互不影响,独立存储
程序计数器主要作用
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它随着现成的创建而创建,随着线程的结束而死亡
VM Stack
虚拟机栈是线程执行Java程序时,处理Java方法中内容的区域,虚拟机栈也是线程私有的区域,每个Java方法被调用的时候,都会在虚拟机栈中创建一个栈顶,而每个栈帧又由局部变量,操作数栈,动态链接和方法返回四部分组成,有些虚拟机的栈帧还包括了一些附加的信息
JMM内存区域可以粗略的氛围堆内存(Heqp)和栈内存(Stack)其中栈就是VM Stack虚拟机栈,或者说是虚拟机栈中局部变量表部分
局部变量表主要存放了编译器可知的各种基本数据类型变量值(boolean、byte、char、short、int、float、long、double),对象应用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
虚拟机栈运行原理
每一次方法调用都会有一个对应的栈帧被压入VM Stack 虚拟机栈,每一个方法调用结束后,代表该方法的栈帧会从VM Stack虚拟机栈中弹出
虚拟机栈是内存的私有区域,并且栈帧不允许被其他线程访问,不存在线程安全问题,栈帧弹出后内存就会被系统回收,所以也不存在垃圾回收问题
在活动线程中,只有位于栈顶的栈才是有效的,成为当前活动栈帧,代表正在执行的当前方法
在JVM执行引擎运行时,所有指令都只能对当前活动栈帧进行操作。虚拟机栈通过pop和push的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上
活动栈帧被弹出的方式:
java方法有两种返回方式,不管哪种返回方式都会导致当前活动栈帧被弹出
虚拟机栈可能产生的错误
java虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError
虚拟机栈的大小
虚拟机栈的大小可以通过-Xss
参数设置,默认单位是byte,也可以使用k,m,g作为单位(不区分大小写)。例如:-Xss 1m
在不同的操作系统下-Xss
的默认值不同
Native Method Stack
native
关键字修饰的本地方法被执行的时候,在本地方法栈中会创建一个栈帧,用于存放该native
本地方法的局部变量表、操作数栈、动态链接、方法出口信息。方法执行完毕后,相应的栈帧也会出栈且释放内存空间。也会出现StackOverFlowError和OutOfMemoryError两种错误
Heap 堆区,用于存放对象实例和数组的内存区域
Heap 堆区,是 JVM 所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。
每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。
每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上
不连续的内存,但必须是逻辑上连续的内存。
JVM 规范中描述,所有的对象实例及数组都应该在运行时分配在堆上。而他们的引用会被保存在
虚拟机栈中,当方法结束,这些实例不会被立即清除,而是等待 GC 垃圾回收。
由于堆占用内存大,所以是 GC 垃圾回收的重点区域,因此堆区也被称作 GC堆
(Garbage Collected Heap)
对象逃逸分析
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了
从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用 (也就是未逃逸出去),那么对象可以直接在栈上分配内存
从垃圾回收的角度,由于现在收集器基本都采用粉黛垃圾收集算法,所以JVM中的堆区往往进行粉黛划分,例如新生代和老年代。目的是为了更好地回收内存,或者更快地分配内存
堆区的组成分为新生代(Young Generation)老年代(Old Generation)。
新生代被分为伊甸区(Eden)和幸存者区(from + to),幸存区又被分为
Survivor 0(from)和Survivor 1(to)
新生代和老年代的比例为1:2,伊甸区和S0、S1比例为8:1:1,不用区域存放对象的用途和方式不同
堆区的内存大小是可以修改的,默认轻快下,初始堆内为物理内存的1/64,最大的物理内存的1/4
-Xms64m
-Xmx64m
-Xmx32m
Heap堆区中的新生代、老年代的空间分配比例,可以通过java -XX:+PrintFlagsFinal -version
命令查看
uintx InitialSurvivorRatio = 8
新生代Young(Eden/Survivor)空间的初始比例 = 8:代表Eden占新生代空间的80%
uintx NewRatio = 2
老年代Old/新生代Young的空间比例 = 2:代表老年代Old是新生代Young的2倍
因为新生代是由Eden+s0+s1组成的,所以按照上述默认比例,如果Enen区内存大小是40M,那么两个Survivor区就是5M,整个新生代区就是50M,然后可以算出老年代Old区内存大小是100M,堆区总大小就是150M
创建一个新对象,在堆内的分配内存
大部分情况下,对象会在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC垃圾回收的时候
依然存活的对象会被移送到Survivor区。Survivor区分为S0和S1两块内存区域。每次YGC的时候,它们将存活的对象复制到未使用的Survivor空间(S0或S1),然后将当前正在使用的空间完全清楚,交换两块空间的使用状态。每次交换时,对象的年龄就会+1
如果YGC要以送的对象大于Survivor区容量的上线,则直接移交给老年代。一个对象也不可能永远呆在新生代,在JVM中一个对象从新生代晋升到老年代的阈值默认值是15,可以在Survivor区交换14次后,晋升至老年代
出于效率的缘故,JVM 的垃圾收集不会对三个区域(伊甸区、幸存区、老年代)进行收集,大部分时候都是回收新生代, HotSpot 虚拟机将垃圾收集分为部分收集( Partial GC )和整堆收集( FulI GC)
部分收集:
**整机收集FGC(Full GC):**回收整个Java堆区,默认堆空间使用带到80%(可调整)的时候会触发FGC。频率根据访问量的多少决定,可能十天也可能一周左右一次(整机收集的频率越少越好)
GC组合垃圾回收只有YGC和FullGC、OldGC不可以单执行。原因是OldGC是STW机制+标记整理算法,相对耗时,只能在关键时刻使用,因此只有FullGC才能出发OldGC
GC垃圾回收的影响
GC耗时太长、GC次数太多会影响进程的性能、导致进程相应变慢、或者无法响应
产生FGC的原因:
堆区最容易出现的就是OutOfMemoryError
错误,这种错误的表现形式有以下两种:
OutOfMemoryError:GC Overhead Limit Exceeded
:当JVM花太多时间执行垃圾回收、并且只能回收很少的堆空间时、就会发生此错误OutOfMemoryError:Java heap space
:如果在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误此种情况,与配置的最大堆内存有关,且受限制于物理内存大小
用于存放类信息、常量、静态常量、JIT即时编译器编译后的机器代码等数据等/例如java.lang.Object类的原喜喜、Integer.MAX_VALUE常量等
JDK1.6:
HotSpot JVM使用Method Area方法去存储,也叫永久代(Permanent Generation)
1.方法去和"永久代(Permanent Generation)"的区别:方法去是JVM的规范。而永久代是JVM规范的一种实现,并且只有HotSpot JVM才有永久代,而对于其他类型的虚拟机,如JRockit(ORacle)、J9(IBM)并没有
2.方法区是一片连续的堆空间,当JVM加载的类信息容量超过了最大可分配空间,虚拟机会抛出OutOfMemoryError:PermGenSpace的Error
3.永久代的GC是和老年代(old generation)捆绑在一起,无论谁满了,都会出发永久代和老年代的垃圾收集
4.可以通过 -XX:PermSize=N 设置方法区(永久代)初始化空间,-XX:MaxPermSize=N 设置方法区(永久代)最大空间,超过这个值将会抛出错误:java.lang.OutOfMemoryError:PermGen
JDK1.7:
1.7是一个过度版本
将字符串常量池、静态变量转移到了堆区
JDK1.8:正式移除永久代,采用Meta Space
元空间代替
元空间的本质和永久代类似,都是对 JVM 规范中方法区的一种具体实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。
java 8 中PermGen永久代被移出HotSpot JVM的原因
1.由于PermGen内存经常会移除,容易抛出java,lang.OutOfMemoryError: PermGen错误
2.移除PermGen可以促进HotSpot JVM 与 JRockit VM 的融合,因为JRockit没有永久代
上述运行结果可以看出,相同的代码,在JDK1.6会出现PermGen Space的永久代内存移除,而从JDK1,7和JDK1.8会出现Java heap space 堆内存移除,并且JDK1.8中PermSize和MaxPermGen参数已经无效。因此,在JDK1.7和JDK1.8中,已经将字符串常量由永久代转移到堆中,并且JDK1.8已经完全移除了永久代,采用元空间来替代
1.-XX:MetaspaceSize参数:主要控制Meta Space GC发生的初始阈值,也就是最小阈值,当使用的Meta Space空间到达MetaspaceSize的时候,就会触发Metaspace的GC
2.-XX:MaxMetaspaceSize参数:最大空间,默认是没有限制的。在jvm启动的时候,并不会分配MaxMetaspaceSize这么大的一块内存出来,metaspace是可以一直扩容的,知道到达MaxMetasoaceSize
String的两种创建方式:
// 先检查字符串常量池中有没有"abc",如果字符串常量池中没有,则创建一个,然后str1指向字符串常量池中的对象,如果有,则直接将str1指向"abc"
String str1 = "abc";
String str2 = new String("abc"); //堆中创建一个新的对象
String str3 = new String("abc"); //堆中创建一个新的对象
System.out.printf(str1 == str2); //false
System.out.printf(str2 == str3); //false
String的intern()方法:
检查指定字符串在常量池中是否存在?如果存在,则返回地址,如果不存在,则在常量池中创建
String s1 = new String("abc");
String s2 = s1.intern(); //查看字符串常量池中是否存在"abc",如果存在则返回地址,如果不存在则在常量池中创建
string s3 = "abc"; // 使用常量池中的已有的字符串常量"abc"
System.out.printf(str2 == str3); //true
String的拼接
String s1 = "str";
String s2 = "ing";
String s3 = "str"+"ing"; // 常量池中的新字符串对象
String s4 = str1 + str2; // 在堆中创建的新字符串对象
String s5 = "string"; // 常量池中的已有字符串对象
System.out.printf(str3 == str4); //false
System.out.printf(str3 == str5); //true
System.out.printf(str4 == str5); //false
String s1 = new String("abc");
这句代码创建了几个字符串对象?
创建1或2个字符串,如果常量池中已存在字符串常量"abc",则只会在堆空间创建一个字符串常量"abc"。如果常量池中没有字符串常量"abc",那么它将首先在池中创建,然后再堆空间中创建,因此将创建总共2个字符串对象