一、java虚拟机的生命周期:
Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可 以把自己的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
二、JVM的体系结构:
一、类加载器解析
二、运行时数据区
三、执行引擎
四、本地方法区
三、运行时数据区分为下图内存模型:
一、堆
堆是存储的单位,是线程共享的,只负责是存放实例变量。
PS:为栈服务
二、栈
栈是线程独有的,一个线程有一个相对应的栈,因为不同的线程处理的逻辑不同,所以需要一个独立的栈。 栈是运行时单位里面存放的都是跟当前线程相关的信息,比如:局部变量、方法、方法返回值、基本数据类型,堆中实例对象的引用等。
PS:一个实例对象的引用占4B。
栈是程序运行的最小单元,main()方法就是栈的起点,也是程序的起点。
-Xss设置栈的大小
三、堆栈分开的好处?
①隔壁模式。
②堆中的实例对象可以被共享。
③栈要保存系统上下文,需要进行地址段调的划分,由于栈的只能向上增长就会限制站的存储能力,而堆中的对象可以动态的增长,拆分后栈只需记录堆中一个地址而已。
④面对对象就是堆和栈的完美结合。
使垃圾成为可能,大大提高垃圾回收的效率,因为如果堆和栈没有分开那么将会是一块很大的内存区域,该区域存在的数据类型将会十分混乱;在垃圾回收时将会对该内存区域整体进行垃圾回收,大大降低的垃圾回收的速率。
四、JAVA中参数传递时是值传递还是引用传递?
如果参数为基本数据类型时为值传递;反之为引用传递。
五、JAVA对象的大小?
一个空的对象(没有任何属性)在堆内存中占8个字节,所以堆内存的分配都是以8的整数倍的来划分的。
六、怎么判断对象是否可以被回收?
①引用计数器
为每个对象创建一个引用计数,有对象引用时计数器+1,引用被释放时-1,当计数器为0时表示可以被回收。(不能解决循环依赖问题)
②可达性分析法
从GCRoots开始向下搜索,搜索所走过的路径成为引用链,一个对象到GCRoots没有任何引用链时,证明这些对象可以被回收。
七、JAVA中的引用类型:
强引用:只要引用在,垃圾回收期永远不会回收。
软引用:非必须引用,内存溢出之前回收。
弱引用:第二次垃圾回收时回收。
虚引用:每次垃圾回收都会被回收。
四、垃圾回收算法
一、标记-清除算法(Mark-Sweep)
此算法分为两个阶段:第一阶段从引用根节点开始标记所有引用的对象;第二阶段遍历整个堆把未标记的对象清除。此算法会暂停整个应用,同时会产生垃圾碎片。
二、标记-复制算法
此算法把内存空间划分为两个相等的区域,垃圾会收时遍历当前使用区域把正在使用的对象复制到另一个区域中。此算法只处理正在使用中的对象,因为复制正本小,复制过去后还能进行相应的内存整理,不会出现内存碎片,但是需要2倍的内存空间。
三、标记-整理算法
此算法结合了“清除算法” 和 “复制算法”的两个优点。此算法也分为两个阶段:第一阶段从引用根节点开始标记所有引用的对象;第二阶段遍历整个堆把未标记的对象清除并且把存活的对象“压缩”到堆中的的其中一块,按顺序排放;此算法避免了“标记-清除算法”的碎片问题,同事也避免了“标记-复制算法”的2倍内存空间问题。
四、分代算法-----现在JVM采用的算法
<1>、根据对象分类
根据对象的存活存活周期不同而将内存划分为几块:新生代,老年代,永久代(1.8已被源空间代替)。
1、新生代:目标对象可能被快速回收,对象的生命周期较短(如:方法的局部变量)
2:老年代:在年青代经历了N次回收仍然存活的对象就会被放到老年代,老年代存在的都是生命周期较长的对象(如:缓存对象,单例对象)
3:永久代:存放静态文件,如:JAVA类,方法等,永久代对垃圾回收影响显著,需要较大的内存空间,对象生成后几乎不变的对象(类的加载信息)。通过-XX:MarPermSize = " "进行设置。
<2>、内存区域
新生代和老年代都在JAVA堆中
永久代在方法区
堆大小 = 新生代(1) + 老年代(2);
<3>JAVA堆对象回收
新生代:采用标记--复制算法;
老年代:标记--整理算法,标记--清除算法。
方法区:标记--整理算法,标记--清除算法;方法区回收的条件比较苛刻,只有同时满足以下3个条件才会被回收:
1、所有实例被回收;
2、加载类的classLoader被回收;
3、Class对象无法通过任何途径访问(包括反射)。
五、垃圾回收器
7种垃圾回收器,如下图:
1、垃圾回收器对应的算法:
2、CMS垃圾回收的过程:
①初始标记
②并发标记
③重新标记
④并发清除
CMS的优点:并发收集,低停顿。
CMS的缺点:对CPU敏感,浮动垃圾,空间碎片无法处理。
CMS晋升失败??新生代------>老年代
3、G1垃圾回收器
1、JDK1.7开始引用G1,JDK被设置为默认的垃圾回收器,目标就是彻底替换CMS。
2、G1的内存分配策略:将内存分成一个个region(1~32MB)。
3、region类型:
3.1 3种常见类型:Eden,Survior和old genaration区。
3.2 巨无霸类型:保存比标准region区大50%及以上的对象,对象在一组连续的内存中,转 移会影响效率,标记阶段发现巨型对象不再存活时,会被直接回收。
3.3 未使用区 :未被使用的region区。
特殊说明:某个region的类型不是固定的,比如一次ygc过后,原来的Eden区变成了空闲的可用区,随后也能分区巨型对象。
4、G1中重要的数据结构和算法
4.1本地线程缓冲区 TLAB
Thread Local Allocation Buffer默认开启,分配在Eden空间,属于单个线程,每个线程都有一个TLAB用于分配对象。
4.2 晋升本地分配缓冲区PLAB
4.3 待收集集合 CSets
4.4 卡表 Card Table
4.5 记忆集合RSets
4.6 Snapshot-At-The-Begining(SATB)
5、G1垃圾回收方式
垃圾回收算法:标记--整理算法
G1的GC类型:
1>YGC:仅年轻代
2>Mixed GC :包含所有年轻代及部分老年代
3>Full GC:全堆扫描每个region.
六、内存逃逸
主要是对象的动态作用域得变化而引起的,故而内存逃逸的分析就是分析对象的动态作用域。
内存逃逸分为:①方法逃逸 和 ②线程逃逸。
优化:即证明一个对象不会逃逸到方法或线程外。
①栈上分配
②同步消除
③标量替换
All in All
逃逸分析是比较耗时的,所以性能未必提升很多,因为其耗时性。
相关参数:
-xx:+DoEscapeAnalysis 开启逃逸分析
-xx:+PrintEscapeAnalysis 通过此参数查看分析结果
-xx:+EliminateAllocation 开启标量替换
-xx:+EliminaLocks 开启同步消除
七、内存溢出
申请内存时,没有足够的内存空间,OutOfMemoryError.
产生原因:①JVM内存小;②程序不严谨,产生过多的垃圾没有及时回收。
程序体现:
①一次性从DB中获取读取过多的数据,内存中加载的数据太庞大。
②集合类中有对象引用,使用后未回收使得JVM不能回收。
③代码中出现死循环产生过多的重复实体。
④启动时JVM参数内存值设定过小。
解决方案:
①适当增加JVM内存大小。
②优化程序,释放垃圾。
③修改系统变量,Catalina.bat-->JAVA_OPTS
主要思路:
①避免死循环
②防止一次性载入太多的数据,从根本上解决内存溢出的方法就是修改程序,及时释放没用的对象,释放内存空间。
八、内存泄漏 Memory Leak
是指申请内存无法释放内存空间,一次内存泄漏的危害可以忽略,但是内存泄漏堆积的后果很严重会耗光内存。
java中内存泄漏的8中情况:
(一)、静态集合类
静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object(); //局部变量,内存泄露,造成obj对象不能被回收
list.add(obj);
}
(二)、单例模式
单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
(三)、内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
(四)、各种连接,如数据库连接、网络连接和IO连接等
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
public static void main(String[] args) {
try{
Connection conn =null;
Class.forName("com.mysql.jdbc.Driver");
conn =DriverManager.getConnection("url","","");
Statement stmt =conn.createStatement();
ResultSet rs =stmt.executeQuery("....");
} catch(Exception e){//异常日志
} finally {
// 1.关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3.关闭连接 Connection
}
}
(五)、变量不合理的作用域
变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
public class UsingRandom {
private String msg;
public void receiveMsg(){
//从网络中接受数据保存到msg中
readFromNet();
//把msg保存到数据库中
saveDB();
// private String msg;
// msg = null
}
}
如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。
(六)、改变哈希值
改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把String 当做 HashMap 的 key 值;
当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。
(七)、缓存泄露
内存泄漏的另一个常见来源是缓存,一旦把对象引用放入到缓存中,就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。如下:
(八)、监听器和其他回调
内存泄漏常见来源还有监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
PS:未更新完!!!