写在最前
从毕业到现在已经过去了差不多一年的时间,工作还算顺利,但总是离不开CRUD ,我觉得这样下去肯定是不行的,温水煮青蛙,势必有一天,会昏昏沉沉的迷失在温水里。所以,需要将之前学习JAVA 当中一些中高级部分的知识需要进行学习和记录,并将其整理博客,一起成长,一起努力。
JVM
JAVA虚拟机在运行的时候,会给所有的变量、以及实例对象等分配内存区域,当然这一块内存区域是在Java 虚拟机上分配的,虚拟机的内存。
这里先来了解一个区别,就是JVM 与 Hotspot
先从JVM内存分布区分为:线程共享区以及线程私有区
线程内存共享区堆 Heap
方法区/永久带
线程的内存共享区域包含堆和方法区,方法区也可以叫做是永久带,其实是HotSpot VM 将堆里面一块关于永久代的内存区域用来实现方法区,为什么要这样做呢?其实虚拟机的垃圾回收机制希望也控制这一块内存的装载与卸载,但是手头有不想单独给他独立划分一个出去,那就从堆里面拿出用来放永久代的内存区域用来实现方法区即可。
堆 heap (线程共享)
堆作为JVM 虚拟机最大的共享内存区域,用来存放实例对象以及数组等,也是垃圾收集器GC 进行的重要区域。这里一会儿会涉及到一个关于分代回收的垃圾处理算法。
首先来了解一小部分,比如我们平时使用new 关键字进行对象的实例化的时候,就会在堆里面开辟一块内存,用来存放我们实例后的对象。
当代JVM 大都采用分代回收的算法,按照GC的角度,又可以将堆细分为新生代和老年代
新生代占据堆的1/3,而老年代占据堆内存的2/3
新生代的划分 Eden/FromSurvivor/To Survivor
新生代Eden区
这里是每一个对象出生的地方,每当新分配一个对象的时候,若出现Eden区域内存不足,则会触发MinorGC
负责清理新生代的内存。
FromSurvivor
上一次GC 清理过后的幸存者,分配到此块区域
To Survivor
在Minor GC 清理过程中的幸存者,移到该区域。
新生代Minor GC 回收过程
回收算法:复制算法
回收过程:复制-》清空-》互换首先将Eden 区和From 区域存活的对象进行复制到To区域
将原有的Eden区和From 区域进行清空
最后将From 域To 区域进行一个互换,这时候To区域将成为下一次GC过程当中的From 区域
老年代区域
从上面学习的内容了解,老年代的对象大多都来自于对象年龄的+1导致对象年龄超过新生代,从而防止到老年代的位置,也有一部分来自于
新生代,新生代在初始化实例的时候,若遇到内存不足,可直接将对象内存分配到老年代。
老年代的对象基本上都很稳定,因此,在老年代工作的GC : Major GC
Major GC 不会频繁的进行内存清理。在Major GC 进行前,至少有一次Minor GC 进行了新生代的清理工作,导致对象年龄增加而在老年代有了其对象。
清理算法:标记清除法
方法区、永久代(线程共享)
方法区用来存放一些常量、静态变量、以及JAVA 虚拟机加载类以后类的一些信息都是存放在方法区的。
方法区还存在一个叫做运行时常量池。
GC 不会在主程序运行期对永久区域进行清理。所以这
也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
运行时常量池
常量池作为方法区的一部分,在Class 文件被java虚拟机加载的时候,一个类对象包含的字段、方法、还有一项就是常量池,常量池用于存放编译时期产生的各种字面量和符号引用。
class 文件被加载后,所产生的内容就会被存放到方法区的常量池。
线程内存私有区
线程私有区域的内存生命周期与线程的生命周期是一致的。依赖用户线程启动则分配内存,线程运行结束则回收内存。
在HotspotVM 当中,每个用户的线程与操作系统的线程直接映射,这一部分的内存跟随本地线程的存活
虚拟机栈(线程私有)
虚拟机栈是描述JAVA方法在运行时候的内存模型,而每个线程在虚拟机栈里面都有属于自己的栈帧,栈帧随着方法执行被创建,
创建入栈,执行完毕方法后出栈,栈帧被销毁。
一个栈帧包含有:方法执行过程产生的局部变量表,以及操作数栈,动态链接,方法出口等。
随着方法创建而创建入栈,随着方法运行完毕则销毁。
本地方法栈(线程私有)
本地方法栈主要为本地Native方法服务,而与之类似的虚拟机栈则是为Java 方法提供服务
程序计数器(线程私有)
程序计数器是一块较小的内存空间,
每个线程都有自己的程序计数器,计数器可以理解为是一个储存每个线程在执行过程中记录当前执行的行号以及
Java 方法在执行过程中虚拟机字节码指令的地址。
Java 8 与元空间
现在基本上所有的开发都一般以JDK 1.8开始,我们需要了解一下JAVA8 当中的元空间。
其实元空间的理解与在JVM当中的方法区/永久代类似。永久代在JAVA8当中被移除
不同在于:元空间不在虚拟机内,而在用户机器的内存上开辟。
JVM在加载类的时候,将类的元数据(字段、名称、类型、长度)等放入本地内存。
而将字符串常量以及类的静态变量等信息放入JVM 堆中形成字符串常量池。
好处:不会再因为永久代从不会被GC进行清理导致的OOM错误等。
容易混淆的地方:
常量池、运行时常量池、字符串常量池
学以致用
通过上面的了解。已经大致了解到常量池的一些相关内容了。最后再提一下。以及新手很难立即的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);//false9 System.out.println(b.intern() ==c);//true10 System.out.println(b.intern() ==d);//true11 System.out.println(b.intern() ==f);//false12 System.out.println(b.intern() == a.intern());//true
就按照这个例子,和大家简单的聊一聊。
1、2行分别用new 关键字创建了对象,此时的对象存在于堆中
3行直接用双引号声明的对象“ab” 则首先会添加到常量池里面。String 类型的c变量指向位于字符串常量池的"ab";
4行通过+号将直接声明的"a"和“b”进行了一个拼接,这里需要着重说明一下:
所以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以后,将运行时常量池就放到堆里面了。