0.【Java内存区域】
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
对象访问
1.【GC(垃圾回收)】
【堆区GC】
【方法区GC】
【垃圾收集算法】
【垃圾收集器】
【内存分配与回收策略】
2.【JVM性能监控与故障处理工具】
3.【JVM加载执行系列】
3.1 类文件结构
3.2 【虚拟机类加载机制】
3.3【类加载过程】
3.4 【类加载器】
线程共享区域:方法区(Method Area)、堆(Heap)
线程私有区域:虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、程序计数器(Program couter Register)
在Java内存运行时区域中,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随着线程生死。栈中的栈帧随着方法的进入和推出执行入栈和出栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这三个区域的内存分配和回收都具备确定性。
Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样,只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的就是这部分内存。
判断对象是否已死?
答:通过对象与GC Root之间是否有引用链存在来判断对象是否已死。这个过程会有2次标记过程,第1次标记会进行筛选,筛选的条件是此对象有没有必要执行finalize方法。
finalize方法是对象逃逸被回收的唯一一次机会。
PS:任何一个对象的finalize()方法只会被系统自动调用一次。如果对象面临下一次回收,他的finalize()方法不会再被执行
可以作为GC Root的对象有:
强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK=null;
public void isAlive() {
System.out.println("yes,i am still alive:");
}
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;//对象重新添加GC Root
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK=new FinalizeEscapeGC();
//obj escape dead first
SAVE_HOOK=null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK !=null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no,i am dead");
}
//obj escape dead second
SAVE_HOOK=null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK !=null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no,i am dead");
}
}
}
永久代的垃圾回收内容:废弃常量和无用的类。
判定一个类是否是“无用的类”:
PS:上图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能是交替执行),用户程序继续运行,而垃圾收集程序运行于另一个CPU上。
1.对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
-XX:+PrintGCDetails 这个参数告诉虚拟机在发生垃圾收集行为时,打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC 很频繁,一般回收速度比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随着至少一次的Minor GC(但非绝对,在Parallel Scavenge收集器策略里就有直接进行Major GC的策略选择过程)。比MinorGC 速度慢10倍以上。
2.大对象直接进入老年代:大对象即为需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组(byte[])。
-XX:PretenureSizeThreshold参数,只对Serial和Par New两款收集器有效。令大于这个设置值的对象直接进入老年代分配。目的就是避免在Eden区及2个Survivor区之间发生大量的内存拷贝。
3.长期存活的对象将进入老年代:JVM给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认是15岁),就会晋升到老年代。
-XX:MaxTenuringThreshold=1 and -XX:MaxTenuringThreshold=15 两种设置。
4.动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
5.空间分配担保:在发生Minor GC时,JVM会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。
这是Mac JDK1.8 bin目录下的:
1.JPS(Java Process Status Tools):
可以列出正在运行的JVM进程,并显示JVM执行主类(Main Class,main()函数所在的类)的名称,以及这些进程的本地JVM唯一ID(LVMID,local virtual Machine Identifier)。对于本地虚拟机进程而言,LVMID与OS的进程ID是一致的。
如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就只能依赖JPS命令显示主类的功能才能区分了。
2.jstat:虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,是运行期定位虚拟机性能问题的首选工具。
如果是本地虚拟机进程,vmid和LVMID是一致的,如果是远程虚拟机进程,格式为:
exp:
注:S0、S1:Survivor1/2,E:Eden,O:Old,M:未知,YGC/FGC:次数,GCT/YGCT/FGCT:时间
3.jinfo:Java配置信息工具--->实时地查看和调整虚拟机地各项参数。
4.jmap:(Memory Map for Java) java内存映像工具:用于生成堆转储快照(heapdump or dump文件),还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
5.jhat:JVM Heap Analysis Tool 虚拟机堆转储快照分析工具:与jmap搭配使用,来分析jmap生成的堆转储快照。
jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结后,可以在browser中查看。
6.jstack(Stack Trace for Java) Java堆栈跟踪工具 用于生成虚拟机当前时刻的线程快照(threaddump or Javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,目的是定位线程出现长时间停顿的原因,如死锁、死循环、请求外部资源导致的长时间等待。
代码编译:本地机器码==>字节码。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式:字节码,是构成平台无关性的基石。实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的。
Class文件是一组以8位字节为基础单位的二进制流。各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。只有2中数据类型:无符号数和表。无符号数属于基本的数据类型,以u1\u2\u4\u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以描述数字、索引引用、数量值或按照UTF-8编码构成字符串值;表是由多个无符号数或其他表作为数据想构成的符合数据类型,通常以“_info”结尾。
【魔数】:每个class文件的头4个字节,唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。第5个和第6个字节是此版本号(Minor Version),第7个和第8个是主版本号(Major Version)。Java版本号从45开始,JDK 1.7以上大于等于51.0。
紧接着主次版本号之后的就是常量池入口(表类型数据项目),主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量:如文本字符串、被声明为final的常量值等。
符号引用:1.类和接口的全限定名;2.字段的名称和描述符;3.方法的名称和描述符
Java代码在Javac编译的时候,并不像C、C++那样“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。
在常量池结束之后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或接口层次的访问信息,包含是类还是接口,是否定义public类型,是否定义为abstract类型,类是否被声明为final等。
访问标志之后,类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定这个类的继承关系。
字段表集合紧跟其后,字段表(field_info)用于描述接口或类中声明的变量,包含了类级变量或实例级变量,但不包含在方法内部声明的变量。字段的作用域(public、private、protected修饰符)、是类级别变量还是实例级变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从祝内存读写)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
方法表集合,如同字段表一样。但是方法里的Java代码经过编译器编译称字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面。与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的就是类构造器“
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需说明所占用的位数。
通过javap -verbose TestClass,为什么Args_size=1 和Locals = 1?
答:在任何实例方法中,都可以通过“this”关键字访问到此方法所属的对象。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个slot位来存放对象实例的引用,从1开始。如果inc()方法定义为static,那么Args_size=0;
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。这就是虚拟机的类加载机制。在Java语言里,类型的加载和连接过程都是在程序运行期间完成的,其动态扩展特性就是依赖运行期动态加载和动态连接这个特点实现。
加载---验证---准备---初始化---卸载这5个阶段的加载顺序是确定的,而解析阶段则不一定。
什么时候加载由虚拟机的具体实现来决定。对于初始化阶段,有且只有4种情况必需立即对类进行“初始化”(加载,验证、准备):
Demo:
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
上述类中的SuperClass.value的引用直接转化为该NotInitalization类对自身常量池的引用,这个两个类在编译成class好之后就不存在任何联系。
运行之后没有输出“superclass init”,说明没有触发SuperClass类的初始化阶段,它是一个虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
【接口加载过程区别】:当一个类在初始化时,要求其父类全部都已经初始化过了,但是接口不需要其父接口都完成初始化,什么时候用什么时候初始化。
【1.加载】 是Class Loading的一个阶段。在加载阶段:虚拟机需要完成以下3件事情:
【2.验证】: 是连接阶段的第一步。目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机安全
2.1 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
这个阶段的验证是基于字节流进行,经过这个阶段的验证之后,字节流才会进入内存的方法区中进行存储。
2.2 元数据验证:
2.3 字节码验证:是整个验证过程中最复杂的一个阶段,主要工作是进行数据流和控制流分析。任务是保证被校验类的方法在运行时不会作出危害虚拟机安全的行为。
2.4 符号引用验证:符号引用---->直接引用,这个转化动作将在连接的第3个阶段——解析阶段中发生。目的:确保解析动作能正常执行,如不能,则抛出一个java.lang.IncompatibleClassChangeError异常的子类。
符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,通常校验内容如下:
1.符号引用中通过字符串描述的全限定名是否能找到对应的类
2.在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
3.符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可以被当前类访问
4. 。。。。。。
【3. 准备阶段】为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
【4. 解析】 虚拟机将常量池内的符号引用替换为直接引用的过程,其关联关系如下:
【5. 初始化】 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源======初始化阶段是执行类构造器
把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让程序自己决定如何去获取所需的类。
对于任意一个类,都需要加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。也就是说,比较两个类是否“相等”。只有在这两个类是由同一个类加载器加载的前提下才有意义;否则,即使这两个类是来源于同一个Class文件,只要加载它们的类加载器不同,那么这两个类就必定不相等。包括:equals()、isAssignableFrom()、isInstance()方法返回的结果。
3.4.1 【双亲委派模型】
==============================后续见【《深入理解Java虚拟机》】JVM的秘密—Part2==========================