《深入理解Java虚拟机(第二版)》学习笔记

1、Java技术体系:

(1)Java程序设计语言
(2)各种硬件平台上的Java虚拟机
(3)Class文件格式
(4)Java API类库
(5)来自商业机构和开源社区的第三方Java类库

2、Java所管理的内存-运行时数据数据区域:
(1)程序计数器Program Counter Register
(2)Java虚拟机栈VM Stack:线程私有的,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成都对应一个栈帧在虚拟机栈中入栈到出栈的过程。
(3)本地方法栈Native Method Stack:虚拟机使用到的Native方法服务。
(4)Java堆Heap:是各个线程共享的内存区域,唯一目的是存放对象实例。
(5)方法区Method Area,是各个线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
(6)运行时常量池Runtime Constant Pool,是方法区的一部分。

3、对象的创建
1)虚拟机遇到一条new指令
2)检查是否能在常量池中定位到一个类的符号引用,检查这个符号代表的类是否已被加载、解析和初始化
3)类加载检查通过后,为新生对象分配内存。
4)内存分配完成后,虚拟机先进行内存空间初始化为零值,并对对象进行必要的设置,如对象头信息的设置。
注:执行new指令后会接着执行init方法。

4、对象在内存中存储的布局:
分3块区域:对象头、实例数据和对齐填充。
(1)对象头(Header):分两部分①用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;②类型指针(对象指向它的类元数据的指针),虚拟机通过这个指针来确定这个对象是哪个类的实例。
(2)实例数据(Instance Data):对象真正存储的有效信息,从父类继承下来的和子类中定义的,都会记录;存储顺序受虚拟机分配策略和字段在Java源码中定义顺序影响。
(3)对齐填充(Padding):自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍;当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

5、对象的访问定位:
对象访问方式取决于虚拟机实现而定,主流的访问方式有使用句柄和直接指针两种
Java程序需要通过栈上的reference数据在操作堆上的具体对象。reference类型在Java虚拟机规范中只规定了一个指向对象的引用。
(1)使用句柄访问:在Java栈本地变量表的reference中存储的是对象的句柄地址,Java堆中会划分一块内存作为句柄池,句柄中包括了对象实例数据与类型数据各自的具体地址信息。对象实例数据的指针指向的是Java堆里实例池的对象实例数据;而对象类型数据的指针指向的是方法区里的对象类型数据。

(2)使用直接指针访问:reference中存储的直接就是对象地址。

6、StackOverflowError、OutOfMemoryError异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;
如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常。

(1)Java堆溢出:
用eclipse工具,配置VM arguments:
-verbose:gc -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

测试代码:

public class HeapOOM {
    static class OOMObject {}
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

抛出异常:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at xcj.homepage.HeapOOM.main(HeapOOM.java:15)

处理Java堆内存问题的思路:先分清是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可通过工具查看泄露对象的类型信息及GC Roots引用链的信息,来定位出泄露代码的位置。
如果不存在泄露,也就是内存中对象必须存活着,就检查虚拟机的堆参数(-Xmx与-Xms),看是否可以调大,同时在代码上检查是否存在某些对象生命周期过程、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值.如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小

(2)虚拟机栈和本地方法栈溢出:
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
多线程下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多的线程。
注:操作系统分配给每个进程的内存是有限制的。如32位的win为2G,主要由虚拟机可设置参数Xmx(最大堆容量)+MaxPermSize(最大方法区容量),以及虚拟机栈和本地方法栈瓜分。
每个线程分配到的栈容量越大,能建的线程数就越小。

测试代码:

public class JavaVMStackSOF {
	private int stackLength = 1;
	public void stackLeak() {
		stackLength++;
		stackLeak();
	}
	public static void main(String[] args) {
		JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
		
		try {
			javaVMStackSOF.stackLeak();
		} catch (Exception e) {
			System.out.println("stackLength:" + javaVMStackSOF.stackLength);
			throw e;
		}
	}
}

抛出异常:
Exception in thread "main" java.lang.StackOverflowError
at xcj.homepage.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)

(3)方法区和运行时常量池溢出:
运行时常量池是方法区的一部分,通过设置-XX:PermSize和-XX:MaxPermSize限制方法区大小,也是限制了其中的常量池容量。

(4)本机直接内存溢出:
DirectMemory容量可通过-XX᧶MaxDirectMemorySize指定,默认与Java堆最大值一样。
DirectMemory导致的内存溢出,在Heap Dump文件中不会看到明显异常。
如果OutOfMemoryError异常后,Dump文件很小,而程序中又直接或间接使用了NIO,可以检查下是不是这方面的原因。

7、JVM -verbose参数详解
(1)java –verbose:gc
在虚拟机发生内存回收时在输出设备显示信息,格式如下: [Full GC 256K->160K(124096K), 0.0042708 secs] 该参数用来监视虚拟机内存回收的情况。
(2)java -verbose:class
在程序运行的时候有多少类被加载!你可以用verbose:class来监视,在命令行输入java -verbose:class XXX (XXX为程序名)你会在控制台看到加载的类的情况。
(3)java –verbose:jni
-verbose:jni输出native方法调用的相关情况,一般用于诊断jni调用错误信息。
在虚拟机调用native方法时输出设备显示信息,格式如下: [Dynamic-linking native methodjava.lang.Object.registerNatives ... JNI] 该参数用来监视虚拟机调用本地方法的情况,在发生jni错误时可为诊断提供便利。

8、判断对象是否存活
通过可达性分析算法(Reachability Analysis)来判定对象是否存活。
思路:"GC Roots"对象作为起始点,向下搜索,当一个对象到GC Roots没有任何引用链(搜索走过的路径称为引用链)相连时或者说从GC ROOTS到这个对象不可达,即不可用,为可回收对象。

GC Roots对象包括以下几种:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
(2)方法区中类静态属性引用的对象。
(3)方法区中常量引用的对象。
(4)本地方法栈中JNI(Native方法)引用的对象。

9、引用:
分为强引用、软引用、弱引用、虚引用4种,引用强度依次逐渐减弱。
Java中提供这四种引用类型主要有两个目的:
第一是可以让程序员通过代码的方式决定某些对象的生命周期;
第二是有利于JVM进行垃圾回收。
强引用(Strong Reference):在程序代码中普遍存在的,或者说创建一个对象并把这个对象赋给一个引用变量。如Object obj=new Object();String str ="hello";
强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用(Soft Reference):描述一些还有用但非必须的对象。被软引用关联的对象只有在内存溢出异常时才会被回收。
弱引用(Weak Reference):描述一些非必须的对象。被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
虚引用(Phantom Reference):不影响对象的生命周期,在任何时候都可能被垃圾回收器回收。

 

相关书籍《深入理解Java虚拟机(第二版)》

posted @ 2019-07-02 17:20  时光编辑师  阅读(626)  评论(0编辑  收藏  举报