1)内存区域
- 程序计数器:可以看作是当前线程所执行的字节码文件(class)的行号指示器,它会记录执行痕迹,是每个线程私有的
- 方法区:主要存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据,该区域是被线程共享的,很少发生垃圾回收。包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。
- 栈:栈是运行时创建的,是线程私有的,生命周期与线程相同,存储声明的变量
- 本地方法栈:为 native 方法服务,native 方法是一种由非 java 语言实现的 java 方法,与 java 环境外交互,如可以用本地方法与操作系统交互
- 堆:堆是所有线程共享的一块内存,是在 java 虚拟机启动时创建的,几乎所有对象实例都在此创建,所以经常发生垃圾回收操作
2)JVM类加载机制
jvm的类加载机制分为5个步骤:加载,链接(即为:验证(校验),准备,解析),初始化(后面其实还有使用和卸载(当使用完成之后,还会在方法区垃圾回收的过程中进行卸载)。)
加载:通过类的权限名获取此类的二进制字节流;将这个类的字节流的静态储存结构转化为方法区的数据结构;在堆中生成代表这个类的Java.lang.Class对象,作为访问方法区数据结构的入口。字节码来源:通过编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
验证(校验):确保class文件的字节流符合虚拟机的要求,并且不会危害虚拟机。
- 文件格式验证:基于字节流验证。
- 元数据验证:基于方法区的存储结构验证。
- 字节码验证:基于方法区的存储结构验证。
- 符号引用验证:基于方法区的存储结构验证。
准备:为类变量分配内存,并初始化默认值,特例是 static final ,其他的static变量的值将放置在类构造器<clinit>方法之中;
解析:将类型的符号应用转化为直接引用
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
举个例子:现在调用方法hello(),这个方法的地址是12345678,那么hello就是符号引用,12345678就是直接引用。
初始化:
初始化阶段是执行类构造器<clinit>方法的过程。<clinit>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<clinit>方法执行之前,父类的<clinit>方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>()方法。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):
- 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
- 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
- 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
- 虚拟机启动时,用户会先初始化要执行的主类(含有main)
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
3)类加载器
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。
系统自带的类加载器分为三种:
1)启动类加载器:它用来加载 Java 的核心库,是用原生代码来实现的
2)扩展类加载器:它用来加载 Java 的扩展库。
3)应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类
还可以创建自定义加载器:开发人员可以通过继承 java.lang.ClassLoader
类的方式实现自己的类加载器。
4)双亲委派机制
如果一个类加载器收到类加载的请求,不会马上对类进行加载,而是将请求委派给父加载器,每层的加载器都会如此,多样所有的请求都会到达启动了加载器,并且只有在接收到父类无法加载的反馈信息后才会自己尝试加载。
因此Object类在程序的各种类加载器环境中都是同一个类。如果不采用双亲委派机制,重写一个相同的系统类,虽然可以被编译,但是永远无法加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
5)new对象过程
当虚拟机执行到new指令时,会去方法区常量池中找类的信息,即去定位该类的符号引用,如果找的说明该类已经在方法区内,则继续执行一下操作,如若没有则先使用Class Loader对类进行加载。
然后虚拟机开始为对象分配内存,对象所需要的内存大小在类加载完成后就已经确定了。分配内存又分为两种,第一种,内存绝对规整,每次分配内存只需将指针后移相应的距离即可;第二种,空闲内存和非空闲内存夹杂在一起,那么就需要用一个列表来记录堆内存的使用情况,然后按需分配内存。java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定.
对于多线程的情况,如何确保一个线程分配了对象内存但尚未修改内存管理指针时,其他线程又分配该块内存而覆盖的情况?有一种方法,就是让每一个线程在堆中先预分配一小块内存(TLAB本地线程分配缓冲),每个线程只在自己的内存中分配内存。但对象本身按其访问属性是可以线程共享访问的。
内存分配到后,虚拟机将分配的内存空间都初始化为零值(不包括对象头)。所以实例变量不赋初值也能使用。
如果new的对象是局部变量,new的对象变量在栈帧的局部变量表,这个对象的引用就放在栈帧。
如果new的对象是实例变量,new的对象变量在堆中,对象的引用就放在堆。
如果new的对象是静态变量,new的对象变量在方法区,对象的引用就放在方法区。
6)堆的新生代,老年代
新生代分为一个Eden区和两个Survivor(比例8:1:1,可修改,而往往老年代的内存比都会远大于新生代的内存比),通常对象主要分配到新生代,少数情况下也可能会直接分配在老年代中(如大对象的分配)。jvm每次使用Eden区和一个Survivor(我们称为From区),在经过一次Minor Gc,会将Eden区,和From还存活的对象复制到另外一个Survivor区(我们称为To区)(特别的因为新生代往往朝生暮死,存活的时间短,所以我们只需要花费很少的开销去对对象进行复制,及采用复制算法),最后交换From和To的身份。此时由Eden区复制过来的对象年龄为1,From来的年龄加1;以后这些对象每在Survivor区熬过一次GC,它们的年龄就加1,当对象年龄达到某个年龄(默认值为15)时,就会把它们移到老年代中。
在Minor Gc中有可能复制到To区的对象在To区无法放下,则这些对象将直接通过分配担保机制进入老年代;
年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。
总结:
1、Minor GC是发生在新生代中的垃圾收集,采用的复制算法;
2、新生代中每次使用的空间不超过90%,主要用来存放新生的对象;
3、Minor GC每次收集后Eden区和一块Survivor区都被清空;
4、老年代中使用Full GC,采用的标记-清除算法。
永久代(方法区)的垃圾收集主要回收两部分内容:废弃常量和无用的类。
判断“无用的类”:
该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
7)Minor GC,Major Gc,Full Gc
Minor Gc:从年轻代回收内存
1.Eden区内存满
2、新创建的对象大小 > Eden所剩空间
Full GC:清理整个堆空间,包括年轻代和老年代(Major GC通常是跟full GC是等价的)
指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程)。
其实只有CMS的concurrent collectio会只对老年代进行GC其它能收集old gen的GC都会同时收集整个GC堆。
1.System.gc()
2.每次晋升到老年代的对象平均大小>老年代剩余空间,即在准备触发Minor GC,如果发现统计数据说之前的Major GC的平均晋升到老年代的大小比目前老年代的剩余内存大,则不会触发Major,而是转而触发Full GC。
Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。
3.MinorGC后存活的对象超过了老年代剩余空间
4.堆内存分配很大的对象
8)垃圾收集器(不全,待补)
新生代垃圾收集器
1.servial(串行,响应速度优先,Stop The World
它采用的是复制算法,它在进行垃圾收集的时候必须停止其他所有工作线程,知道垃圾收集完成(“Stop The World”)。
2.ParNew(并行,响应速度优先,Stop The World)
servial的多线程版本。
3.Parallel Scavenge(并行,吞吐量优先,)
同样是复制算法,多线程,Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。
老年代垃圾收集器
1.Servial Old(串行,响应速度优先,Stop The World)
Servial 的老年代版本,但是使用的是标记整理算法
2.Parallel Old(并发,即和用户线程一起执行,
Parallel Old是Parallel Scavenge的老年代版本,使用的是标记整理算法
3.CMS(并发)
Concurrent Mark Sweep是一种以最短回收停顿时间为目标的垃圾收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。Mark Sweep说明它使用的是标记清除算法。
G1垃圾收集器
横跨整个堆内存,面向服务端的垃圾收集器。通过把堆内存分为大小相等的多个区域(2的幂次方),回收时计算出每个区域回收所获得的空间以及所需时间的经验值,根据记录两个值来判断哪个区域最具有回收价值,所以叫Garbage First(垃圾优先)。
9)类成员的初始化
静态域:静态代码块、静态成员变量 非静态域:非静态代码块、非静态成员变量 (成员方法不包含在里面,因为方法只能讲加载而非初始化)
1.没有继承父类的情况:
初始化顺序为:静态域 -> 非静态域 -> 构造函数 (左边优于右边)。
2.继承父类的情况:
初始化顺序为:父类静态域->子类静态域->父类非静态域->父类构造函数->子类非静态域->子类构造函数(左边优于右边)。
10)类初始化触发
主动引用
1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2.用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要触发初始化操作。
3.初始化一个类的时候,发现其父类还有进行过初始化,则需要触发先其父类的初始化操作。
注意这里和接口的初始化有点区别,,一个接口在初始化时,并不要求其父接口全部都完成了初始化,只要在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
4.虚拟机启动时,需要指定一个执行的主类(包含main方法的类),虚拟机会先初始化这类。
5.用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化操作。
被动引用
1.对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。(即子类直接引用从父类继承而来的静态变量)
2.通过数组定义来引用类,不会触发此类的初始化
3.
public class ConstClass {
static {
System.out.println("ConstClass init....");
}
public static final String MM = "hello Franco";
}
package com.xdwang.demo;
public class Test3 {
public static void main(String[] args) {
System.out.println(ConstClass.MM);
}
}
运行结果:
hello Franco
并没有ConstClass init….,这是因为虽然Test3里引用了ConstClass类中的常量,但其实在编译阶段通过常量传播优化,已经将此常量存储到Test3类的常量池中。两个类在编译成class之后就不存在任何联系了。
11)JMM(https://www.cnblogs.com/null-qige/p/9481900.html)
1.虚拟机栈(栈帧的本地变量表)中应用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(NATIVE方法)引用的对象
当一个对象对于GCRoots不可达时,并发直接被垃圾回收。会先执行该对象的fnalize()方法,此时该对象有一次自救机会,将自己关联到GCRoot上。如果对象在finalize()方法中将自己关联到GCRoots上,该对象将不会被垃圾回收器回收。但是虚拟机并不保finalize()执行完毕之后才进行垃圾回收,因此finalize()方法并不能一定自救成功。并且如果一个对象被自救过一次之后,仍旧脱离GCRoot,第二次将不再执行finalize()方法。finalize()方法运行代价高昂,不稳定性高,只是JAVA诞生之初为了让C/C++程序员接受而做出的一种妥协,有些说法说finalize()可以用来关闭外部资源,但是try{}finally{}可以执行得更好,JAVA程序员完全可以无视finalize()的用法。
13)内存溢出
1.内存溢出和内存泄漏的区别
内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。
2.堆溢出的原因?
堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。
堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。
如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具***置。
如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
3.栈溢出的原因?
由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss
参数来设定,存在两种异常:
StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。
4.运行时常量池溢出的原因?
String 的 intern
方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。
在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize
和 -XX:MaxPermSize
限制永久代大小,间接限制常量池。在 while 死循环中调用 intern
方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。
5.方法区溢出的原因?
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。
JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize
指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。
JDK1.2 后对引用进行了扩充,按强度分为四种:
强引用: 最常见的引用,例如 Object obj = new Object()
就属于强引用。只要对象有强引用指向且 GC Roots 可达,在内存回收时即使濒临内存耗尽也不会被回收。
软引用: 弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。
弱引用: 弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于 YGC 具有不确定性,因此弱引用何时被回收也不确定。
虚引用: 最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。
暂时这么多,刷题去
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
例如,我们可以把一个只有根节点为1的二叉树序列化为"1,",然后通过自己的函数来解析回这个二叉树
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
//牛客的千万别加static,牛客的java并不和c/c++一样,类只会加载一次,所有在多样例下加了出错
int num = 0;
String Serialize(TreeNode root) {
if (root == null) {
return "#";
} else {
return root.val + "," + Serialize(root.left) + "," + Serialize(root.right);
}
}
TreeNode Deserialize(String str) {
if(num >= str.length() || '#' == str.charAt(num)) {
num+=2;
return null;
}
int sum =0;
while(num < str.length() && str.charAt(num) != ','){
sum = sum * 10 + (str.charAt(num) - '0');
num++;
}
num++;
TreeNode tree = new TreeNode(sum);
tree.left = Deserialize(str);
tree.right = Deserialize(str);
return tree;
}
}
补:2020/7/16
16)G1为什么分成一小块一小块的内存?
G1把新生代老年代的固有内存的物理划分取消了,解决了内存不够的问题。但是仍然是分代垃圾收集器。在G1中,还有一种特殊的区域,叫Humongous区域。当一个对象的内存超过了分区的一半时,G1收集器将认为 这是一个巨型的对象,默认会直接分配到老年代,但是如果这个对象是短期的对象,也就是存活时间不长,那将得不偿失,为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
17)哪些对象可以被当作GC ROOT对象
1.虚拟机栈中(即栈帧中的本地变量表)引用的对象;
2.方法区中静态属性引用的对象;
3.方法区中常量引用的对象
4.本地方法栈中Native引用的对象
18)引用计数法会造成循环引用,那spring中如何解决容器初始化时的循环依赖呢?
三级缓存,即提前暴露,待补