该系列文章主要是记录下自己暑假这段时间的学习笔记,暑期也在实习,抽空学了很多,每个方面的知识我都会另起一篇博客去记录,每篇头部主要是另起博客的链接。
JavaSE集合(已写)
JavaEE框架(未写)
虚拟机JVM运行时区域及垃圾回收(已写)
Java并发(未写)
计算机网络(已写)
八大经典排序算法原理及实现(已写)
一、JVM 运行时数据区域
JVM运行时数据区域可以分为:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区(包含运行时常量池)
1、程序计数器:
可看做是当前线程所执行的字节码的行号指示器
- 线程私有
2、Java 虚拟机栈
描述的是Java方法执行的的内存模型,每一个方法执行的同时都会创建一个栈帧(栈帧详解)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行到结束,都对应着一个栈帧入栈到出栈
- 局部表量表:存放了编译器可知的各种基本数据类型、对象引用
- 线程私有
- 如果线程请求的栈深度大于虚拟机所允许的深度会抛出StackOverflowError异常
- 如果虚拟机栈动态扩展时无法申请到足够的内存,会抛出OutOfMemoryError错误
3、本地方法栈
与Java虚拟机栈的发挥的作用差不多,区别在于Java虚拟机栈是为Java方法服务、本地方法栈是为本地(Native)方法服务
- 线程私有
- 如果线程请求的栈深度大于虚拟机所允许的深度会抛出StackOverflowError异常
- 如果虚拟机栈动态扩展时无法申请到足够的内存,会抛出OutOfMemoryError错误
4、Java 堆
堆在虚拟机启动时创建,唯一的目的就是存放几乎所有的对象实例。
- 线程共享
- 堆是垃圾收集的主要区域,常被称为GC堆(Garbage Collected Heap)
- 可分为新生代、老年代:
- 再细致可以分为Eden、From Survivor、To Survivor空间:
- 堆中没有内存完成实例分配,并且堆也无法扩展时,抛出OutOfMemoryError
5、方法区
存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据
- 线程共享
- 当方法区无法满足内存分配需求时,将抛出OutOfMemooryError
6、运行时常量池
是方法区的一部分.Class文件除了有类的版本、字段、方法、接口等描述信息外;还有一项常量池,用于存放编译期生成的各种字面量和符号引用??,这些内容再类被加载后进入方法区的运行常量池中存放。
- 运行时常量池相对于Class文件常量池具有动态性,Java语言不要求常量一定只有编译期才产生,String类的intern()方法可以在运行期间将常量插入运行时常量池
- 当常量池无法再申请到内存时,会抛出OutOfMemoryError
7、直接内存
并不是虚拟机运行时数据的一部分,也不是Java虚拟机规范中定义的内存区域,但是频繁的使用也会造成OutOfMemooryError
二、对象的创建
大家都知道Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建。语言层面上有通过 new方式 来创建、也有通过反射机制方式创建。在JVM中是怎么创建的呢?
- 类加载检查:当创建对象指令下达后,先检查是否能在常量池中定位到该类的符号引用,并且检查这个类是否已经被加载、解析、和初始化过。若没有,那必须先执行类加载
- 为新生对象分配内存:对象所需分配的大小在类加载完成后便可完全确定,分配内存其相当于在堆中划分出一块相应的大小的内存。在为对象分配内存时,需要考虑线程安全的问题
- 将分配到的内存空间都初始化为0值(不包括对象头):保证了对象的实例字段在Java代码中可以不赋初值就直接使用
- 对对象进行必要的设置:比如对象是哪个类的实例,如何才能找到元数据信息,对象个哈希码、对象的GC分代信息等。这些信息都存储在对象头中
- 上述步骤之后,虚拟机层面上一个新对象已经产生,但从Java层面上对象的创建才开始。其 <init>方法还没有执行,所有的字段都还未0
三、对象的访问定位
建立对象都是为了使用它,在Java程序中,通过栈中的 reference 数据来操作堆上的具体对象
-
句柄访问方式
-
直接指针访问方式
四、JVM垃圾收集器与内存分配策略
在了解JVM垃圾回收之前,需要知道3个问题:
- 哪些内存需要释放?
- 什么时候释放?
- 如何回收?
首先要知道JVM运行时数据区域中程序计数器、Java虚拟机栈、本地方法都是随线程而生、线程而亡,即线程私有的。在这3个区域的内存分配和回收都具有确定性,在方法结束或线程结束时内存就自动回收了,因此不用考虑垃圾回收。
在Java堆中和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存也不一样,只有在程序运行期间才能知道,这部分内存的分配和回收都是动态的。因此在这两个区域考虑垃圾回收。
判断对象是否存活
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效后,计数器就减1;当计数器为0时的对象就是不可能再被使用的对象
- 优势:实现简单、效率高
- 劣势:难以解决对象之间相互循环引用的问题
2、可达性分析算法
通过一系列称为"GC Roots"对象作为起始点,从这些点向下搜索,搜索过程中走过的路径称为引用链,当一个对象到GC Roots没有任何的引用链相连接时,则证明该对象是不可用的
- 克服引用计数算法无法解决对象之间相互循环引用的问题
- GC Root 的对象包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
3、再次确认对象的存亡
经过上述任意一种算法,判断出不可用的对象并非是非死不可的。一个不可用的对象再第经过上述算法之后,将第一次标记并且进行筛选,筛选条件为该对象是否有必要执行finelize()方法。
当对象没有覆盖finelize()方法,或者finelize()方法已经被虚拟机调用过,JVM将这两种情况都视为没有必要执行
4、回收方法区
很多人认为JVM方法区(永久代)中是没有垃圾收集的,其实是有的,主要回收这两部分:废弃常量和无用的类
垃圾回收算法
1、标记-清除算法
如同其名字一样,算法分为标记和清除两个阶段:首先标记所有需要回收的对象,
在统一回被标记的对象;其标记过程和判断对象的存活过程类似
- 不足:
- 标记和清除两个过程效率都低;
- 标记清除后会产生大量不连续的内存碎片,空间碎片太多会导致后面分配大内存对象时,无法找到足够的连续内存而不得不提前出发一个垃圾收集动作
2、复制算法
将可用内存划分为大小相等的两块,每次只使用其中的一块;当这一块内存快使用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的那块内存空间一次清理掉。
- 解决了标记清理的效率和内存碎片问题
- 不足:将内存缩小为原来的一半,付出的代价太高
新生代中解决复制算法的不足:
研究表明新生代中的对象98%是“朝生夕死”的,所以并不需要按1:1比例来划分空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其比例为8:1:1,每次使用Eden空间和一块Survivor空间,即新生代中可用内存空间为90%。
若另一块Survivor空间没有足够空间存放上一次新生代中收集下来存活的对象,这些对象将会直接被分配到老年代
3、标记整理算法
- 复制算法在对象存活率较高时就要进行较多复制操作,效率也会变低,更关键是会浪费空间
针对老年代的特点,提出标记-整理算法,其标记过程和标记清除算法中一样,而后续步骤则不一样,而是让所有存活的对象都向一端移动,然后直接清除掉该端边界以外的内存。
- 标记-整理主要解决了内存碎片问题
分代收集算法
将JVM堆分为新生代和老年代,这样可以根据各个年代的特点采用最合适的收集算法
新生代中,每次垃圾收集时发现大批对象死去,只有少量对象存活,就选用复制算法,只需付出少量存活对象的复制成本就可以完成收集
老年代中,对象的存活率很高,无额为的空间为其进行分配担保,就必须使用标记-清除或标记-整理算法
简单总结:
- 两个最基本的java回收算法:复制算法和标记清理算法
- 复制算法:两个区域A和B,初始对象在A,继续存活的对象被转移到B。此为新生代最常用的算法
- 标记清理:一块区域,标记要回收的对象,然后回收,一定会出现碎片,那么引出
- 标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象
- 两个概念:新生代和年老代
- 新生代:初始对象,生命周期短的
- 永久代:长时间存在的对象
- 整个java的垃圾回收是新生代和年老代的协作,这种叫做分代回收。
垃圾收集器
**Serial **收集器是针对新生代的收集器,采用的是复制算法
Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理
Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法
Serial Old(串行)收集器,新生代采用复制,老年代采用标记清理
Parallel Old(并行)收集器,针对老年代,标记整理
CMS收集器,针对老年代,基于标记清理
G1收集器,整体上是基于标记清理,局部采用复制
综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。
JVM内存分配参数
- Xmx:最大堆大小
- Xms:初始堆大小,即最小内存值
- Xmn:年轻带大小
- XXSurvivorRatio:年轻带中 Eden:FromSurvivor:ToSurvivor=3:1:1