通过线程私有/共享分类进行描述;
包括
作用:一小块内存区域,每个线程都有一个,代表当前线程所执行到的字节码的行号指示器;
值:行号,如果当前是 native
方法,则为 undefined
,未规定 OutOfMemoryError
;
作用:存储线程执行代码过程中数据的一块内存区域,描述方法执行的内存模型,由一组栈帧组成;
配置参数:
-Xss // 如 -Xss1m
JDK11后,64位机器,栈大小windows不能低于180K,linux下不能低于228K。
每个方法被执行时创建一个栈帧(存储局部变量表,操作栈,动态链接,方法出口),每个方法的调用到执行完成,对应一个栈帧在虚拟机栈中从入栈到出栈的过程;
可能产生 StackOverflowError
、OutOfMemoryError
;
一组变量存储空间,变量槽Slot,每个占用一字节/32位(取决于虚拟机实现),故一个double long
等类型变量占用两个槽位;
作用:存放方法参数和方法内部定义的局部变量;
特点:进入方法之前,这个方法需要多大的局部变量空间完全确定,方法运行期间不会改变局部变量表的大小,通过索引访问;
作用:虚拟机的工作区,类似局部变量表,标准的入栈出栈操作来进行计算;
特点:byte、short
等小于1字节的数据进入操作数栈前会被转换为 int
类型
概念:符号引用在运行时转换为直接引用的过程即动态链接,相对概念静态解析;
符号引用:即 .class
文件的常量池中的方法名/成员变量名;
直接引用:可以调用的引用,内存地址;
静态解析:类加载时或第一次使用时将符号引用转换为直接应用;
值:方法调用者的程序计数器值 / 异常处理表;
特点:方法退出时,如果有返回值,把返回值压入调用者栈帧的操作数栈,PC计数器的值调整为调用入口下一指令;
类似 Java虚拟机栈 运行 Java字节码,本地方法栈 服务于虚拟机使用到的 native
方法;
某些虚拟机将两者合二为一;
包括:
存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
永久代是 HotSpot 虚拟机在 JDK7 及以前的方法区实现,JDK8后,已使用元空间Meta-Space存储相关的数据。
运行时常量池是方法区的一部分,Class文件的常量池表,存放着编译期生成的各种字面量与符号引用,在类加载后存放到方法区的运行时常量池中,一般符号引用翻译出的直接引用也存储在这里。
字符串常量池在JDK6及之前,在方法区/永久代中,之后及JDK7及之后,被放在了堆中,String::intern()
将返回堆中首次出现的字符串引用:
String str1 = new StringBuilder("方法").append("区").toString();
str1.intern() == str1; // jdk6及之前:false, jdk7及之后,true(前提:之前没将“方法区”三个字放在常量池中);
String str2 = new StringBuilder("ja").append("va").toString();
str2.intern() == str2; // jdk6及之前:false, jdk7及之后,false,因为之前已将“java”放在常量池中,所以前者返回的不是str2在堆中的引用,而是java第一次出现时在堆中的引用);
String str3 = new String("堆"); // 将先在常量池中创建,再在堆中创建另一个
str2.intern() == str2; // false ,前者返回的是因为字面量创建在常量池中的;
存储的数据:
类型信息、字段信息、方法信息;
常量外的类静态变量对象;
一个指向 ClassLoader
(堆中)的指针;
一个指向 Class
对象(堆中)的指针;
常量池:
字面量:常量字面量、基本类型值(静态成员在常量池,成员在堆,局部在栈);
符号引用、直接引用;
特点:
JVM
参数可以调整大小;HotSpot
虚拟机中GC
分代扩展至方法区,用永久代实现方法区;OutOfMemoryError
配置参数:
-XX:MaxMetaspaceSize // 方法区最大内存
-XX:MetaspaceSize // 方法区内存
-XX:MinMetaspaceFreeRatio //在垃圾收集之后控制最小的元空间剩余容量的百分比
不能通过字符串常量池验证,可通过cglib动态加载足够数量的类验证,可通过大的静态变量对象验证。
线程共享的一块内存区域,存放对象实例,可抛出 OutOfMemoryError
;
垃圾回收管理的主要区域,分代收集算法分为新生代和老年代,默认分配比例:新生代:老年代 = 1:2;
程序新创建的对象从新生代分配内存;有三块空间组成,Eden Space
和两块相同的survivor
空间组成,后者分别为 from survivor
和 to survivor
,默认分配比例为:8:1:1。
JVM每次只会使用Eden
和其中一块Survivor
空间为对象服务,另一块Survivor
空闲,故新生代的可用内存空间为90%;
存放经过多次新生代GC
仍存活的对象,如缓存对象,新建对象直接进入老年代的情况有:
运行时常量池中找到符号引用
-> 检查类是否加载、链接、初始化
-> 分配内存 (指针碰撞、空闲列表、本地线程分配缓冲)
-> 对象实例字段初始化为0值
-> 对象头信息设置(类型信息、元数据地址、hashCode、GC分代年龄)
-> 执行new后声明的构造方法
不是JVM
运行时数据区的一部分;
1.4之后,NIO
,一种基于通道Channel
和缓冲器ByteBuffer
的IO
方式,可以使用Native
函数直接分配,然后通过一个存储在Java
堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作,避免了Java
堆和Native
堆来回复制数据;– TODO
因为不受Java
堆内存大小限制,但是受物理内存大小限制,因此设置堆内存-Xmx
时需要考虑。
配置参数:
-XX:MaxDirectMemorySize // 若不指定,默认与堆内存最大值一致
–TODO,绘制图
从对象存活判断、垃圾收集算法、垃圾收集器、内存分配与垃圾回收策略几个方面整理。
包括:
在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不需要再存活的。
JVM
并不使用引用计数法来管理内存,单纯的引用计数
通过一系列GC Roots
的根对象作为起始节点,根据引用关系向下搜索,搜索走过的路径成为引用链,与引用链不相连的对象即是可回收的;
可作为GC Roots的对象:
NullPointExcepiton
、OutOfMemoryError
)等,还有系统类加载器。引用涉及强引用、软引用、弱引用、虚引用。-- TODO
包括:
适用与分代有关,故先介绍分代收集理论。
建立在两个分代假说上:
困难:跨代引用,导致仅回收新生代区域,却需要扫描全部老年代以确定是否可以进行回收。
解决:新生代上建立一个全局的数据结构,记忆集,把老年代划分为若干个小块,标识出老年代的那一块内存会存在跨代引用,此后发生 Minor GC 时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
分代收集理论决定了后面产生的"Minor GC"、“Major GC”、"Full GC"等,进而产生了”标记-复制算法“、”标记-清除算法“、”标记-整理算法“等。
根据可达性分析算法将存活对象标记,然后将未标记的对象清理掉。
优点:
问题:
故适用于回收对象较少时,在一些要求低延迟的垃圾收集器的老年代收集中组合标记-整理算法使用。
根据可达性分析算法将存活对象标记,然后标记的对象复制到另一块干净内存中去,将原内存块一次清除。
优点:
问题:
根据弱分代假说,新生代对象大多很快死亡,这个收集算法适用于新生代的垃圾收集。Survivor区用于收集过程中存储存活对象,默认占新生代的1/10,也就是90%的新生代内存可用于分配。同时,需要有逃生门,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代进行分配担保。
标记过程与标记清除一样,后续步骤直接让所有存活对象向内存空间一端移动,直接清理掉边界以外的内存。
优点:
问题:
适用于要求高吞吐量的垃圾收集器老年代的收集策略,CMS收集器将其和标记-清除算法组合使用,当后者导致垃圾碎片多到影响大对象分配时,使用前者进行收集。
– TODO
– TODO
主要包括以下几个策略:
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。
配置参数测试:
-Xms20m -Xmx20m // 设置堆大小20M
-Xmn10m // 设置新生代10m
-XX:SurvivorRatio=8 // 设置eden:from survivor: to survivor = 8:1:1
-XX:+PrintGCDetails // 打印GC日志
public class TestGC {
private static int BYTE_NUM_1M = 1024 * 1024;
public static void main(String[] args) {
byte[] a = new byte[BYTE_NUM_1M * 2],
b = new byte[BYTE_NUM_1M * 2],
c = new byte[BYTE_NUM_1M * 2],
d = new byte[BYTE_NUM_1M * 4];
}
}
以上代码正常情况下,应该能实现一次Minor GC,能够看到新生代GC后存储着数组d
,而a、b、c
被移动到年老代。
但是我用JDK11,使用的是G1垃圾回收器,从GC日志中已经不能直接看出上述表现了,这里留在以后看了调优工具后再做更新。–TODO
大对象指需要大量连续空间的对象,如很长的字符串,元素数量庞大的数组,复制开销大,写程序时应该尽量避免分配短命大对象。可以设置JVM参数使占用内存大于设置值的对象直接进入老年代,以避免在新生代间频繁复制。
-XX:PretenureSizeThreshold=3145728 // 这里不能直接写3M
/**
* VM 参数:-Xmn10m -Xms20m -Xmx20m -XX:PretenureSizeThreshold=4194304 -XX:+PrintGCDetails
* 4M=4194304
*/
public class TestPretenure {
private static int BYTE_NUM_1M = 1024 * 1024;
public static void main(String[] args) {
byte[] a = new byte[5 * BYTE_NUM_1M];
}
}
同5.1,这里使用JDK11,不能看出效果,显示5M
的数组全部分配在young region
。–TODO
虚拟机给每个对象定义了一个年龄计数器(Age),存储在对象头中。经过一次Minor GC后,Age + 1,当年龄增加到一定程度(默认15),会被晋升到年老代。
-XX:MaxTenuringThreshold // 自定义进入老年代的年代数
为了更好适应不同程序的内存状况,对象不一定年龄大于MaxTenuringThreshold
才可以进入老年代,如果在Survivor
区相同年龄对象占用内存之和大于Survivor
空间一半,年龄大于或等于该年龄的的对象即可进入老年代。
发生Minor GC前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果是,则此次Minor GC安全,不然会检查-XX:HandlePromotionFailure
参数,如果允许且老年代剩余连续空间大于历次晋升到老年代的新生代对象大小,则可以冒险执行Minor GC,否则要改进为进行一次Full GC。
JDK6 Update24之后,只要老年代剩余连续空间大于新生代对象总大小或历次晋升的平均大小,就会进行Minor GC,否则进行Full GC。不再使用上面那个参数。
文章中仍有许多需要完善的地方,后续继续读书和看博客会继续更新。
欢迎批评指正。
参考:
《深入理解Java虚拟机 第三版》
https://www.jianshu.com/p/0ecf020614cb JVM运行时内存
https://www.cnblogs.com/warehouse/p/9466137.html 内存、GC、调优工具
https://www.cnblogs.com/anyehome/p/9071619.html JVM调优参数、经验规则