在jvm虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个new 操作去写对应的delete/free操作,不容易出现内存泄漏和内存溢出问题。正是因为Java程序员把内存控制权利交给Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
一、运行时的数据区域
java虚拟机在执行java程序的过程中会把管理的内存划分成若干个不同的数据区域
线程私有的:
- 程序计数器
- 虚拟机
- 本地方法栈
线程共享的:
- 堆
- 方法区
1、程序技术器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变计数器的值来选取下一条要执行的字节码指令,java的switch、if、rty-catch等功能都需要以来计数器来完成。
注:线程恢复功能也需要计数器。为了线程切换后恢复到正确的位置,每条线程都需要有一个独立的程序计数器,各计数器之间互不影响,独立存储,我们称这类内存区域为“线程私有”的。
2、java虚拟机栈
java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是java方法执行的内存模型。
java内存可以粗糙的分为堆内存和栈内存,其中栈就指的是虚拟机栈,或者说是虚拟机栈中局部变量表部分。
局部变量表主要存放了编译器可知的各种数据类型(八种基本数据类型)、对象引用(这里的对象引用指的是reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
3、本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行java方法(就是字节码)服务,本地方法栈为虚拟机使用到的Native方法服务
本地方法(Native方法):Java无法直接访问到操作系统底层如硬件系统,为此 Java提供了Java Native Interface来实现对于底层的访问。Java Native Interface,它是Java的SDK一部分,Java Native Interface允许Java代码使用以其他语言编写的代码和代码库,本地程序中的函数也可以调用Java层的函 数,即Java Native Interface实现了Java和本地代码间的双向交互。
4、堆
java虚拟机管理的内存中最大的一块,java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
java堆是发生GC主要区域,也被称为GC堆,从垃圾回收的角度,由于现在收集器基本都采用分代垃圾回收算法,所以java堆还可以细分为:新生代和老年代:在细致一点:Eden空间、From Survivor、to Survivor空间等。进一步划分的目的是更好地回收内存和更快的分配内存
5、方法区
方法区和java堆一样,是各个线程共享的区域,它用于存储已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等数据。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
二、java类的加载过程
jva类加载需要经历
1、加载
加载过程主要干三件事:
- 通过一个类的全限定名获取该类的二进制流
- 将该二进制流中的静态存储结构转化为方法运行时数据结构
- 在内存中生成该类的Class对象,作为该类的数据访问入口
2、验证
验证的目的是为了确保CLass文件的字节流中的信息不会危害到虚拟机,在该阶段主要完成了四种验证:
- 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型
- 元数据验证:对字节码描述的信息进行语义分析,如这个类是否含有父类,是否继承了不能被继承的类等
- 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法的验证。如,方法中的类型转换是否正确,跳转指令是否正确等
- 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行
3、准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在java堆中
4、解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也可能在初始化之后
5、初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码
三、java中垃圾收集的方法
1、引用计数法
如果对象没有被引用,就会被回收,缺点:需要维护一个引用计算器
2、复制算法
效率高,但是需要内存容量大,比较耗内存,使用在占空间比较小,刷新次数多的新生区
3、标记清除
效率比较低,会产生碎片
4、标记压缩
效率低速度慢,需要移动对象,但不会产生碎片
5、标记清除压缩
使用于占空间大刷新次数少的养老区
四、什么是类的加载器,类的加载器有哪些
通过类的权限定名获取该类的二进制字节流的代码块叫做类的加载器。
类的加载器主要有四种
1、启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接用
2、扩展类加载器(Extensions ClassLoader)它用来加载java的扩展库。java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类
3、系统类加载器(System ClassLoder)也叫应用类加载器:它根据java应用的类路径(Class Path)来加载java类。一般来说,java应用的类都是由它来完成加载的。可以通过ClassLoder.getSystemClassLoder()来获取它。
4、用户自定义类加载器,通过继承java.lang.ClassLoder类的方式实现
五、内存泄漏与溢出的区别?何时产生内存泄漏?
内存泄漏与溢出的区别:
1、内存泄漏是指分配出去的内存无法回收了。
2、内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出。比如用byte类型的变量存储10000这个数据,就属于内存溢出。
3、内存溢出是提供的内存不够;内存泄漏是无法再提供内存资源。
何时产生内存泄漏:
1、静态集合类:在使用Set、Vector、HashMap等集合类的时候需要特别注意,有可能会发生内存泄漏。当这些集合被定义成静态的时候,由于它们的生命周期跟应用程序一样长,这时候,就有可能会发生内存泄漏。
2、监听器:在Java中,我们经常会使用到监听器,如对某个控件添加单击监听器addOnClickListener(),但往往释放对象的时候会忘记删除监听器,这就有可能造成内存泄漏。好的方法就是,在释放对象的时候,应该记住释放所有监听器,这就能避免了因为监听器而导致的内存泄漏。
3、各种连接:Java中的连接包括数据库连接、网络连接和io连接,如果没有显式调用其close()方法,是不会自动关闭的,这些连接就不能被GC回收而导致内存泄漏。一般情况下,在try代码块里创建连接,在finally里释放连接,就能够避免此类内存泄漏。
4、外部模块的引用:调用外部模块的时候,也应该注意防止内存泄漏。如模块A调用了外部模块B的一个方法,如:public void register(Object o)。这个方法有可能就使得A模块持有传入对象的引用,这时候需要查看B模块是否提供了去除引用的方法,如unregister()。这种情况容易忽略,而且发生了内存泄漏的话,比较难察觉,应该在编写代码过程中就应该注意此类问题。
5、单例模式:使用单例模式的时候也有可能导致内存泄漏。因为单例对象初始化后将在JVM的整个生命周期内存在,如果它持有一个外部对象(生命周期比较短)的引用,那么这个外部对象就不能被回收,而导致内存泄漏。如果这个外部对象还持有其它对象的引用,那么内存泄漏会更严重,因此需要特别注意此类情况。这种情况就需要考虑下单例模式的设计会不会有问题,应该怎样保证不会产生内存泄漏问题。
六、GC线程是否为守护线程?
GC线程是守护线程。线程分为守护线程和非守护线程(即用户线程)。只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
七、什么是双亲委派模型,是做什么的?
双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类。