java必备知识进阶

1.java的内存管理

a.主要包括:内存分配和内存回收

b.注意点:java的垃圾回收是不能保证一定发生的,除非jvm内存耗尽,合理的管理对象还是有必要的

c.java程序执行过程:java源文件-》java字节码文件.class-》类加载器-》直接到执行引擎或者先到运行时数据区(内存)再到执行引擎

其中类加载过程有:加载-》链接(验证【为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求】、准备【为类变量分配内存并设置初始值】、【解析虚拟机将常量池的符号引用转为直接引用】)-》初始化(初始化类变量和其它资源、执行类构造器方法的过程)

其中类加载顺序:启动类加载器-》扩展类加载器-》应用程序类加载器

其中运行时内存区域划分图:

图

其中类成员初始化顺序:先静态后普通再构造,先父类后子类

1、父类静态变量和代码块-》子类静态变量和代码块
2、父类普通变量和代码块-》执行父类构造器
3、子类普通变量和代码块-》子类构造器
4、static方法初始化优先于普通方法,静态初始化只有在必要时刻进行且只初始化一次

注意: 子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法。如果父类没有不带参数的构造方法,那么子类必须用supper关键子来调用父类带参数的构造方法,否则编译不能通过。

d.四种垃圾回收器和finalize方法:

java中垃圾回收器可以帮助程序猿自动回收无用对象占据的内存,但它只负责释放java中创建的对象所占据的所有内存,通过某种创建对象之外的方式为对象分配的内存空间则无法被垃圾回收器回收;而且垃圾回收本身也有开销,GC的优先级比较低,所以如果JVM没有面临内存耗尽,它是不会去浪费资源进行垃圾回收以恢复内存的。最后我们会发现,只要程序没有濒临存储空间用完那一刻,对象占用的空间就总也得不到释放。我们可以通过代码System.gc()来主动启动一个垃圾回收器(虽然JVM不会立刻去回收),在释放new分配内存空间之前,将会通过finalize()释放用其他方法分配的内存空间。

四种收集器:Serial收集器(单线程的新生代收集器,必须暂停其它所有工作线程直到收集结束)、Parallel收集器(多线程串行收集器)、CMS收集器(基于标记-清除算法实现,容易产生大量碎片,cpu资源占用大)、G1收集器(标记整理算法实现,不会产生空间碎片,高吞吐量)

finalize工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用并且只能调用一次该对象的finalize()方法(通过代码System.gc()实现),并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果我们重载finalize()方法就能在垃圾回收时刻做一些重要的清理工作或者自救该对象一次(只要在finalize()方法中让该对象重新和引用链上的任何一个对象建立关联即可)。finalize()方法用于释放用特殊方式分配的内存空间,这是因为我们可能在java中调用非java代码来分配内存,比如Android开发中调用NDK。那么,当我们调用C中的malloc()函数分配了存储空间,我们就只能用free()函数来释放这些内存,这样就需要我们在finalize()函数中用本地方法调用它。

e.如何判断java对象需要被回收

引用计数,计数为0表示不可用,引用计数记录着每个对象被其它对象所持有的引用数,被引用一次加1,时效减1;当一个对象被回收后,该对象所引用的其它对象的引用计数都会减少,它很难解决对象之间的循环引用实例

可达性分析算法,从GC ROOT对象向下搜索其走过的路径称为引用链,当一个对象不再被任何的GC ROOT对象引用链相连时说明该对象不再可用;GC ROOT对象包括四种:方法区中常量和静态变量的引用对象,虚拟机栈中变量引用的对象,本地方法栈中引用的对象;解决循环引用是因为GC ROOT是一组特别管理的指针,他们不是对象图里的对象,对象也不可能引用到这些指针

比较,可达性分析避免了循环引用的问题,引用计数算法只需要在每个实例对象之初,通过计数器来记录所有的引用次数即可,而可达性分析需要遍历整个GC根节点来判断是否回收

f.java对象的四种引用

