Java 中级 学习笔记 1 JVM的理解以及新生代GC处理流程和常量池、运行时常量池、字符串常量池的理解

写在最前

从毕业到现在已经过去了差不多一年的时间,工作还算顺利,但总是离不开CRUD ,我觉得这样下去肯定是不行的,温水煮青蛙,势必有一天,会昏昏沉沉的迷失在温水里。所以,需要将之前学习JAVA 当中一些中高级部分的知识需要进行学习和记录,并将其整理博客,一起成长,一起努力。

JVM

JAVA虚拟机在运行的时候,会给所有的变量、以及实例对象等分配内存区域,当然这一块内存区域是在Java 虚拟机上分配的,虚拟机的内存。

这里先来了解一个区别,就是JVM 与 Hotspot

JVM 可以理解为是一种标准,就好像我们JAVA 里面定义的接口一样,它是一个笼统的概念。

Hotspot则具体是JVM 的一种具体实现,它由SUN 公司开发,并且在Open-jdk 与 sun-jdk 当中包含的,都是Hotspot 虚拟机。

特点:JIT 即时编译、热点探测等

  参考:https://www.cnblogs.com/baxianhua/p/9528192.html

 

先从JVM内存分布区分为:线程共享区以及线程私有区

线程内存共享区

  • 堆 Heap
  • 方法区/永久带

线程的内存共享区域包含堆和方法区,方法区也可以叫做是永久带,其实是HotSpot VM 将堆里面一块关于永久代的内存区域用来实现方法区,为什么要这样做呢?其实虚拟机的垃圾回收机制希望也控制这一块内存的装载与卸载,但是手头有不想单独给他独立划分一个出去,那就从堆里面拿出用来放永久代的内存区域用来实现方法区即可。

 

堆 heap (线程共享)

堆作为JVM 虚拟机最大的共享内存区域,用来存放实例对象以及数组等,也是垃圾收集器GC 进行的重要区域。这里一会儿会涉及到一个关于分代回收的垃圾处理算法。

首先来了解一小部分,比如我们平时使用new 关键字进行对象的实例化的时候,就会在堆里面开辟一块内存,用来存放我们实例后的对象。

 

当代JVM 大都采用分代回收的算法,按照GC的角度,又可以将堆细分为新生代和老年代

新生代占据堆的1/3,而老年代占据堆内存的2/3

 

新生代的划分 Eden/FromSurvivor/To Survivor

新生代Eden区

这里是每一个对象出生的地方,每当新分配一个对象的时候,若出现Eden区域内存不足,则会触发MinorGC

负责清理新生代的内存。

Minor GC

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

  1. 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  2. 内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
  3. 执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
  4. 质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。

所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。

https://www.cnblogs.com/williamjie/p/9516264.html

FromSurvivor

上一次GC 清理过后的幸存者,分配到此块区域

To Survivor

在Minor GC 清理过程中的幸存者,移到该区域。

 

新生代Minor GC 回收过程

回收算法:复制算法

回收过程:复制-》清空-》互换

  1. 首先将Eden 区和From 区域存活的对象进行复制到To区域
  2. 将原有的Eden区和From 区域进行清空
  3. 最后将From 域To 区域进行一个互换,这时候To区域将成为下一次GC过程当中的From 区域

老年代区域

从上面学习的内容了解,老年代的对象大多都来自于对象年龄的+1导致对象年龄超过新生代,从而防止到老年代的位置,也有一部分来自于

新生代,新生代在初始化实例的时候,若遇到内存不足,可直接将对象内存分配到老年代。

老年代的对象基本上都很稳定,因此,在老年代工作的GC : Major GC 

Major GC 不会频繁的进行内存清理。在Major GC 进行前,至少有一次Minor GC 进行了新生代的清理工作,导致对象年龄增加而在老年代有了其对象。

清理算法:标记清除法

MajorGC 采用标记清除算法首先扫描一次所有老年代,标记出存活的对象,然后回收没
有标记的对象。 MajorGC 的耗时比较长,因为要扫描再回收。 MajorGC 会产生内存碎片,为了减
少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的
时候,就会抛出 OOM(Out of Memory)异常。 

方法区、永久代(线程共享)

方法区用来存放一些常量、静态变量、以及JAVA 虚拟机加载类以后类的一些信息都是存放在方法区的。

方法区还存在一个叫做运行时常量池。

GC 不会在主程序运行期对永久区域进行清理。所以这
也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

 

运行时常量池

常量池作为方法区的一部分,在Class 文件被java虚拟机加载的时候,一个类对象包含的字段、方法、还有一项就是常量池,常量池用于存放编译时期产生的各种字面量和符号引用。

class 文件被加载后,所产生的内容就会被存放到方法区的常量池。

线程内存私有区

线程私有区域的内存生命周期与线程的生命周期是一致的。依赖用户线程启动则分配内存,线程运行结束则回收内存。

Hotspot VM 当中,每个用户的线程与操作系统的线程直接映射,这一部分的内存跟随本地线程的存活

 

虚拟机栈(线程私有)

虚拟机栈是描述JAVA方法在运行时候的内存模型,而每个线程在虚拟机栈里面都有属于自己的栈帧,栈帧随着方法执行被创建,

创建入栈,执行完毕方法后出栈,栈帧被销毁。

一个栈帧包含有:方法执行过程产生的局部变量表,以及操作数栈,动态链接,方法出口等。

随着方法创建而创建入栈,随着方法运行完毕则销毁。

本地方法栈(线程私有)

本地方法栈主要为本地Native方法服务,而与之类似的虚拟机栈则是为Java 方法提供服务

