JVM运行时数据区及判断对象是否该回收
参考文献:周志明.深入理解Java虚拟机 JVM高级特性与最佳实践第2版 [M]北京:机械工业出版社 2016.5;
目录:
1、JVM运行时数据区:
2、CAS:
3、判断对象是否存活:
4、方法区回收:
5、强引用、软引用、弱引用、虚引用:
6、eclipse.ini的配置:
1、JVM运行时数据区:
(1)方法区(Method Area),别名(Non-Heap,非堆),也称为永久代:
A)线程共享;
B)作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码、元数据等数据。
C)在Java虚拟机规范中规定了抛出异常错误情况:
OutOfMemoryError错误:如果在方法区中无法满足内存分配需求时,将会抛出。
D)Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
E)概念理解:
①元数据:元数据是指用来描述数据的数据,即描述代码间的关系,或者代码与其他资源(例如数据库表)之间内在联系的数据。在JDK5.0引入了Annotation的概念来描述元数据。在java中,元数据以标签的形式存在于Java代码中,元数据标签的存在并不影响程序代码的编译和执行。
②即时编译器:JIT(Just-In-Time Compiler),一个把Java的字节码转换成可以直接发送给处理器的指令的程序。
③运行时常量池(Runtime Constant Pool):方法区的一部分,相对于Class文件常量池来说是具备动态性,在运行期间将新的常量放入池中,比如String类的intern()方法。
(2)虚拟机栈(VM Stack):
A)线程私有;
B)作用:生命周期与线程相同,虚拟机栈是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
C)局部变量表存放了基本数据类型、对象引用类型和returnAddress类型(指向了一条字节码指令的地址)。
D)在Java虚拟机规范中,该区域存在两种异常错误情况:
StackOverFlowError错误:如果线程请求的栈深度大于虚拟机所允许的深度,当应用程序递归太深而发生堆栈溢出时,抛出该错误。
OutOfMemoryError错误:如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,因为内存溢出或没有可用的内存提供给垃圾回收器时,Java虚拟机无法分配一个对象,这时抛出该错误。
(3)本地方法栈(Native Method Stack):
A)线程私有;
B)作用:为虚拟机使用到的Native方法服务。
C)概念理解:
Native方法:一个Java调用非Java代码的接口。
A native method is a Java method whose implementation is provided by non-java code.
(4)堆(Heap):
A)线程共享;
B)作用:用于存放对象实例。
Java虚拟机规范中的描述:所有的对象实例以及数组都要堆上分配内存。
C)Java堆是垃圾收集器管理的主要区域。
D)Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。可扩展,通过-Xmx和-Xms控制。
E)抛出异常错误情况:
OutOfMemoryError错误:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出。
(5)程序计数器(Program Counter Register):
A)线程私有;
B)作用:一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
C)线程之间的程序计数器是私有的,互不影响的。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,多核处理器中的一个内核都只会执行一条线程中的指令。
2、CAS:
(1)CAS是乐观锁思想的一种实现方式,Compare and Swap。
(2)CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
(3)并发情况下对象创建在虚拟机中修改一个指针所指向的位置时非线程安全的解决方法:
①采用CAS配上失败重试的方式保证更新操作的原子性。
②把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
3、判断对象是否存活:
(1)引用计数算法(Reference Counting):
①思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的。
②缺陷:很难解决对象之间相互循环引用的问题。
③示例程序:
package com.remoa.gc;
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
垃圾回收类型:GC和Full GC
a)GC一般为堆空间某个区发生了垃圾回收
b)Full GC基本都是整个堆空间及永久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。
执行ReferenceCountingGC类的main方法时,查看日志,得到
Java HotSpot(TM) 64-Bit Server VM (25.121-b13) for windows-amd64 JRE (1.8.0_121-b13), built on Dec 12 2016 18:21:36 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4101080k(1823232k free), swap 7946124k(3722224k free)
CommandLine flags: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=1073741824 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2017-08-03T19:54:33.063+0800: 1.653: [GC (Allocation Failure) [PSYoungGen: 65536K->10744K(76288K)] 65536K->19899K(251392K), 0.0307685 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
2017-08-03T19:54:45.643+0800: 14.233: [GC (GCLocker Initiated GC) [PSYoungGen: 61129K->10744K(141824K)] 70285K->35266K(316928K), 0.0457428 secs] [Times: user=0.09 sys=0.03, real=0.05 secs]
2017-08-03T19:54:45.712+0800: 14.302: [GC (Metadata GC Threshold) [PSYoungGen: 14404K->10733K(141824K)] 38926K->35501K(316928K), 0.0291536 secs] [Times: user=0.06 sys=0.03, real=0.03 secs]
2017-08-03T19:54:45.742+0800: 14.332: [Full GC (Metadata GC Threshold) [PSYoungGen: 10733K->0K(141824K)] [ParOldGen: 24768K->32176K(175104K)] 35501K->32176K(316928K), [Metaspace: 19817K->19817K(1069056K)], 0.1809813 secs] [Times: user=0.58 sys=0.00, real=0.18 secs]
2017-08-03T19:54:47.958+0800: 16.549: [GC (Metadata GC Threshold) [PSYoungGen: 73568K->10728K(262144K)] 105745K->52026K(437248K), 0.0235766 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]
2017-08-03T19:54:47.982+0800: 16.573: [Full GC (Metadata GC Threshold) [PSYoungGen: 10728K->0K(262144K)] [ParOldGen: 41298K->44743K(175104K)] 52026K->44743K(437248K), [Metaspace: 33113K->33113K(1081344K)], 0.1755330 secs] [Times: user=0.58 sys=0.02, real=0.18 secs]
2017-08-03T19:54:53.391+0800: 21.981: [GC (Metadata GC Threshold) [PSYoungGen: 151427K->10723K(272896K)] 196171K->80447K(448000K), 0.0476548 secs] [Times: user=0.19 sys=0.00, real=0.05 secs]
2017-08-03T19:54:53.439+0800: 22.029: [Full GC (Metadata GC Threshold) [PSYoungGen: 10723K->0K(272896K)] [ParOldGen: 69723K->70601K(204288K)] 80447K->70601K(477184K), [Metaspace: 54720K->54720K(1101824K)], 0.4781368 secs] [Times: user=1.66 sys=0.00, real=0.48 secs]
2017-08-03T19:54:59.838+0800: 28.428: [GC (Allocation Failure) [PSYoungGen: 262144K->37867K(286720K)] 332745K->119056K(491008K), 0.0905302 secs] [Times: user=0.20 sys=0.08, real=0.09 secs]
2017-08-03T19:55:00.443+0800: 29.033: [GC (Metadata GC Threshold) [PSYoungGen: 71411K->24366K(299008K)] 152600K->105563K(503296K), 0.0356737 secs] [Times: user=0.09 sys=0.00, real=0.03 secs]
2017-08-03T19:55:00.478+0800: 29.069: [Full GC (Metadata GC Threshold) [PSYoungGen: 24366K->0K(299008K)] [ParOldGen: 81197K->77799K(236544K)] 105563K->77799K(535552K), [Metaspace: 90513K->90513K(1134592K)], 0.2971272 secs] [Times: user=0.94 sys=0.00, real=0.30 secs]
2017-08-03T19:56:43.184+0800: 131.774: [GC (System.gc()) [PSYoungGen: 125594K->53555K(270848K)] 203522K->131491K(507392K), 0.0702808 secs] [Times: user=0.27 sys=0.00, real=0.07 secs]
2017-08-03T19:56:43.254+0800: 131.844: [Full GC (System.gc()) [PSYoungGen: 53555K->0K(270848K)] [ParOldGen: 77935K->115080K(236544K)] 131491K->115080K(507392K), [Metaspace: 126259K->125082K(1169408K)], 0.7623347 secs] [Times: user=2.31 sys=0.03, real=0.76 secs]
2017-08-03T19:57:43.190+0800: 191.780: [GC (System.gc()) [PSYoungGen: 4557K->128K(195072K)] 119638K->115208K(431616K), 0.0083615 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2017-08-03T19:57:43.198+0800: 191.789: [Full GC (System.gc()) [PSYoungGen: 128K->0K(195072K)] [ParOldGen: 115080K->113283K(236544K)] 115208K->113283K(431616K), [Metaspace: 125135K->125135K(1169408K)], 0.8877026 secs] [Times: user=2.69 sys=0.00, real=0.89 secs]
新生代中通过垃圾回收,由4557K变为128K,生成的两个对象objA和objB每个对象都存在一个占2048K的bigSize,说明两个对象都已经被回收,虚拟机没有因为这两个对象互相引用就不回收它们,虚拟机不是通过引用计数算法来判断对象是否存活的。
(2)可达性分析算法(Reachability Analysis):
①思想:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
②在Java语言中,可以作为GC Roots的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象。
③不可达的对象,要真正回收,需要至少经历两次标记过程:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。
若对象判定为有必要执行finalize()方法,则对象将被放置在一个F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
④GC对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。
⑤示例代码:
package com.remoa.gc;
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("The Object is still alive.");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
SAVE_HOOK = this;
System.out.println("finalize method executed!");
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为finalize方法优先级很低,所以暂停0.2秒来等待它。
Thread.sleep(200);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("The Object is dead.");
}
//对象拯救自己失败,因为对于任何给定对象,Java虚拟机最多只调用一次finalize方法。
SAVE_HOOK = null;
System.gc();
Thread.sleep(200);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("The Object is dead.");
}
}
}
finalize()方法:当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法。对于任何给定对象,Java虚拟机最多只调用一次finalize方法。
运行结果:
图3.1 运行结果截图
解释说明:
2017-08-03T19:44:14.849+0800: 1879.417: [GC (System.gc()) [PSYoungGen: 9328K->640K(245248K)] 90327K->81638K(763392K), 0.0103835 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2017-08-03T19:44:14.849+0800(当前时间戳):
1879.417(时间戳):
[GC(代表Young GC) (System.gc())
[PSYoungGen 新生代GC收集器:
9328K(新生代垃圾回收前的大小)->
640K(新生代回收后的大小)
(245248K)(新生代总大小)]
90327K(整个堆回收前的大小)->
81638K(整个堆回收后的大小)
(763392K)(堆的总大小),
0.0103835 secs(回收时间)]
[Times: user=0.00(用户耗时)
sys=0.00(系统耗时),
real=0.01 secs(实际耗时)]
4、方法区回收:
(1)永久代中的垃圾收集主要回收两部分内容:
废弃常量和无用的类。
(2)回收常量:没有任何地方引用了这个常量。
(3)回收无用的类:满足三个条件:
①该类所有实例都已经被回收,Java堆中不存在该类的任何实例。
②加载该类的ClasssLoader已经被回收。
③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
5、强引用、软引用、弱引用、虚引用:
(1)强引用:引用指向new出来的对象。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
(2)软引用:描述一些还有用但并非必须的对象。软引用对象最常用于实现内存敏感的缓存。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。SoftReference类实现软引用。
(3)弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。弱引用最常用于实现规范化的映射。WeakReference类实现弱引用。
(4)虚引用:也称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。PhantomReference类实现虚引用。虚引用最常见的用法是以某种可能比使用Java终结机制更灵活的方式来指派pre-mortem清除操作。
6、eclipse.ini的配置:
-startup
plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.300.v20150602-1417
-product
org.eclipse.epp.package.jee.product
--launcher.defaultAction
openFile
;最大堆大小。
--launcher.XXMaxPermSize
256M
;eclipse.ini是一个文本文件,其内容相当于在Eclipse运行时添加到eclipse.exe之后的命令行参数
-showsplash
org.eclipse.platform
;最大非堆内存(方法区)的大小,默认是物理内存的1/4(eclipse.exe启动的时候设置的参数)
--launcher.XXMaxPermSize
256m
--launcher.defaultAction
openFile
--launcher.appendVmargs
;所有在-vmargs之后的参数将会被传输给JVM,所有如果对Eclipse设置的参数必须写在-vmargs之前
-vmargs
-Dosgi.requiredJavaVersion=1.7
;虚拟机占用系统的最小内存,堆空间初始分配大小为256M
;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制
-Xms256m
;虚拟机占用系统的最大内存,堆空间最大为1024M,按需分配
;默认空余堆内存小于40%时,JVM会增大堆直到-Xmx的最大限制
-Xmx1024m
;开启打印垃圾回收日志
-verbose:gc
;设置垃圾回收日志文件的输出路径,这里保存在桌面
-Xloggc:C:/Users/邓小艺/Desktop/eclipse_gc.log
;打印垃圾回收时间信息时的时间戳(日期的形式,如2017-08-03 T16:50:20 234+800)
-XX:+PrintGCDateStamps
;打印垃圾回收详情
-XX:+PrintGCDetails
;打印在进行GC的前后堆的信息
;-XX:+PrintHeapAtGC