一、Java技术体系
一、Java技术体系概述
1、Java体系结构包括的四个独立但相关技术
- Java程序设计语言
- Java class文件格式
- Java应用编程接口(API)
- Java虚拟机
当编写并运行一个Java程序时,就同时体验了这四中技术。
用Java语言编写源代码,把它编译成java class文件,然后再在Java虚拟机中运行class文件。
编写程序时,通过调用类(这些类实现了Java API)中的方法来访问系统资源(比如I/O)。
当程序运行的时候,它通过调用class文件中实现了java API的方法来满足程序的Java API调用。
1、编译时环境(将.java文件编译成.class文件)
程序的源文件 A.java -----> Java编译器(javac) -----> A.class
2、运行时环境
程序的class文件 A.class ------> Java虚拟机 <------ (Java API的class文件如:Object.class String.class)
3、Java虚拟机是什么
Java虚拟机的主要任务时装载class文件并且执行其中的字节码。Java虚拟机包括一个类装饰器(class loader),他可以从程序和API中装载class文件。Java API中只有程序执行时,需要哪些类才会被装载。字节码由引擎来执行。
要理解Java虚拟机,Java虚拟机可能指的是三种不同的东西
- 抽象规范
- 一个具体的实现
- 一个在运行中的虚拟机实例
Java虚拟机抽象规范仅仅是个概念。而规范的具体实现,可能来自多个提供商,并存在多个平台上。当运行一个Java程序的同时,也就在运行一个Java虚拟机实例。
4、Java虚拟机的生命周期
一个运行时的Java虚拟机实例的天职就是:负载运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
Java虚拟机实例通过调用初始类的main()方法来运行一个Java程序。Java程序初始类中的main()方法,将作为该程序初始线程的起点,任何其他的线程都是由这个初始线程启动的。
在java虚拟机内部有两种线程:守护线程与非守护线程。
守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程,只要还有任何非守护线程在运行,那么这个java程序也在继续运行(虚拟机任然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。
2、运行时的数据区域
Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
1、程序计数器
一块较小的内容空间,它可以看作是当前线程执行的字节码行号指示器。
2、栈
栈是线程私有的,它的生命周期与线程相同。
栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等消息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在栈中入栈到出栈的过程。
Java虚拟机,对于栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果扩展时无法申请到足够的内存,就会抛OutOfMemoryError异常
3、本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用本地方法服务。
4、堆
Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
虚拟机的堆大小可以通过-Xmx和-Xms控制。
如果在堆中没有内存完成实例分配,并且堆也无法在扩展时,将会抛出OutOfMemoryError异常
5、方法区
各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器后的代码等数据。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
二、Java异常总结
1、Java载体内存溢出:OutOfMemoryError异常
java堆溢出:实例
-Xms:设置初始化堆大小
-Xmx:设置堆最大可使用空间
-XX:HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出时Dump出当前的内存堆转储快照以便事后进行分析。
示例代码
import java.util.ArrayList; import java.util.List; public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } }
编译运行
javac HeapOOM.java java -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError HeapOOM
2、Java栈溢出:stackoverflowerror异常
虚拟机栈和本地方法栈溢出
-Xss128k
在单线程情况下,无论是栈帧太大还是虚拟机容量太小,当内存无法分配的时候,都是stackoverflowerror异常
示例代码
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 (Throwable e) { System.out.println("Stack length : " + javaVMStackSOF.stacklength); throw e; } } }
3、方法区溢出:
运行时常量池的内存溢出 -XX:PermSize=10M -XX:MaxPermSize=10M Java 8 已经将永久代从方法区移除
intern() 是一个native 方法 如果字符串常量池中已经包含一个等于此string对象的字符串,则返回次string对象的引用
示例代码
import java.util.ArrayList; import java.util.List; public class RuntimeConstantPoolOOM { public void testIntern() { String string1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(string1==string1.intern()); String string2 = new StringBuilder("ja").append("va").toString(); System.out.println(string2==string2.intern()); } public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } }
4、本机直接内存溢出
-Xms20M -XX:MaxDirectMemorySize=10M
示例代码
import java.lang.reflect.Field; import sun.misc.Unsafe; public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { // TODO Auto-generated method stub Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = null; try { unsafe = (Unsafe) unsafeField.get(null); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } while (true) { unsafe.allocateMemory(_1MB); } } }
三、jvm Java堆内存
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;
栈中的栈帧随着方法的进入和退出而有条不絮执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束了。
而java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
1、垃圾收集GC
GC 线上一般不开启,可以在测试环境分析瓶颈
在堆里面存放着Java世界中几乎所有的对象实例。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还"存活"着,哪些已经"死去"(即不可能被任何途径使用的对象)。
1、引用计数器算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象是不可能再被使用的。客观的说,引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。
但是至少主流的Java虚拟机里面没有选用引用计数算法来判断对象是否存活,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
简单实例:在下面的代码中testGC()方法:对象a和b都有字段instance,赋值 a.instance = b和b.instance = a除此之外,这两个对象再无任何引用,实际上这两个对象以及不可能在被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知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 main(String[] args) { ReferenceCountingGC a = new ReferenceCountingGC(); ReferenceCountingGC b = new ReferenceCountingGC(); a.instance = b; b.instance = a; a = null; b = null; //在这里发送GC,a和b是否能被回收 System.gc(); } }
使用jvm运行程序时,开启GC日志:
-XX:+PrintGC 输出GC日志 -XX:+PrintGCDetails 输出GC的详细日志 -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式) -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息 -Xloggc:../logs/gc.log 日志文件的输出路径
2、可达性分析算法
java的主流实现中都是通过可达性分析来判断对象是否存活。这个算法的基本思想就是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在java语言中,可作为GC Roots的对象包括下面几种
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常用引用的对象
- 本地方法栈中引用的对象。
3、Java分代收集算法
Java主要采用了分代收集算法。分代收集算法主要将对象存储器的长短将内存进行划分
广泛地说,JVM堆内存被分为两部分:年轻代(Young Generator)和老年代(Old Generation)
Java堆新生代内存分配设置可以通过参数 -Xmn来设置, 如-Xmn10m 是给新生代分配10m的内存,如果这时堆大小设置为20m,那么剩下的10m就会被分配在老年代。
还可以通过-XX:NewRatio这个参数来设置年轻代和老年代的大小,这个参数是设置年轻代和老年代的空间比例,如-XX:NewRatio=4,表示年轻代和老年代的空间比例为1:4,默认值是2
年轻代是所有新对象产生的地方。当年轻代内存空间被用完时,就会触发垃圾回收。这个垃圾回收叫Minor GC。年轻代被分为3个部分----Ende区和两个Survivor区
年轻代中Ende区和两个Survivor区的占用内存空间大小设置
可以通过参数:-XX:SurvivorRatio来设置Ende区和两个Survivor区的空间比例;例如-XX:SurvivorRatio=2,表示ende区和两个Survivor的空间比为2:1:1
,默认值是8,即ende区和两个Srurvivo的空间比8:1:1
大多数新建的对象都位于Eden区。当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。(-XX:MaxTenuringThreshold 默认15) 这个值也可以调调,这个表示在新生代折腾多少次后进入年老代,
下面看这个实例:
设置了这些参数:-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m,堆内存分配20M,新生代10M,老生代10M,默认情况下Survivor区为8:1:1,所以Eden区域为8M
public class JavaTest1 { static int m = 1024 * 1024; public static void main(String[] args) { //分配2兆 byte[] a1 = new byte[2 * m]; System.out.println("a1 ok"); //分配2兆 byte[] a2 = new byte[2 * m]; System.out.println("a2 ok"); } }
年老代:年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。通常会在老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC会花费更多的时间。
Stop the World事件:所有的垃圾收集都是“Stop the World”事件,因为所有的应用线程都会停下来直到操作完成(所以叫“Stop the World”)。
因为年轻代里的对象都是一些临时(short-lived )对象,执行Minor GC非常快,所以应用不会受到(“Stop the World”)影响。
由于Major GC会检查所有存活的对象,因此会花费更长的时间。应该尽量减少Major GC。因为Major GC会在垃圾回收期间让你的应用反应迟钝,所以如果你有一个需要快速响应的应用发生多次Major GC,你会看到超时错误。垃圾回收时间取决于垃圾回收策略。这就是为什么有必要去监控垃圾收集和对垃圾收集进行调优。从而避免要求快速响应的应用出现超时错误。
实例:设置大对象直接进入老年代
参数:-XX:PretenureSizeThreshold(该设置只对Serial和ParNew收集器生效) 可以设置进入老生代的大小限制,单位是字节
-XX:PretenureSizeThreshold=3145728 ,此例为3M(3*1024*1024)
jvm:-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m
public class JavaTest2 { static int m = 1024 * 1024; public static void main(String[] args) { //分配2兆 byte[] a1 = new byte[2 * m]; System.out.println("a1 ok"); byte[] a3 = new byte[4 * m]; System.out.println("a2 ok"); } }
实例:长期存活的对象进入老年代
为了演示方便,我们设置-XX:MaxTenuringThreshold=1(默认15),当在新生代中年龄为1的对象进入老年代。
jvm:-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m
public class JavaTest3 { static int m = 1024 * 1024; public static void main(String[] args) { //分配2兆 byte[] a1 = new byte[1 * m / 4]; System.out.println("a1 ok"); byte[] a2 = new byte[7 * m]; System.out.println("a2 ok"); byte[] a3 = new byte[3 * m]; //GC System.out.println("a3 ok"); } }
4、动态对象年龄判定
为了使内存分配更加灵活,虚拟机并不要求对象年龄达到MaxTenuringThreshold才晋升老年代,如果Survivor区中相同年龄所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象在Minor GC时将复制至老年代
5、空间分配担保
新生代使用复制算法,当Minor GC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC。
在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败,如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC。