本次讲述jvm分代模型的基础概念,这个专栏会由浅入深的不断构建起来,循序渐进,和上一篇一样,是非常基础的内容
其实这一篇才是对象分配的内容:深入理解JVM虚拟机 - jvm的对象分配策略 会发现里面有很多东西需要消化,而这一篇会讲一些基础的内容。
JVM的基础分代模型:年轻代,老年代,永久代。这里需要注意内存模型通常是和垃圾回收期相辅相成的,现代的垃圾收集器已经十分复杂了,甚至已经没有了分代的概念,我们所讲的新生代老年代是最初设计的一些理念,很多人学到更为先进的垃圾收集器可能会蒙蔽,比如堆内存怎么变成一块一块的了,以前不是说堆内存只有新生代和老年代这两块完整大的空间呢?
下面基础了解一下对象分配的基础概念,这些概念可能在学习JAVA的时候就已经接触过了,所以也都是简单提一下:
下面用一张图解释对象分配的基础概念:
我们以下的代码为例简单讲解对象分配的方式。
public class OneWeek {
private static final Properties properties = new Properties();
public static void main(String[] args) throws IOException {
InputStream resourceAsStream = OneWeek.class.getClassLoader().getResourceAsStream("app.properties");
properties.load(resourceAsStream);
System.out.println("load properties user.name = " + properties.getProperty("user.name"));
}/*运行结果:
load properties user.name = 123
#app.properties:
user.name=123
*/
}
对象分配的细节
首先,当线程开启的时候,首先会加载并且初始化「OneWeek.class」对象,同时将Main()方法压入到虚拟机栈中,同时创建栈帧以及局部变量表等内容。
然后,执行字节码引擎执行字节码里面的指令,根据代码可以看到,方法首先会拿到当前类的class文件,并且调用当前类加载器加载app.properties
这个文件到内存当中,注意在加载的过程中会创建char[]数组存储加载的内容,以及创建文件IO流读取文件等操作,这部分的对象都是「优先分配在新生代」的。
当程序计数器执行到:properties.load(resourceAsStream);
这一行代码对应的字节码指令的时候,会发现Properties.class没有加载,同时又发现他是一个静态对象,所以会把当前的对象引用分配到方法区进行贮存,注意方法区存放的是对象的引用不是对象的实例,「实例依旧优先分配在新生代」。但是这里为什么直接划分到老年代了呢?因为我们知道这个静态常量如果被其他的类引用,那么可以算作是长期存活对象,那么长期存活的对象迟早是要进入到老年代的,所以图中直接划分到老年代了。
同时我们也可以发现如果新生代在垃圾回收之后存在长期存活的对象,会在垃圾回收之后自动晋升到老年代进行存储。
特别要注意我们平时new出来的对象都是「强引用」。哪怕是栈帧局部变量「只被使用过一次」对象的引用随着栈帧回收,也是「不会立马回收」的,而是要等到垃圾回收线程开启之后被回收掉。
下面分别说明一下这四个点是如何来的:
什么是「对象年龄」?对象年龄就是在JVM运行的时候,新生代中的对象只要每躲过一次垃圾回收,内部的引用计数器就会把当前年龄的对象+1,当对象的年龄累加到15之后,该对象在下一次垃圾回收之后就会晋升到老年代。当然此时并不是高枕无忧了,当老年代也被占满的时候如果当前对象已经没有被GC ROOT引用了,也还是会被当做垃圾回收的。
大对象最典型的案例就是「大字符串」或者很大的「字节数组」,因为需要占用 「连续的内存空间」,如果新生代无法容纳,那么毫无疑问是需要老年代作为兜底放到老年代直接存放,至于具体参数后续的文章会一一解释,这里了解基础概念即可。
老年代什么时候会触发垃圾回收的操作?条件毫无疑问是老年代放不下对象了,那么老年代为什么会满的,上一段我们说过老年代的对象都是从新生代来的,所以毫无疑问是新生代来到老年代发现老年代放不下了,所以老年代此时就会进行垃圾回收了,老年代的回收叫做Full GC。
看完上面这些,我们需要考虑的是 「新生代进入老年代」的时机,为什么要考虑这个东西,我们来分析一下:
首先是大对象,大对象进入新生代发现新生代放不下,如果老年代也发现放不下就直接Full GC了?这未免也太悲观了,万一垃圾回收之后放下来了,那不是白白浪费性能,不合适。其次,新生代一定要等到自己满了才进入老年代么,这样未免又太乐观了,因为万一新生代总有一些存活对象活在“等待区”(survior区)又不肯进入老年代,中间赖着不走,那么这一片区域反而失去了他的价值,所以也是不合适的,不如提前进入老年代。
分代的核心参数如下,需要注意的是要注意「区分大小写」,输错会导致参数不生效:
-Xms
:java堆内存的大小-Xmx
:java堆内存的最大大小-Xmn
:java堆当中的新生代大小,扣除新生代剩下就是老年代的内存大小-XX:PermSize
:永久代大小**(JDK8废弃,被替换为:-XX:MetaspaceSize)**-XX:MaxPermSize
:永久代最大大小**(JDK8废弃,被替换为:-XX:MaxMetaspceSize)**-Xss
:每个线程栈内存大小❝-xms和-xmx用来限定java堆的总大小以及扩张的最大大小,但是通常会设置为「一样的参数」,因为扩容需要
stop world
极大的影响系统性能。 -Xmn:是「新生代」的空间大小,老年代会自动根据「总的堆大小 - 新生代」大小算出来。 -xx:permsize和-xx:maxpermsize。会限制永久代大小和最大的大小,通常情况下设置为256M够用 - Xss 参数限制 每一个线程的「栈内存」大小。其实就是每一个线程对应虚拟机栈的大小,注意这区域不能太大,当然也不能太小。 ❞
-XX:MetaspaceSize
以及 -XX:MaxMetaspceSize
。在IDEA当中的启动参数设置如下:
-Xms1024m
-Xmx2048m
-XX:ReservedCodeCacheSize=500m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-XX:CICompilerCount=2
-Dsun.io.useCanonPrefixCache=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-Djdk.attach.allowAttachSelf=true
-Dkotlinx.coroutines.debug=off
-Djdk.module.illegalAccess.silent=true
本文讲述了JVM的分代模型,新生代,老年代,接着,我们对于JVM对象在分代里面分配的一些基础概念,比如对象优先分配在老年代,对象年龄晋升到老年代以及垃圾回收之后长期存活对象进入老年代,同样,JVM也存在一些特殊的判断机制让新生代提前进入老年代,这些都是十分重要的优化,在后续的系列文章中会深入讲解。
了解分代的概念以及熟悉JVM的内存模型是非常重要的,因为现代垃圾收集器不断进化以及复杂甚至放弃分代的理念,十分有必要了解分代的历史以及分代的进程,同时不分代势必会是未来趋势。