内存区域
程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
- 作用 记住下一条jvm指令的执行地址
- 特点
- 线程私有的
- 不会内存溢出
虚拟机栈
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[插图](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
栈内存溢出
- 栈帧过大(一般不会出现)
- 栈帧过多
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
heap 堆
- 通过new对象创建的关键字都会使用堆内存
- 他是线程共享的,堆中的内存都需要考虑线程安全问题
- 他有垃圾回收机制
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
方法区
方法区主要存储类相关的部分,主要包括、类信息,类加载器信息。
方法区逻辑上是堆的一部分(不同jvm厂商不一样)oracle hotspot虚拟机在1.8以前叫永久代(永久代作为方法区的实现),使用的是堆的一部分,1.8以后叫做元空间(metaspace),用的是本地的内存,操作系统的内存,已经不是jvm,将stringtable移动到了堆里面
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
Bytebuffer,操作系统划出一块区域java代码可以直接访问,系统和java代码可以共享,磁盘读取可以读取到直接内存,java代码也可以读到直接内存,比普通的系统缓冲区再到java缓冲区这一部分节省了
垃圾回收
判断一个对象是否可以被回收
-
引用计数法(循环引用会出错,所以不用)
-
可可达性分析法 (扫描堆中的所有对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以被回收,这也是jvm采用的方法)
四种引用
- 强引用
我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
- 弱引用
只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用
虚引用与前面软引用和弱引用不同的是必须通过引用队列才能发挥作用,在直接内存中,在NIO,bytebuffer会分配一块直接内存,会把直接内存传递给虚引用对象Cleaner,将来bytebuffer没有强引用来引用他,bytebuffer会被垃圾回收掉,但是他分配的直接内存不能被java的垃圾回收管理,所以我们让bytebuffer被回收时,让虚引用对象进入引用队列,会有一个线程会定时在引用队列中查找,如果有就会调用clean方法(clean方法会根据前面记录的直接内存的地址,调用Unsafe.freeMemory,会把直接内存给释放掉)
- 终结器引用
Object的finalllize方法,当对象重写了finallize方法,并且没有强引用引用时就可以当成垃圾进行回收,终结方法的调用就是通过终结器引用,当对象被垃圾回收时,先将终结器引用加入到引用队列,再由一个优先级很低的线程finallize线程,根据终结器引用找到要作为垃圾回收的对象,调用finallize方法,这才是真正的回收掉了(我们发现finallize方法效率很低,第一次还不能回收,要先将他入队,入队了以后还要等,如果迟迟不被调用,内存就一直得不到回收,所以我们不推荐使用finallize方法)
软引用应用
软引用使用的例子
情景:一个集合里面需要存放特别多的数据,例如使用字节数组来存储图片,假如将字节数组全部存放到集合中,因为是强引用,所以数据量大时会出现内存溢出的现象,因此可以将这些不是特别重要的信息使用弱引用来存储,一旦内存不足,就会被垃圾回收器回收
List<byte[]>list=new ArrayList();
List<SoftReference<byte[]>>list=new ArrayList();
SoftReference<byte[]>ref=new SoftReference<>(new byte[4*1024*1024]);
list.add(ref);
引用队列
当软引用引用的对象被回收时,我们同时也希望软引用本身被回收,所以就引出了引用队列的使用,当软引用所关联的byte数组被回收时,软引用自己会加入到queue中去。
ReferenceQueue<byte[]>queue=new ReferenceQueue<>();
SoftReference<byte[]>ref=new SoftReference<>(new byte[4*1024*1024],queue);//这就关联了引用队列
//将在引用队列中的对象全部都移除掉
Reference<? extends byte[]> poll=queue.poll();
while(poll!=null){
list.remove(poll);
poll=queue.poll();
}
弱引用根软引用类似
WeakReference<byte[]>list=new ArrayList<>();
垃圾回收算法
- 标记清除
原理:1.根据是否被gcroo直接或者间接引用,来判断是不是垃圾,将垃圾进行标记 2.将标记的垃圾的起始地址保存起来,代表是可以使用的内存
优点:速度较快
缺点:空间不连续,容易造成内存的碎片
- 标记整理
原理: 1.标记 2.整理(紧凑)
由于要移动,所以速度比较慢
- 复制
原理:1.标记
2.移动 有两块内存区域,一块from,一块to,将from中的不是垃圾的连续拷贝到to中,在将to和from进行交换(交换的是from和to引用的地址,因此效率比较高)
缺点: 要占用两份内存空间
上面这三种算法,实际在jvm中会根据不同情况采用。
分代垃圾回收
内存中垃圾有新生代和老年代。
新生代有伊甸园区,幸存区from,幸存区to,老年代。
- 伊甸园区
对象首先在这个区域分配
- 幸存区from
将垃圾回收幸存下来的对象放到这里来
- 幸存区to
不存储任何对象,只是应用标记复制算法所需要的空间,新生代回收垃圾时,将伊甸园区和幸存区的存活对象拷贝到这里来,同时交换from和同的指针,将对象的年龄加一。
- 老年代
老年代也是存放垃圾回收幸存下来的对象的,当一个对象的年龄超过15(回收了15次都存活,后面可以更改这个默认值)就会从幸存区晋升到老年代。其实当新生代空间不足,大对象也可能直接进入老年代。
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾回收器
- Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( Stop The World" ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
- ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
- 4.3 Parallel Scavenge 收集器
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。
-
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
-
对 CPU 资源敏感;
-
无法处理浮动垃圾;
-
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
-
G1垃圾回收器
G1(Garbage First)是一款主要面向服务端应用的垃圾收集器,JDK 9发布之日,G1宣告取代ParallelScavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
类加载
类加载过程
类的生命周期 :加载连接初始化使用卸载
类的加载过程:加载连接初始化
- 加载
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
- 连接
分为验证,准备,解析
-
- 验证
文件格式验证 验证是否符合 Class 文件格式的规范,允许进入方法区
元数据验证 对字节码描述的信息进行语义分析以确保符合 Java 语言规范
字节码验证 保证方法执行的安全性,StackMapTable 将字节码验证的类型推导转变为类型检 查,JDK6 将校验辅助措施挪到了 Javac 编译器里进行
符号引用验证 验证该类是否缺少或者被禁止访问它依赖的某些外部类、 方法、 字段等资源,以确 保确保解析行为能正常执行
- 准备
为静态变量分配内存并赋初始值 (零值),final static 在 javac 的时候,字段表集合中会有一个 ConstantValue 的属性,那么在准备阶段就会被赋期望的值,其余的会存放在< clinit >中,在类初始化 阶段才会被赋期望值
- 解析
将符号引用被替换为直接引用并对其可访问性进行检查,需要在 getfield,putfield,getstatic, putstatic 等指令之前执行 解析主要针对 类或接口、 字段、 类方法
-
初始化
初始化阶段是执行初始化方法 <clinit> ()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
双亲委派模型
先介绍一下类的加载器
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类
介绍一下双亲委派模型
三种类加载器以组合方式形成父子关系,如果一个类加载器收到了类加载的请求, 它首先不会自己去尝 试加载这个类,而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有 的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载
打破双亲委派模型
亲委派模型的三次破坏 对于 JDK2 以前的代码做出的妥协,双亲委派模型在 JDK 1.2 之后才被引入,之前已经有代码重写 了loadClass方法 JNDI 会对资源进行查找和集中管理,需要调用 SPI代码,但是 JNDI 通过启动类加载器记载不认识 ClassPath 路径的上的 SPI 代码,故引入了线程上下文类加载器 追求程序的动态性,如热部署