强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”); 不管系统     资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。
          软引用 :通过SoftReference类实现,eg : SoftReference p = new SoftReference(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。
          弱引用 :通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收
         虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现

g.垃圾回收算法

停止-复制算法
这是一种非后台回收算法,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,内存浪费严重.它先暂停程序的运行,然后将所有存活的对象从当前堆复制到另外一个堆,没被复制的死对象则全部是垃圾,存活对象被复制到新堆之后全部紧密排列,就可以直接分配新空间了。此方法耗费空间且效率低,适用于存活对象少。

标记-清扫算法
同样是非后台回收算法,该算法从堆栈区和静态域出发,遍历每一个引用去寻找所有需要回收的对象,对每个找到需要回收对象都进行标记。标记结束之后,开始清理工作,被标记的对象都会被释放掉,如果需要连续堆空间,则还需要对剩下的存货对象进行整理;否则会产生大量内存碎片

标记-整理算法
先标记需要回收的对象,但是不会直接清理那些可回收的对象,而是将存活对象向内存区域的一端移动,然后清理掉端以外的内存。适用于存活对象多。

分代算法
在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用停止复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。

        h.内存相关问题

内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制(例如你把它的地址给弄丢了),因而造成了资源的浪费。Java 中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,Java堆内也可能发生内存泄露(Memory Leak; 当我们 new 了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露

内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

双亲委派模型:表示类加载器之间的加载顺序从顶至下的层次关系,加载器之间的父子关系一般都是通过组合来实现,而不是继承。可以防止内存中出现多份同样的字节码,并确保加载顺序

双亲委派模型的工作过程是:在loadClass函数中,首先会判断该类是否被加载过,加载过则进行下一步—-解析,否则进行加载;如果一个类加载器收到了类加载器的请求,先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜说范围中没有找到所需的类时,子加载类才会尝试自己去加载)

静态分派和动态分派:静态分派发生在编译阶段,是指依据静态类型(变量声明时定义的变量类型)来决定方法的执行版本,例如方法重载中依据参数的定义类型来定位具体应该执行的方法;动态分派发生在运行期,根据变量实例化时的实际类型来决定方法的执行版本,例如方法重写;目前的 Java 语言(JDK1.6)是一门静态多分派、动态单分派的语言。

动态分派具体实现Java虚拟机是通过在方法区中建立一个虚方法表,通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址,如果子类没有覆盖父类的方法,那么子类的虚方法表里面的地址入口与父类是一致的;如果重写父类的方法,那么子类的方法表的地址将会替换为子类实现版本的地址。方法表是在类加载的连接阶段(验证、准备、解析)进行初始化,准备了子类的初始化值后,虚拟机会把该类的虚方法表也进行初始化。

JDK7和8中内存模型变化:JDK7中把String常量池从永久代移到了堆中,并通过intern方法来保证不在堆中重复创建一个对象;JDK7开始使用G1收集器替代CMS收集器。JDK8使用元空间来替代原来的方法区,并且提供了字符串去重功能,也就是G1收集器可以识别出堆中那些重复出现的字符串并让他们指向同一个内部char[]数组,而不是在堆中存在多份拷贝

 


2.堆和栈的区别

1、空间分配的不同:栈是由操作系统自动分配释放,存放函数的参数值,局部变量的值等,而堆一般是由程序员分配释放,弱不手动释放,程序结束时可能由os回收

2、缓存方式不同:栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完立即释放,而堆是存放在二级缓存中,生命周期由虚拟机的GC算法决定,并不是一旦成为孤立对象就能被回收

3、数据结构的不同:堆可以被看成是一棵树 ,栈是一种先进后出的数据结构

 


3.字符串hash方法

•现在我们希望找到一个hash函数,使得每一个字符串都能够映射到一个整数上
•比如hash[i]=(hash[i-1]*p+idx(s[i]))%mod
•字符串:abc,bbc,aba,aadaabac
•字符串下标从0开始
•先把a映射为1,b映射为2,c->3,d->4,即idx(a)=1, idx(b)=2, idx(c)=3,idx(d)=4;
•好!开始对字符串进行hash
假设我们取p=13 ,mod=101
先把abc映射为一个整数
hash[0]=1,表示 a 映射为1
hash[1]=(hash[0]*p+idx(b))%mod=15,表示 ab 映射为 15
hash[2]=(hash[1]*p+idx(c))%mod=97
这样,我们就把 abc 映射为 97 这个数字了。
•用同样的方法,我们可以把bbc,aba,aadaabac都映射到一个整数
•用同样的hash函数,得到如下结果
• abc  ->  97
• bbc  ->  64
• aba  ->  95
• aadaabac  ->  35
•那么,我们发现,这是一个字符串到整数的映射
•这样子,我们就可以记录下每个字符串对应的整数,当下一次出现了一个已经出现的字符串时,查询整数是否出现过,就可以知道 字符串是否重复出现。
•现在要判断两个字符串是否一致,怎么办呢?直接用它们的hash值判断即可,若hash值一致,则认为字符串一致;若hash值不一致,则认为是不同的字符串。
•我们要判断两个字符串是否一致,没有那么麻烦,直接先判断长度是否一致,然后再判断每个对应的字符是否一致即可。
•但,如果要判断多个字符串里有多少个不同的字符串,怎么办呢?
•两两字符串都进行比较?时间复杂度太高
•把每个字符串hash成一个整数,然后把所有整数进行一个去重操作,即可知道答案了。
当遇到冲突时,我们可以想办法调整p和mod,使得冲突概率减小之又小。我们一般认为p和mod一般取素数,p取一个较大的素数即可(6位到8位),mod取一个大素数,比如1e9+7,或者1e9+9。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章