什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
JVM参数
JVM内存结构
程序计数器:当前线程所执行的字节码的行号指示器,线程私有,不会发生内存溢出。
虚拟机栈:每个线程运行时所需要的总内存,称为虚拟机栈。每个栈由多个栈帧组成,对应者每次方法调用所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
存储内容:局部变量表-基本类型和对象引用类型(不同于对象本身),操作数栈,方法出口等信息。
问题1:垃圾回收器是否涉及栈内存?
垃圾回收器不涉及栈内存,方法执行完就会弹出栈,内存就会被回收了。
2.栈内存分配是越大越好么?
-Xss size设置栈内存大小。并不是,栈越大,会使线程的数量越小,因为计算机的物理内存是一定的。一般系统默认的栈内存就可以了。-Xss128k默认
- 方法内的局部变量是否是安全的?
如果方法内局部变量没有逃离方法的作用范围,他是线程安全的
如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全问题。
- 栈内存溢出?
- 栈帧数量过多,比如递归调用太深。
- 栈帧过大,一般不会出现这种情况。
- 线程运行诊断?
top命令查看进程
ps H -eo pid,tid,%cpu 查看具体线程
Jstack 进程id 可以根据线程id找到有问题的线程,进一步定位到有问题代码的源码行号
本地方法栈:java调用JNI所需要的内存空间。
(Hotspot将本地方法栈和虚拟机栈合而为一)
堆:通过new关键字,创建对象都会使用堆内存
特点:线程共享,堆中对象都需要考虑线程安全的问题,有垃圾回收机制
堆内存溢出问题?
长生命周期对象拥有短生命周期对象的引用就可能导致内存溢出。
-Xmx和-Xms控制堆内存大小。
-Xmn设置新生代内存大小。
堆内存诊断
1.jps工具 查看当前系统中有哪些java进程
2.jmap工具 查看堆内存的占用情况(只能查看某一时刻的)
jmap -heap pid 查看整个jvm内存状态
jmap -histo pid 查看jvm堆中对象详细占用情况
Jmap-dump:format=b,file=文件名 pid
Eclipse Memory Analyzer 分析jvmduidump文件的插件。
- jconsole工具 图形界面的,多功能检测工具(可以连续检测)
- Jvisualvm工具 可视化的方式展示虚拟机内容。
方法区内存溢出
-XX:MaxPermSize=8m 方法区的实现是永久代位置在heap上。
-XX:MaxMetaspaceSize=8m java1.8的方法区实现是元空间,位置在本地内存。
字节码动态生成技术,动态的完成类加载。Spring和mybatis都会动态加载很多类,容易造成方法区的内存溢出,但是方法区用元空间实现后,存储位置在本地内存,不容易造成内存溢出。
class文件结构(类基本信息(访问修饰符,包名类名,版本,父类,接口等),常量池,类方法定义(包含了虚拟机指令)
运行时常量池
常量池就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名,参数类型,字面量等信息
运行时常量池,常量池是字节码文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变成真实地址。
S3 == s4:false s3 == s5:true s3 == s6:true
x1 == x2:false
调换后两行位置后: 1.8的是x1 == x2:true,1.6的是x1 == x2:false
s3 编译器优化 相当于s3 = “ab”,因为编译的时候就可以确定s3的值
S4采用的是new stringbuilder.append(s1).append(s2).toString 相当于new String(“ab”)
字符串常量池是延迟加载的,即执行一行代码就把字符串常量加入到池中,并不是一次性全部加载。
s.intern()//1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池。最后都会把串池中的对象返回。
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则把此对象复制一份放入串池。最后都会把串池中的对象返回。
总结:StringTable(字符串常量池) 特性
常量池中的字符串仅是符号,第一次用到时才变为对象。
利用串池的机制,来避免重复创建字符串对象。
字符串变量拼接的原理是StringBuilder(1.8)
字符串常量拼接的原理是编译期优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池中。
StringTable1.6
Stringtable 在常量池中,并位于permGen永生代(方法区的具体实现)中。
Stringtable1.8
永生代被删除了,使用元空间(MetaSpace)来替代(方法区的具体实现)。元空间位于本地内存中,常量池于位元空间中,但Stringtable单独放到了堆中。
永生代只有当触发full gc时才会进行垃圾回收,但是只有当老年代内存不足时触发full gc。
Stringtable垃圾回收(HashTable类似,使用数组和链表实现)
-Xmx10m -XX:PrintStringTableStatistics -XX:printGCDetails -verbose:gc
跟堆的垃圾回收一样。
StringTable调优
- -XX:StringTableSize=20000(如果要是程序中字符串常量池中数据比较多,可以把该值给的比较大)这个是hashtable桶的个数,桶的个数越大,哈希冲突的可能就越小,那么查找速度就越快。
- 考虑将字符串对象是否入池。使用String.intern()对内存进行调优,将大量重复的字符串对象放入到串池中。
如何判断对象可以回收
引用计数法
只有对象被其他变量所引用,那么就让该对象的引用计数+1,不再引用就-1。没有引用就为0,就可以被垃圾回收了。
弊端:循环引用,A对象引用B对象,B对象引用A对象。就会造成对象使用完却无法被回收的现象。
可达性分析法(java使用)
通过可达性分析法来判断对象是否可用。通过一系列称为GC roots的对象作为起始点,从这些节点开始向下搜索,搜过的路径称为引用链。如果一个对象到GCroots没有任何引用链,那么它就是可回收对象。
GC roots对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
四种引用
强引用
new一个对象然后赋值给了变量,那么变量就强引用了这个对象。比如Object obj = new Object(),只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用
SoftReference,在系统将要发生内存溢出异常之前,将这些对象列入回收范围内进行第二次垃圾回收。如果这次回收还没有足够的内存才会抛出内存溢出异常。
弱引用
WeakReference,当垃圾回收器工作时,无论当前内存是否足够,都会回收只被弱引用的对象。
虚引用
PhantomReference,虚引用完全不会对其生存时间造成影响。唯一的作用就是在这个对象被收集器回收时收到一个系统通知。以NIO的ByteBuffer为例,它就是一个虚引用。当ByteBuffer使用完被回收后,虚引用Cleaner对象就会进入到引用队列,ReferenceHandler会定时查找队列里是否有Cleaner对象,如果有就会调用Cleaner对象的clean方法,clean方法就会根据之前记录的直接内存的地址调用Unsafe对象的freeMemory方法来释放掉直接内存。
终结器引用
当一个对象重写了Object的finalize()方法,并且没有强引用时,虚拟机会自动帮忙创建终结器引用。当对象被垃圾回收时,就把终结器引用加入引用队列。再由一个优先级较低的finalizeHandler线程会在某些时间查看引用队列是否有终结器引用,根据终结器引用找到引用的对象,然后调用对象的finalize()方法。调用完后,等下一次垃圾回收时就可以真正回收掉占用的内存。(不推荐使用finalize()方法,效率极低,内存并不会立即释放,可能长时间占用)
软弱虚终结器引用也是对象,可以配合队列使用。虚引用和终结器引用必须配合引用队列使用。如果在创建时分配了一个引用队列,那么当引用对象被回收掉后,引用就会进入引用队列。然后可以把队列里面的引用资源给释放掉。
垃圾回收算法
标记清除
优点:速度块
缺点:会产生内存碎片
标记整理
优点:不会产生内存碎片
缺点:整理过程会移动对象的位置,速度较慢
复制
- 先在from标记不被引用的对象2.然后把from区上存活的对象复制到to区中。3.清空from区,并交换from和to区。
优点:速度块,且不会产生内存碎片
缺点:需要占用两倍的内存空间
垃圾回收器结合了以上的多种垃圾回收算法实现。
分代垃圾回收
把java堆分为新生代和老年代。新生代又分为伊甸园,幸存区from和幸存区to。这样可以把不同生命周期的对象放到不同特点的内存中。新生代的垃圾回收较为频繁,老年代的垃圾回收不频繁,适合长时间存活的对象。类似家里面的垃圾一样,有些比较有价值的旧物件也是垃圾但是舍不得丢掉,会放在家里面的储藏室里。家里的垃圾桶放没有价值的新垃圾。垃圾桶经常清理,随着日子的增长储藏室的东西越来越多然后需要清理,然后相当于老年代垃圾清理触发full gc。它的频率低于垃圾桶的回收频率。
工作原理:
当创建新对象时,对象会被分配到伊甸园中。当伊甸园的内存不足时就会触发Minor GC,采用复制算法,将存活的对象复制到幸存区to中,并将对象的寿命+1(对象寿命的阈值最大为15,即对象头中4个bit来存放其信息).然后交换幸存区from和幸存区to的位置。伊甸园和幸存区to的内存就空出来了。第二次除了伊甸园中的对象还会清理幸存区from中国的对象,按照前面的方式回收。当幸存区的对象寿命超过阈值就会把对象放入老年代中。当老年代中内存也不足时,就会触发一次full gc。
相关vm参数
垃圾回收器
7款垃圾收集器
Servial:单线程,复制算法
Servial old配合servial使用,标记整理算法
ParNew:多线程,复制算法(在servial基础上改进成了多线程)(目标提高回收速度,缩短回收时间)
Parallel Scavenge:多线程,复制算法 (目标在于达到一个可控制的吞吐量)
Parallel old:多线程,标记整理算法
CMS:多线程,标记清除算法,总共四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 标记清除(用户线程继续执行)
CMS缺点:1.对cpu资源敏感。并发标记,虽然不会导致用户停顿,占了一部分线程导致程序变慢,降低吞吐量。
- 标记清除的时候还会产生新的垃圾,这些垃圾称为浮动垃圾,cms无法在该次垃圾清理过程中清除这些垃圾。
- 基于标记清除算法,会产生内存碎片。
G1:多线程,整体是标记整理算法,局部是复制算法,总共四个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收(用户线程不能继续执行)
特点:
- 能够充分利用多cpu,多线程环境的硬件优势,尽量缩短stw(暂停其他线程)时间。
- G1整体采用标记整理算法,局部是复制算法。
- 宏观上G1不再区分年轻代和老年代,内存被划分为多个独立的region区域。但小范围内还是有年轻代和老年代的区分,但是不是物理隔离,而是一部分region的集合,不需要内存连续。即还是会采用不同的gc方式来处理不同的区域。G1的年轻代和老年代是逻辑概念,每个region会随着G1的运行在不同代之间进行切换。
- 筛选回收会对各个region的回收价值和成本进行排序,根据用户所期望的GC停顿来制定回收计划,优先回收成本小,价值高的region区域。
与CMS的相比的优势:
没有内存碎片,可以精确控制停顿。
新生代GC(minor GC)指发生在新生代的垃圾收集动作,特点:收集速度快,频率也快。
老年代GC(major GC/Full GC)指发生在老年代的垃圾收集动作,出现full gc通常会伴随着至少一次的minorGC。Full GC的速度一般比Minor GC慢10倍以上。
内存分配策略:上面的是一些垃圾回收的策略,以下是内存分配策略
1.对象优先在Eden分配
对象在新生代Eden区中分配。当Eden区没有足够空间时,虚拟机将发起一次minor gc。
2.大对象直接进入老年代。
大对象指需要大量连续内存空间的java对象,比如很长的字符串已经数据。
3.长期存活的对象进入老年代。
对象头中存放了对象的年龄,对象在Eden中诞生并经过一次minor 后仍然存活,并且能够被Survivor区容纳的话,就会进入Survivor,并且对象年龄设为1.然后每熬过一次minor gc后年龄就+1,当年龄到达一定程度后(阈值默认为15)就会晋升到老年代中。
4.动态对象年龄判断。
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivir空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
对象的内存布局
对象在内存中存储的布局可以分为三块区域:对象头,实例数据和对齐填充。
64位计算机的对象头,markword是8个字节。Class对象指针是四个字节
类加载机制
1.加载
1.通过类的全限定名得到该类的二进制字节码文件
2.将字节码文件中的静态数据结构转换成运行时数据结构
3.在内存中生成该类的class对象
2.链接
1.验证
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
2.准备
为静态属性分配内存,并初始化为默认值。
3.解析
将符号引用转换成直接引用
3.初始化
初始化类,静态属性和静态代码块初始化。
类加载的时机(一共6种)?
双亲委派机制?什么作用?怎么破坏?写一个String类能加载进去么?
当某个类加载器加载某个.class文件的时候,并不是由该类加载器加载,而是由这个类加载器的上级类加载器加载,递归操作,如果上级的类加载器没有加载,自己才会加载这个类。
作用:1.避免类的重复加载。2.避免核心类被篡改。
破坏:
1.自定义类加载器,重写loadClass方法;
2.使用线程上下文类加载器;
不能。
BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
JVM类加载方式
类加载有三种方式:
- 1、命令行启动应用时候由JVM初始化加载
- 2、通过Class.forName()方法动态加载
- 3、通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()区别
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。Class.forName(name,initialize,loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。