Java 最佳实践(一)
一、JVM
1. Jvm的体系结构?
包括: 类装载器(class loader subsystem)子系统、运行时数据、执行引擎(execution engine)。
说明:
jvm实例是一个进程,对应了一个独立运行的java程序;
执行引擎实例对应一个用户线程。
2. Jvm内存组成?(运行时数据)
a. 堆内存
运行时动态分配实例对象和数组,栈存放对象引用;
线程共享;
GC回收;
b. 栈内存
存储方法状态 如局部变量、方法参数、返回值等;
线程隔离;
执行方法先添加栈帧,执行完就出栈;
StackOverflowError,OutOfMemoryError.
c. Native栈
功能与栈相同;
每次调用本地方法,另起一个本地栈;
例如: JavaMethod -> JavaMethod -> CMethod -> CMethod -> JavaMethod -> JavaMethod.
d. 方法区
类加载.class文件后,存放 类型信息、属性、方法、静态或final常量;
线程共享;
e. 寄存器
pc程序计数器(记录下一个程序执行位置);
optop操作数栈顶指针(记录Java栈区的指针);
说明:
直接内存说明:
直接内存不是虚拟机运行时数据区的一部分。通过Native函数库直接分配的堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
3. 类装载器
a. 类加载过程
加载(Loading) 查找class字节码文件,直接的父类和接口都会加载到字节数组中;
链接(Linking)(a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用
初始化(init) 对静态变量、静态代码执行初始化操作。
b. 类加载实现原理
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述(继承关系):
①Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
②Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
③App ClassLoader
负责记载classpath中指定的jar包及目录中class
④Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
双亲委派模型
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
优势:
保证类加载器与类的唯一性,安全性
双亲委派模型的系统实现:
在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
4. 执行引擎
将字节码文件转换成jvm可以识别的指令;
解释器:一条一条的读取,解释并执行字节码指令;
即时(Just-In-Time)编译器:为了弥补解释器的缺点,在合适的时候,将常用的方法编译成本地代码。
。因此,内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
二、GC
1. 判断对象是否存活的算法?
a. 引用计数算法
原理: 为每个对象增加一个引用计数器,当计数器为0,代表对象已经死亡;
问题:很难解决循环引用问题。
b. 可达性分析算法
原理: 它的基本思想是通过一系列被称为“GC Root”的对象为起点,从这个起点向下搜索,搜索走过的路径称为引用链,当某个对象不在任何引用链上时,则说明这个对象不可能再被使用。
GC Root包括以下几种对象:
1. 虚拟机栈中引用的对象;
2. 本地方法栈中JNI引用的对象;
3. 方法区中类静态成员变量引用的对象;
4. 方法区中常量引用的对象;
2. 内存分配和回收策略
目前为止,jvm已经发展处三种比较成熟的垃圾收集算法:1.标记-清除算法;2.复制算法;3.标记-整理算法;4.分代收集算法
1. 标记-清除算法
这种垃圾回收一次回收分为两个阶段:标记、清除。首先标记所有需要回收的对象,在标记完成后回收所有被标记的对象。这种回收算法会产生大量不连续的内存碎片,当要频繁分配一个大对象时,jvm在新生代中找不到足够大的连续的内存块,会导致jvm频繁进行内存回收(目前有机制,对大对象,直接分配到老年代中)
2. 复制算法
这种算法会将内存划分为两个相等的块,每次只使用其中一块。当这块内存不够使用时,就将还存活的对象复制到另一块内存中,然后把这块内存一次清理掉。这样做的效率比较高,也避免了内存碎片。但是这样内存的可使用空间减半,是个不小的损失。
3. 标记-整理算法
这是标记-清除算法的升级版。在完成标记阶段后,不是直接对可回收对象进行清理,而是让存活对象向着一端移动,然后清理掉边界以外的内存
4. 分代收集算法
当前商业虚拟机都采用这种算法。首先根据对象存活周期的不同将内存分为几块即新生代、老年代、永久代,然后根据不同年代的特点,采用不同的收集算法。在新生代中,每次垃圾收集时都有大量对象死去,只有少量存活,所以选择了复制算法。而老年代中因为对象存活率比较高,所以采用标记-整理算法(或者标记-清除算法)
GC的执行机制
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显示调用
4.上一次GC之后Heap的各域分配策略动态变化
永生代(Permanent Space)为方法区
三、synchronized
四、内存泄漏和内存溢出区别
内存泄漏: memory leak,是指程序在申请内存后,无法释放已申请的内存空间,多次泄漏导致内存被耗光。
例子:
1. 数据库连接,网络连接,IO连接等没用显式调用Close关闭,会导致内存泄漏;
2. 监听器的使用,在是否对象的同时没用相应删除监听器的时候也可能导致内存泄漏;
内存溢出: out of memory,是指程序申请内存时,没有足够的内存空间供其使用。