Java运行时内存结构:
1. 线程共享:
堆:虚拟机启动时创建。用于存放对象实例,几乎(因为逃逸分析技术和栈上分配技术的优化,堆区不再是对象实例存放的唯一内存区)所有对象实例都在堆里分配内存。
堆是GC垃圾回收的重点区域。
由于对象生命周期的两极化,堆又可分为新生代和老年代。新生代又分为Eden空间,To Survivor空间、From Survivor空间。
可以通过选项“Xms/Xmx”来设置堆内存大小。Xms用于设置堆的初始内存,Xmx用于设置堆的最大内存(当内存空间超过Xmx所设置的最大内存,会抛出OutOfMemoryError异常)
在实现时,堆可以是固定大小的也可以是可扩展的。
方法区:用于存储已加载的类信息,常量,静态变量等。很多人将方法区称作永久代,但是二者并不等价。只是HotSpot虚拟机中用永久代来实现方法区而已。
尽管方法区并不会像堆中频繁的执行内存回收,但也并不意味着方法区中的数据就永远不会被回收。
可以通过选项“PermSize/MaxPermSize”来分别设置方法区的初始内存和最大内存,当所需内存超过最大内存时会抛出OutOfMemoryError异常。
方法区可以是固定大小的也可以是可扩展的。
运行时常量池:属于方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
字节码文件除了包含类的信息、字段、方法以及借口等信息,还有一项就是常量池表。运行时常量池就是常量池表运行时的表示形式。
当所需内存超过方法区的最大内存时会抛出OutOfMemoryError异常。
2. 线程私有:
Java(JVM)栈:
JVM栈用于存储栈帧。生命周期与线程的生命周期同样长。
栈帧包括局部变量表、操作数栈、方法出口等信息。
局部变量表包括各种原始数据类型,对象引用,returnAddress.(字节码指令的地址)
一个方法从调用到返回就是栈帧从进栈到出栈的过程。
如果线程请求的栈容量大于栈的最大内存,则会抛出StackOverflowError异常。
若扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常。
本地方法栈:用于支持本地方法(如用C/C++编写的方法)的执行。
同样会抛出StackOverflowError异常和OutOfMemoryError异常。
PC计数器:当前执行的字节码指令地址。Java解释器通过改变PC计数器的值来获取将执行的下一条字节码指令地址。(若正在执行的时本地方法,则PC计数器的值为0)内存区中唯一一个没有明确规定要抛出OutOfMemoryError异常的内存区。
垃圾回收:
GC(Garbage Collect)是java自动内存管理机制的具体实现。
GC的主要任务为内存的动态分配和垃圾回收。
分代收集策略是什么?
JVM中尤其是Java堆区中的对象生命周期具有两极化,所以应该采取不同的垃圾收集策略。
评价GC的工作性能有哪些指标?
吞吐量:程序运行的时间/(程序运行的时间+垃圾回收的时间)
暂停时间:执行垃圾回收时,正在工作的线程被暂停的时间
堆空间
收集频率:相对于程序的执行,垃圾收集发生的频率
什么时候宣判对象死亡?
当一个对象已经不被任何一个存活的对象引用时,就宣判这个对象已经死亡
判断对象死亡有什么方法?
垃圾标记算法:
1. 引用计数法
为每一个对象创建一个私有的引用计数器,当目标对象被其他存活对象引用时,引用计数器+1,不在引用时则-1.当目标对象的计数器值为0时,则宣判对象已死亡。
缺点:当一些已死亡的对象不在被存活的对象引用时,但是它们彼此之间却存在着引用关系,则这些对象的引用计数器值永远不会为0,这样垃圾收集器就无法回收这些对象,最终可能引发内存泄露。
2. 根搜索算法
能被根对象直接或者间接连接的对象才是存活对象。
根对象集合中内容:
Java栈中的对象引用
方法区中类静态属性的对象引用
本地方法栈中的对象引用
运行时常量池中的对象引用
与一个类对应的唯一数据类型的Class对象
优点:有效解决了引用计数法中一些已经死亡的对象但因为存在相互引用而无法被有效标记的问题
垃圾回收算法:
1. 标记-清除算法:分为垃圾标记和内存释放阶段。
缺点:执行效率低下,而且由于被释放的对象所占内存空间的不连续,导致垃圾回收后会产生一些不连续的内存空间,即内存碎片,从而导致没有足够的内存空间分配给占用内存空间较大的对象。
2. 复制算法(广泛引用于新生代):
当执行一次MinorGC(新生代中的垃圾回收)时,Eden中存活的对象会被复制到To Survivor空间中,同样地,From Survivor空间中经历过一次MinorGC后存活下来的对象也会被复制到To Survivor空间中。(两种情况下不会。1,存活的对象的分代年龄超过-XX:MaxTenuringTreshold所指定的阈值时,将会直接晋升到老年代中2.ToSurvivor空间已满,存活的对象也是直接晋升到老年代中)之后,GC就可以对死亡的对象进行一次FullGC,释放掉无用的对象。接下来From和To空间将会互换位置。
3. 标记-压缩算法:
在标记-清除算法的基础上进行了改进。在垃圾标记阶段后,将所有存活的对象都移动到一块连续的空间中,执行一次FullGC(老年代的垃圾回收),释放掉无用对象的空间后,已用和未用的空间都各自一边,彼此之间维系这一个记录下次内存分配起始点的指针,当为新对象分配内存时,使用指针碰撞技术,将新对象分配在第一个空闲的位置上,并修改指针的偏移量。
垃圾收集器:
几个概念:
串行回收:当有多个CPU可用时,只会有一个CPU被用于垃圾回收,并且在执行垃圾回收时,正在工作的线程会被暂停,直至垃圾回收完成后才恢复工作。
并行回收:可以有多个CPU进行垃圾回收,但是在垃圾回收过程中,正在工作中的线程也会被暂停,直至垃圾回收完成。
Stop-the-world机制:垃圾收集器会在垃圾回收过程中暂停所有正在工作的线程,直至内存回收完成后才会恢复之前被暂停的线程。
Serial收集器:采用串行回收,复制算法,Stop-the-world机制的方式执行内存回收。
Serial Old收集器:采用串行回收,标记-压缩算法,Stop-the-world机制。
在JVM局限在单个CPU的环境下,采用Serial+Serial Old收集器执行Client模式下的内存回收会是不错的选择。
ParNew收集器:Serial收集器的多线程版本。同样采用复制算法,Stop-the-world机制,只不过是并行回收。(相比Serial收集器,ParNew收集器的优势是在多CPU宿主环境中)在某些注重低延迟的应用场景下,使用ParNew+CMS收集器是执行Server模式下的内存回收几乎是最佳选择。
Parallel收集器:与ParNew收集器一样采用并行回收,复制算法,Stop-the-world机制。不同的是,Parallel收集器能更好地控制程序吞吐量的大小。
通过选项“-XX:GCTimeRatio”设置执行内存回收的时间占所占JVM运行的总时间,也就是控制GC的回收频率。
通过选项“-XX:MaxGCPauseMillis”设置执行内存回收时“Stop-the-world”机制的暂停时间阈值。Parallel Old收集器采用并行回收,标记-压缩算法,“Stop-the-world”机制执行老年代的内存回收。在程序吞吐量优先的场景下,用Parallel+Parallel Old执行Server模式下的内存回收是不错的选择。
CMS(“Concurrently-Mark-Sweep”)收集器:基于并行回收,标记-清除算法,“Stop-the-world”机制的一款优秀的老年代垃圾收集器。也称作“Mostly-Concurrently”收集器,为并发而生,低延迟是它的优势。
CMS垃圾收集器的内存回收主要分为以下几个阶段:
1. 初始标记:标记处内存中那些被根对象所连接的对象是否可达
2. 并发标记:并发标记处那些不可达的对象
3. 再次标记:由于并发标记过程中程序的工作线程与垃圾收集线程同时运行或者交叉运行,所以无法保证并发标记阶段所标记的对象的引用是否已经遭到更改,所以需要再次标记
4. 并发清除:释放无用对象所占用的内存空间
CMS采用空闲列表法来为新对象分配内存。JVM负责维护一个列表,其中的内容是当前内存空间中空闲内存块的坐标,当为新对象分配内存时,会从列表中定位到一个与新对象大小一致的连续内存空间用于存放新对象。
由于内存碎片存在的弊端,可以通过选项“-XX:CompactAtFullCollection”用于指定在执行FullGC后是否对内存空间进行压缩整理。(但是由于压缩整理过程无法并发执行,会导致线程暂停时间更加长)
可以通过选项“-XX:CMSFullGCs-BeforeCompaction”用于设置在执行多少次FullGC后对内存空间进行压缩整理
G1收集器:一款基于并行和并发,低延迟,暂停时间更加可控的区域化分代式垃圾收集器。
G1并没有采用传统的物理隔离的新生代和老年代,而仅仅是在逻辑上。G1将堆内存空间分成2048个大小相同的独立的Region空间。这样分的好处是可以提升GC的收集效率,缩短Stop-the-world的暂停时间。
相比Parallel收集器而言,G1收集器能更好地控制暂停时间。
虽然Parallel收集器可以通过选项“-XX:MaxGCPauseMillis”来控制暂停时间,但对于大内存空间的释放,其暂停时间并非是完全可控的,只能说控制在一定的范围内。
而G1收集器并非全堆扫描,而是优先释放占用内存较大的Region块,所以G1收集器的暂停时间更加可控。
G1收集器的内存回收执行过程可分为以下几个阶段:
1. 初始标记阶段:标记Root-Region
2. 根区域扫描:扫描Root-Region引用的一些老年代对象
3. 并发标记:找出整个Java堆区的活跃对象
4. 再次标记:完成整个堆区存活对象的标记
5. 清除阶段:先计算出所有的活跃对象并释放一些自由的Region,然后处理Remembed Set,最后并发重置空闲的一些Region块,并将它们放回空闲列表
6. 拷贝阶段:将存活的对象拷贝到未使用的Region块中
(除了并发标记阶段和清除阶段的第三个阶段外,其余都是基于Stop-the-world机制的)