程序计数器(线程私有)

程序计数器是一块较小的内存空间,

每个线程都有自己的程序计数器,计数器可以理解为是一个储存每个线程在执行过程中记录当前执行的行号以及

Java 方法在执行过程中虚拟机字节码指令的地址。

Java 8 与元空间

现在基本上所有的开发都一般以JDK 1.8开始,我们需要了解一下JAVA8 当中的元空间。

其实元空间的理解与在JVM当中的方法区/永久代类似。永久代在JAVA8当中被移除

不同在于:元空间不在虚拟机内,而在用户机器的内存上开辟。

JVM在加载类的时候,将类的元数据(字段、名称、类型、长度)等放入本地内存。

而将字符串常量以及类的静态变量等信息放入JVM 堆中形成字符串常量池。

好处:不会再因为永久代从不会被GC进行清理导致的OOM错误等。

 

容易混淆的地方:

常量池、运行时常量池、字符串常量池

在Class文件中除了有类的版本【高版本可以加载低版本】、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)【此时没有加载进内存,也就是在文件中】,用于存放编译期生成的各种字面量和符号引用

下面对字面量和符号引用进行说明
字面量
字面量类似与我们平常说的常量,主要包括:

  1. 文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
  2. 被final修饰的变量。

符号引用
主要包括以下常量:

  1. 类和接口和全限定名:例如对于String这个类,它的全限定名就是java/lang/String。
  2. 字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
  3. 方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型

2.2 运行时常量池

我们知道类加载器会加载对应的Class文件,而上面的class文件中的常量池,会在类加载后进入方法区中的运行时常量池【此时存在在内存中】。并且需要的注意的是,运行时常量池是全局共享的,多个类共用一个运行时常量池。并且class文件中常量池多个相同的字符串在运行时常量池只会存在一份。
注意运行时常量池存在于方法区中。

2.3 字符串常量池

  看名字我们就可以知道字符串常量池会用来存放字符串,也就是说常量池中的文本字符串会在类加载时进入字符串常量池。
那字符串常量池和运行时常量池是什么关系呢?上面我们说常量池中的字面量会在类加载后进入运行时常量池,其中字面量中有包括文本字符串,显然从这段文字我们可以知道字符串常量池存在于运行时常量池中。也就存在于方法区中。
不过在周志明那本深入java虚拟机中有说到,到了JDK1.7时,字符串常量池就被移出了方法区,转移到了里了。
那么我们可以推断,到了JDK1.7以及之后的版本中,运行时常量池并没有包含字符串常量池,运行时常量池存在于方法区中,而字符串常量池存在于堆中。

引用:https://www.cnblogs.com/gxyandwmm/p/9495923.html

 

学以致用

通过上面的了解。已经大致了解到常量池的一些相关内容了。最后再提一下。以及新手很难立即的String 这个引用类型的一些操作中涉及到的内容

String intern() 

Intern() 方法算是很常见但却很容易忽略的一个关键方法,在JDK的文档中,它是这样定义的。

若池里面存在与之内容相同的字符串,则返回常量池那个对象的引用,若不存在,则创建一个,并返回此对象在池里面的引用地址。

 1 String a = new String("ab");
 2 String b = new String("ab");
 3 String c = "ab";
 4 String d = "a" + "b";
 5 String e = "b";
 6 String f = "a" + e;
 7 
 8 System.out.println(b.intern() == a);//false
 9 System.out.println(b.intern() == c);//true
10 System.out.println(b.intern() == d);//true
11 System.out.println(b.intern() == f);//false
12 System.out.println(b.intern() == a.intern());//true

就按照这个例子,和大家简单的聊一聊。

1、2行分别用new 关键字创建了对象,此时的对象存在于堆中

3行直接用双引号声明的对象“ab” 则首先会添加到常量池里面。String 类型的c变量指向位于字符串常量池的"ab";

4行通过+号将直接声明的"a"和“b”进行了一个拼接,这里需要着重说明一下:

JAVA 在编译的时候就会把类似“aaa”+"bbb"的代码直接优化成:“aaabbb”

所以4行这里进行了所谓的拼接,其实编译后还是“ab”,当然,第三行执行完后,常量池已存在“ab” 那么String 类型的变量d 指向常量池“ab”

5、6行通过先定义一个“b”存放到字符串常量池后,通过拼接变量的方式,其实这个对象最后是创建在了堆里面而不会进入常量池。

 

答案解析

8行通过b变量执行intern() 方法后,去常量池找,找到返回的其实是c的内存地址。则肯定和a(new 出来的)内存地址不相等。false

9行就不用说了,c与c比较,肯定true

10行 d其实指向的本来就是c的内存地址。true

11我们知道一个字符串常量+一个字符串变量得到的一个新对象其实是在堆里面出现的,肯定不会相同。false

12最后一个想必不用多说,两个都拿出的是c的地址。true

 

小结

通过今天的学习,掌握JVM 当中内存的分布关系以及堆这个最重要的内存共享区域内的对象迭代过程,以及从Class 文件被编译,编译后形成常量池,再到Class文件被

JVM加载到内存后,将对象的信息存入方法区,而方法区域存在的运行时常量池。也就为了存放对象的字面量、以及符号引用

再到JDK8以后,将运行时常量池就放到堆里面了。

 

参考:

常量池相关内容: https://www.cnblogs.com/gxyandwmm/p/9495923.html

字符串的拼接:https://www.cnblogs.com/nianzhilian/p/8810966.html

intern() https://www.runoob.com/java/java-string-intern.html

posted @ 2020-01-09 16:33  程序猿小码  阅读(632)  评论(0编辑  收藏  举报