JVM内存分配和垃圾收集
Posted LouD_dm
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM内存分配和垃圾收集相关的知识,希望对你有一定的参考价值。
1.内存分配
JVM运行时数据区分为5个区域。
程序计数器、Java虚拟机栈、本地方法栈是线程私有
Java堆、方法区是线程共享区域
程序计数器
程序计数器是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复都是通过这个计数器完成的。
由于Java虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的,一个处理器都只会执行一个线程的指令。为了线程切换后能恢复到正确的执行位置,每个线程需要有一个单独的程序计数器。每个线程互不影响,称这类内存区域为线程私有的内存区域。
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同。Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,对应一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用、和returnAddress类型(指向了一条字节码指令的地址)
本地方法栈
本地方法栈和Java虚拟机栈类似,只不过虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为了虚拟机用到的本地(Native)方法服务
Java堆
Java堆是虚拟机所管理的内存中最大的一块, 并且是所有线程共享的一块内存区域。java虚拟机规范中描述“所有的对象和数组都应在堆内分配内存”,随着即时编译器的进步,栈上分配,标量替换优化手段导致在堆上分配不是那么绝对了。
Java堆是垃圾收集器管理的内存区域,也被称为“GC堆”。现代垃圾收集器大多基于分代收集来设计的,主要有“新生代”、“老年代”、“永久代”,新生代又包含“Eden空间”、“From Survivor空间”、“To Survivor空间”,当然也有不基于分代收集的垃圾收集器。
方法区
方法区和Java堆一样是所有线程共享的一个区域,用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,有个别名称为“非堆”。
之前的JDK8以前有永久代的概念,HotSpot准备把分代设计扩展到方法区,也就是使用永久代代替方法区。因为有着永久代的内存限制,容易遇到内存泄漏的问题。java8废弃了永久代,采取直接内存
运行常量池
运行常量池是方法区的一部分。class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息为常量表,存放编译期生成的各种字面量和符号引用,这部分信息会在类加载之后放入常量池。
运行时常量池的一个重要特征是具备动态性,运行期间也可以将常量放入常量池,常量池方法区的一部分回收方法区的内存限制。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,这部分内存被频繁使用,也有可能导致OutOfMemoryError异常出现。jdk1.4加入的nio类 ,引入了通道(channel)和缓冲区(buffer)的io方式,它可以使用native函数库直接分配堆外内存,通过一个存储在java堆里的DirectByteBuffer对象作为这块内存的引用进行操作,这样在一定场景下能提高性能,因为避免了java堆和native堆里来回复制数据。
2. 垃圾收集算法
2.1 如何判断对象是否可以回收
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用,计数器加一,否则计数器减一。引用计数法虽然占用一些额外的内存进行计数,原理简单,判定效率也很高,主流java虚拟机没有使用这个方法,缺点无法解决对象相互循环引用的问题。
可达性分析算法
主流商用程序语言的内存管理系统都是使用可达性分析算法来判断对象是否存活,这个算法的基本思路是通过一系列的“GC Goots”的根对象作为起始节点集,从这些节点开始,,根据引用关系向下搜索,搜索过程所走过的路径被称为引用链,如果这个对象到GC Root没有引用链相连就证明对象不可能在被使用了。
Java中可作为GC Root的对象包含:
虚拟机栈中应用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等
在方法区静态属性引用的对象,譬如java类中的引用类型静态变量。
在方法区常量引用的对象,譬如字符串常量池里的引用
2.2 垃圾回收算法
标记清除
标记复制
标记整理
3.垃圾收集器
Serial [ˈsɪəriəl] 收集器
ParNew收集器
Parallel Scavenge [ˈpærəlel] [ˈskævɪndʒ]收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
Garbage Fiest(G1)收集器
Shenandoah收集器
4.如何选择垃圾收集器
JVM内存分配和垃圾收集策略
java内存区域
程序计数器
因为java可以多线程并发执行,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器。记录正在执行的虚拟机字节码指令的地址。
这个区域不会产生内存溢出异常。
栈
java虚拟机栈
栈中主要存放了编译期可知的四类八种基本数据类型存(逻辑型 boolean、文本型char、整数型byte、short、int、float、浮点数型double、long),对象引用类型,和对象引用类型(reference)。
本地方法栈
本地方法栈和java虚拟机栈所发挥的作用非常相似,他们之前的区别是虚拟机栈为虚拟机执行java方法服务。而本地方法栈是为虚拟机使用到的Native方法服务。
-Xss参数可以设置本地方法栈的内存上限。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
程序中 递归如果找不到出口也会抛出此异常(不断的进行入栈操作)
堆
用于存放对象的实列
可通过-Xms和Xmx来扩展堆的大小。因为现在的收集器基本都采用分代收集算法,所以java堆中还可以细分:新生代和老年代
如果在堆中没有内存完成实列分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。-Xmx可以设置堆内存的上限 -Xms 可以设置内存初始化的大小
方法区(永久代、也叫非堆)
用于储存已被虚拟机加载的类的信息(编译后的class)、常量、静态变量、即时编译器编译后的代码等数据,Class在被Loader时就会被放到方法区(PermGen space)中
垃圾收集行为在这个区比较少出现的,有点类似它的名字,永久代
当方法区无法满足内存分配需求时,将抛出OutOfMemoryErro(后面会跟PermGen space字符串)异常。-XX:MaxPermSize可以设置方法区的上限。
运行时常量池(方法区的一部分)
用于存放编译期生成的各种字面量和符号的引用,既然运行时常量池是方法区的一部分,自然收到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
JAVA的String声明的变量就是放在运行时常量池中比如String str="test"; 。但是 String str=new String("test");是new一个对象 是放在堆中
每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。
如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串
以上
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。可通过-Xmx和-XX:MaxPermSize可以设置内存和方法区上限。-Xmx2048m -XX:MaxPermSize=512m
把-Xmx 和-Xms大小设置一样内存大小,可以提高JVM的运行性能(取消掉伸缩区)
JVM垃圾收集算法
标记——清除算法
首先标记出所有需要回收的对象,在标记完成后统一回收。
缺点:这种回收算法会产生大量不连续的内存碎片。以后程序运行过程中产生较大的对象时,无法找到足够连续的内存 而不得不提前触发另一次垃圾收集动作。
复制算法
将内存按容量划分大小相等的两块,每次只使用一块。当这一块的内存用完了,就将还存活的对象复制到另外一块内存上面。
缺点:将内存的使用率缩小了一半
标记——整理算法
首先标记出所有需要回收的对象,然后让所有存货的对象都向一端移动。然后直接清理掉端边界意外的内存
分代收集算法
我们jdk采用的应该就是分代收集算法。根据对象存货周期的不同将内存划分为几块,针对每一块使用不同的算法
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要对少量存活的对象复制就可以进行收集。
老年代中,因为对象存货率高,没有额外空间对它进行分配担保,就需要使用 标记——整理或者标记——清楚算法。
基于JDK命令行工具的监控
更新中。。。。。。
以上是关于JVM内存分配和垃圾收集的主要内容,如果未能解决你的问题,请参考以下文章