大部分内容都是《深入理解Java虚拟机上的内容》的总结,少部分内容是来自于网上或者自己的理解。读完应该会把没笔记的markdown文件放在github上。

本部分笔记对应的是《深入理解Java虚拟机》第二章和第三章。

Java内存区域与内存溢出

运行时数据区域

Java虚拟机在执行Java程序过程中会把它所管理的内存区域划分若干个不同的数据区域

程序计数器(Programm Counter Register)

程序计数器是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理等基础功能都是需要依赖这个计数器来完成的。

在任一时刻,一个处理器都只会执行一条线程中的指令,因此,为了线程切换后能后恢复到正确的执行位置,每一条线程都需要一个独立的程序计数器,计数器之间不会互相影响,独立存储,我们称为“线程私有的”。

Java虚拟机栈(Java Virtual Machine Stacks)

Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型,每个方法在执行时会创建一个栈帧(stack frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

局部变量表,存放了编译期可知的各种基本数据类型,对象引用和returnAddress类型。

本地方法栈(Native Method Stack)

虚拟机栈是为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

Java堆 (Java Heap)

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

方法区(Method Area)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码片段等数据

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项重要的常量池(Constant Pool Table),用于存放编译期生成的各种字面变量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放

Java并不一定要求常量一定只有在编译期间产生,也可以在运行期间将新的常量放入池中。

直接内存

手动分配的。

NIO(new Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一种存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

HotSpot虚拟机对象探秘

对象的创建

1 虚拟机遇到new命令,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已被加载,解析和初始化过。如果没有,就必须要先执行相应的类加载。

2 在类检查通过了,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便知道了,为对象分配空间任务等同于把一块确定大小的内存从Java堆中划分出来

3 虚拟机要对对象进行必要的设置,例如这个对象时哪个类的实例,对象哈希码等。这些信息存放在对象的对象头(Object Header)

4 执行完new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

对象在内存中布局,分为3块,对象头(Header),实例对象(Instance),对其填充(Padding)

对象头

分为两个部分,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态,线程持有锁,偏向线程ID等。官方称为Mark Word。

另一个部分是类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是一个Java数组,那在对象头中还必须要有一块用于记录数据长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是数组的元数据中却无法确定数组的大小。

实例数据部分

是对象真正存储的部分,也是在程序代码中所定义的各种类型的字段内容。(我的理解,Java并不能给实例动态的添加字段,属性)。

对齐填充

HotSpot要求对象的大小的必须是8个字节的整数倍,所以有时需要填充。

垃圾收集器与内存分配策略

程序计数器,虚拟机栈,本地方法栈这个3个区域随线程而生,随线程而亡。栈中的栈帧随着方法的进入和退出而有序地执行出栈和入栈的操作。这一部分的内存分配和回收都是确定的。

GC Roots

通过一些称为GC Roots的对象作为起始点,从这些节点开始搜索,搜索和该节点法生直接或者间接引用关系的对象,将这些对象以链的形式组合起来,形成一张关系网,又叫做引用链。最后垃圾收集器就回收一些不在这张关系网上的对象。

有四种对象可以作为GC Roots

1 栈帧中的引用对象

2 静态属性引用对象

3 常量引用的对象

4 本地方法栈中JNI引用的对象

引用

强引用

指在程序代码普遍存在的,类似”Object o = new Object()“。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

软引用

描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

弱引用

被弱引用关联的对象只能生存到下一次垃圾回收发生之前,当垃圾收集器开始工作时,无论当时内存是否足够,都会回收只被弱引用关联的对象。

虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的唯一目的是,能在这个对象被收集器回收时收到一个系统通知。PhantomReference类来实现虚引用。

二次标记

第一次标记不在引用链中的对象。如果该对象有有finalize方法,会将对象放入到F-Queue队列中,之后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行它。随后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象在finalize函数中自救成功,那么第二次标记时,它将被移除即将回收的集合,否则它就真正被回收。

finalize函数

被gc时会调用,程序退出时会调用。因为任何一个对象的finalize()的方法都只会被系统自动的调用一次,如果对象面临下一次的回收,它的finalize()不会被执行。尽量避免去使用它

回收方法区

废弃常量

系统中没有一个String对象叫“ABC”,则就回收。常量池中的其他类(接口),方法,字段的符号引用类似。

无用的类

1 该类所有实例都已经被回收

2 加载该类的ClassLoader已经被回收

3 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾回收算法

标记-清除算法

首先标记所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。会造成大量不连续的内存碎片。

复制算法

它将可用的内存按照容量分为大小相等的两块,每次只用其中的一块。带一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

回收新生代

将内存分配为一块较大的Eden空间和两块较小的Survivior空间,每次使用Eden和其中一块Survivior。当回收时,将Eden和刚才使用的Survivior空间还存活的对象一次性复制到另外一块Survivor空间上(如果另一块Survivor没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象直接通过分配担保机制进入老年代)最后清理Eden和刚才使用的survivor空间。HotSpot虚拟机默认Eden和Survivor的比例时8:1。

标记-整理算法

首先标记所有需要回收的对象,在标记完成之后,让所有存活的对象都向一段移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象存活周期的不同将内存划分为几块。

一般新生代使用复制算法回收,老年代使用标记-整理算法回收。

垃圾收集器

Serial收集器(新生代)

它只会利用一个CPU或者一条线程去完成垃圾收集工作。在它进行垃圾回收时,必须要停止其他所有的线程,知道它收集结束

ParNew收集器(新生代)

Serial收集器的多线程版本。

Parallel Seavenge收集器(新生代)

目的是达到一个可控的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (用户代码时间 + 垃圾收集时间)。

Serial Old收集器(老年代)

Serial收集器的老年代版本。使用标记-整理算法

Parallel Old收集器(老年代)

是Parallel Seavenge收集器的老年代版本,使用多线程和标记-整理算法。

CMS收集器(Concurrent Mark Sweep)(老年代)

是一种以获取最短回收停顿时间为目标的收集器。第一次实现了让垃圾回收线程和用户线程基本上同时工作。使用了标记-清除算法。

  1. 初始化标记(CMS initial mark)stop the world

    只标记与GC Roots能直接关联的对象

  2. 并发标记(CMS concurrent mark)

    对于GC Roots进行可达性分析。并发标记

  3. 重新标记(CMS remark)stop the world

    修正在并发标记期间内因用户线程运作而导致标记产生变动的那一部分对象

  4. 并发清除(CMS concurrent sweep)

    并发清除

缺点

  1. 吃CPU资源
  2. 浮动垃圾

    由于CMS并发清理阶段,用户线程还在运行着,也会导致新的垃圾产生。所以CMS只好留待下一次GC时再清除掉。

  3. 内存碎片

G1收集器(新生代和老年代)

它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是无力隔离,它们是一部分(Region)的集合。

  1. 初始标记(Initial Marking)stop the world

    只标记与GC Roots能直接关联的对象。并且修改TAMS(Next to at Mark Start)的值,让下一阶段用户并发运行时,能正确可用的Region中创建对象。

  2. 并发标记(Concurrent Marking)

    对于GC Roots进行可达性分析。

  3. 最终标记(Final Marking)stop the world

    修正在并发标记期间内因用户线程运作而导致标记产生变动的那一部分对象。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中。

  4. 筛选回收(Living Data Counting and Evacuation)

    对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

GC日志

33.125 [GC [DefNew 3324K->152K(3712K), 0.0026 secs] 3324K->152K(10240K) 0.00316secs

最前面数字 33.125 表示GC发生的时间,从Java虚拟机启动以来经历的秒数

[GC [ Full GC 表示这次垃圾回收的类型,Full表示,发生了Stop-The-World

[DefNew 表示GC发生的区域,与GC收集器有关。

3324K->152K(3712K), GC前该内存区域已使用容量->GC后该内存区域已使用的容量(该内存区域总容量)

0.0026 secs该内存区域GC所占用的时间

3324K->152K(10240K) GC前Java堆已使用容量->GC后Java堆已使用的容量(Java堆总容量)

0.00316secs 总的时间,包含I/O等

内存分配和回收策略

对象的内存分配,就是在堆上分配,对象主要分配在新生代Eden区上,如果启动了本地线程分配缓冲,将线程优先分配TLAB上分配。

TLAB(Thread Local Allocation Buffer) 在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一块空间,如果需要分配内存,就在自己的空间上分配。

对象优先分配在Eden上

大对象直接进入老年代

长期存活的对象将进入老年代

年龄计数器,在新生代没熬过一次gc,则加1。满足设置值,则移动到老年代。

动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小综合大雨Survivor空间的一般,年龄大于等于该年龄的对象就直接进入老年代

空间分担担保

虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间或者历次晋升到老年代对象的平均大小,如果这个条件成立,那么进行新生代GC是安全的,否则进行一次Full GC。


fish
101 声望2 粉丝

希望你能够学习新的技术