JVM面试总结(1)-java内存区域和内存溢出异常

2.1内存分布图:
JVM面试总结(1)-java内存区域和内存溢出异常_第1张图片
方法区和堆是线程共享的;
虚拟机栈,本地方法栈,程序计数器是线程私有的;
1. 线程计数器:用来指定当前线程执行字节码的行号指示器。JVM的多线程是通过线程轮流切换分配执行时间来实现的,在任何时刻,每个处理器都只会执行一个线程中的指令,当线程进行切换的时,为了线程能恢复当正确的位置,所以每个线程必须有个独立的线程计数器,这样才能保证线程之间不互相影响。字节码解释器的工作就是通过改变这个指示器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要这个计数器来完成。
2. 虚拟机栈:生命周期与线程是相同的,描述java方法执行的内存模型。每个方法在执行的同时,都会创建一个栈帧(方法运行时的基本数据结构),用于存储局部变量表,操作数栈,动态链接,方法出入口等信息,每个方法的调用到执行完成的过程就是一个栈帧入栈到出栈的过程;局部变量表存储方法相关的局部变量,包括基本数据,对象引用和返回地址等。局部向量表在编译期就确定了。
3. 本地方法栈:与虚拟机栈执行的基本相同,一般混为一谈,统称栈,唯一的区别就是虚拟机栈是执行Java方法的,本地方法栈是执行native方法的;
4. Java堆:线程共享。主要存储对象的实例。可拓展。 逻辑连续,物理不一定连续,分为新生代和老年代。还可能划分多个线程私有的分配缓冲区(TLAB),。
5. 方法区(永久区):存储被虚拟机加载的类信息(class文件)(类的全限定名,字段,方法,接口)、常量、静态变量、即时编译的代码等数据等;物理上不需要连续,运行时常量池是方法区的一部分,常量池主要用于存放编译生成的各种字面量和符合引用,直接引用。jdk6.0之前,字符串常量池一直在方法区。Jdk7.0后,在堆区。
对方法区的垃圾回收主要是针对常量池的回收和对类型的卸载。
6. 直接内存:,不是虚拟机运行时的一部分,可以直接访问堆外的内存;所以当内存空间无法动态扩展的时候就会出现OutOfMemoryError异常;通过nio中的DirectByteBuffer对象对直接内存的引用进行操作。
2.2 对象创建与异常
1.对象的创建:
几种方式:克隆,new 关键字,反序列化,反射。
先new ->检查常量池中是否有符号引用(编译器生成在方法区中的),并且符号引用表示的类是否被加载,解析和初始化过,没有则先进行类加载,再开始分配内存。
分配内存:1. 指针碰撞(java内存规整,带有压缩整理的垃圾收集器),空闲列表(空闲内存相互交错,没有压缩整理)
解决内存分布的并发问题:

  1. 对分配内存空间的过程进行同步处理-采用CAS配上失败重试的方式保证原子性
  2. 把内存分配的动作按照线程划分在不同的空间之中。即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB:线程私有)。
    2.对象的内存布局:
    1, 对象头:对象自身运行时的数据(哈希码,GC分代年龄,锁状态标志,线程持有的锁)(类型指针:对象指向它的类元数据的指针。通过指针知道它是那个类的实例)
    2, 实例数据:对象真正储存的有效信息(各种字段),
    3, 对齐填充:占位符,补全字节。因为要求对象的起始地址必须是8字节的整数倍。
    3.对象的访问定位:
    通过栈上的引用数据类型来操作堆上的具体对象
    两种方式:
  3. 句柄(存储对象的句柄地址):在对象被移动时(垃圾收集时会移动对象即内存整理),句柄只需修改句柄中的对象实例数据的指针,栈中的reference(引用)不需要修改。而直接指针需要修改。
  4. 直接指针:速度更快,因为少了一次指针定位的时间开销。
    4.outofmemoryerror异常:
    1.java堆(heap)溢出:对象太多。注意概念:内存泄露:程序申请了内存空间,没有用,也无法释放。和内存溢出:没有足够的空间供其使用,
    2.虚拟机栈和本地方法栈溢出(递归方法太深):
    1.请求的栈深度大于了允许的最大深度。
    2.扩展栈时无法申请到足够的内存空间。
    3.方法和运行时常量池溢出(创建了太多的常量在常量池):
    4.本机直接内存溢出。
    Java内存泄漏的5中情况:
    1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

public class UsingRandom {
	
	private String msg;
	public void receiveMsg(){
		readFromNet();// 从网络中接受数据保存到msg中
		saveDB();// 把msg保存到数据库中
	}
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。

你可能感兴趣的:(java虚拟机)