Arthas:Alibaba开源的Java诊断工具,采用命令行交互模式,提供了丰富的功能,是排查jvm相关问题的利器。
jvm中每调用一个方法就会生成一个栈帧,放在栈空间
栈区分为虚拟机栈和本地栈
虚拟机栈存储java方法的栈帧,本地栈存储native方法的栈帧
程序计数器:存放每一个线程执行到了方法的哪一步,每个线程都有程序计数器
程序计数器在jvm相当于记录指令的内存地址(指令执行到哪一步了)
本地栈是没有程序计数器的,因为本地方法是C/C++的,java无法计数,程序计数器一直是0
方法区:class类信息,静态变量,常量
在jdk1.8之前,方法区是堆内的一块连续的区域
在jdk1.8之后,方法区从jvm内存移出来了,变成元数据区(MetaSpace)
,移到了操作系统之中
堆:存放对象,数组
堆分为新生代,老年代。默认新生代占堆内存的1/3,老年代占堆内存的2/3。
新生代分为Eden区和两个Surrivor区(2/10),Eden区占新生代的(8/10)
public class classLoaderTest { public static void main(String[] args) throws ClassNotFoundException { //父子关系 AppClassLoader<-ExtClassLoader<-BootStrap ClassLoader ClassLoader cl1=classLoaderTest.class.getClassLoader(); System.out.println("cl1->"+cl1); System.out.println("parent of cl1->"+cl1.getParent()); //打印出null,因为BootStrap ClassLoader由C++开发,是JVM虚拟机的一部分,本身不是JAVA类 System.out.println("grandparent of cl1->"+cl1.getParent().getParent()); ClassLoader cl2=String.class.getClassLoader(); //String 和 java.util.List 这两个类是JVM启动的时候就会默认加载到JVM进程中的,它们的ClassLoader打印出来都是null //这就说明这个null是jvm内存还空无一物的时候,由底层C++初始化的ClassLoader System.out.println("cl2->"+cl2); System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader()); } }
JAVA的类加载器:AppClassLoader<-ExtClassLoader<-BootStrap ClassLoader
每种类加载器都有自己的加载目录
/** * Loads the class with the specified <a href="#name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * <tt>resolve</tt> flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting <tt>Class</tt> object. * * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * <p> Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method * during the entire class loading process. * * @param name * The <a href="#name">binary name</a> of the class * * @param resolve * If <tt>true</tt> then resolve the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found */ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
根据上面代码解读类加载机制:
每次加载过的类都是有缓存的(通过底层C+代码做的),如果缓存中有,直接跳过if (c == null) ,如果缓存中没有,就去找parent if (parent != null)
,就让父类代替加载这个类,父类也会去找是否自己被加载过
parent为空,就进入下面的BootstrapClassLoader
c = findBootstrapClassOrNull(name);
如果还是没有找到,就自己去找
双亲委派机制的核心如下图
要加载一个类(AppClassLoader),如果已经加载过,就直接返回,否则去找他的父类(ExtClassLoader),看父类有没有加载过,有直接返回,没有,就继续找ExtClassLoader的父类BootStrap ClassLoader,看有没有加载过,有就直接返回,没有的话,就如下图的右边部分,去BootStrap ClassLoader规定的路径去找BootStrap ClassLoader,找不到的话,就向下,去ExtClassLoader的规定加载路径找,再找不到,就交由应用程序自己加载,就去Class.path属性下去找
总结:向上委托查找,向下委托加载
双亲委派的作用:对java底层起保护作用,保护java底层的类不会被应用程序覆盖
比如,你自己再定义一个String类,可以写自己的main方法,如果可以这样,java就乱掉了,因为String还要进行其他的操作,就是对基础的类进行保护
如果我们自己写了一个String类,那么加载的时候,类加载器不会直接找自定义的String,而是向上找,一直找到BootStrap ClassLoader,因为BootStrap ClassLoader里面有一个String类,而不是去class,path下面找String
类加载过程:加载 ->连接 ->初始化
加载:通过双亲委派机制把class文件(字节码)加载到jvm内存之中,并映射成jvm认可的数据结构
连接 :resolveClass方法,是一个native方法
连接分为3个阶段:
然后初始化
类加载完执行静态代码块和构造方法
类加载
类的初始化:静态属性,静态代码块是在类的加载过程中加载的
对象的初始化:普通属性,构造方法是在对象创建的时候初始化的
对象的创建:
1 用户创建一个对象,JVM首先需要去方法区找对象的类型信息,然后再创建对象
2 JVM要实例化一个对象,首先要在堆中先创建一个对象,会涉及半初始化状态,
3:对象首先会分配在堆内存的新生代的Eden区,经过一次MinorGC(发生在新生代的GC),对象如果存活,就会从Eden区
进入Surrivor区
,再经过一次MinorGC,如果是垃圾,就被清理掉,如果不是,就移到另一个Surrivor区
,并且给它的年龄加1,就这样,在两个sURRIVOR区之间来回拷贝,每移动一次,年龄加1。经过多次MinorGC发现这个对象依然存活,超过一定年龄之后,对象就会转入老年代
,进入老年代之后,如果多次GC还是一直存活,直到方法运行结束,栈中的引用被移出, 没有引用指向这个对象,那么就会被老年代的GC进程回收
4 当方法执行结束后,栈中的指针会移除掉
5堆中的对象,经过FullGC(发生在老年代),就会被标记为垃圾,被GC线程清理掉
GC是推动对象在JVM内存中移动的关键
不一定,在某些情况下,为了优化,会创建在栈区,当对象只在一个地方用的时候,会优先在栈中分配,在栈里分配,生命周期就变得比较简单,方法一结束,栈帧就没了,对象就移除了,就不用GC介入了
正常情况下,对象是要在堆上进行内存分配的,但是随着编译器优化技术的成熟,虽然虚拟机规范是这样要求的,但是具体实现上还是有些差别的。
如HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。
————————————————
版权声明:本文为CSDN博主「吃饭的时候记得叫我啊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wtmdcnm/article/details/104921377
JVM系列 - Java对象都是创建在堆内存中的吗?
比如拆房子,需要先把要拆的地方标记一个拆字,然后拆迁队就相当于垃圾回收器去拆
有两种定位垃圾的方式
1:引用计数:给堆内存中的每个对象记录一个应用个数,有指针指向它就证明它有用,当有另一个对象来引用它,引用计数加1,引用它新的对象被清除后,它的引用计数减1,引用计数大于0就有用。引用个数为0就认为是垃圾(这是早期JDK中使用的方式)
2:根可达算法:在内存中,从引用根对象(GC Root
)向下一直找引用,能找到的就是存活对象,找不到的,即便有互相的引用,即便引用个数不为0也可以认为是垃圾
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
垃圾回收算法是组成垃圾回收器的工具
2:Copying 拷贝算法:为了解决标记清除算法的内存碎片问题,就产生了拷贝算法
内存分为大小相等的两半。每次只使用一半,垃圾回收之后,把上一半存活的对象到下一半,拷贝完之后,上面这一块内存就可以全部清除,这样内存全部都是连续的,就不会有内存碎片了
弊端:浪费空间,这么大的内存只能用一半?而且效率跟存活对象的个数有关,如果存活对象多,那么移动的次数就会非常多,因为所有的存活对象都要移动
3:MarkCompack 标记压缩算法:为了解决拷贝算法的空间利用率问题,标记之后,在回收的阶段,不是直接清理内存,而是把存活的对象往一端移动 ,然后将端边界以外的所有内存直接清除,就不用分两半了,所有内存都可以用,也解决了拷贝算法每个存活对象必须移动的问题
这三种算法各有利弊,各有各自的适合场景
Stop the world .是在垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态(比如要打扫卫生,就让房间里的其他人先停止制造垃圾,等我打扫完)在STW状态下,java的所有线程都是停止执行的,GC线程除外,native方法可以执行(因为调用的是底层C++的代码,跟JVM没有太大的关系),但是这些native方法不能与JVM进行交互,GC各种算法优化的重点,就是减少STW,同时,这也是JVM调优的重点。
Serial:串行,通知所有线程,我要垃圾回收了,开启一个GC线程,垃圾回收之后,这些线程再执行。就像踢足球,需要GC时,直接暂停,GC完了再继续
弊端:只开启了一个GC线程,效率较低,Serial是比较早期的垃圾回收器,在单CPU还可以,在多CPU架构下,性能就会下降,通常只适用于几十兆的内存空间(就像一个人打扫房子,房子大了就打扫不过来)
Parallel:并行,在Serial基础上,多开启几个GC线程进行垃圾回收
PS(Pprallel Scavenge)+PO(Parallel old)是jdk1.8默认的垃圾回收器
在多CPU的架构下,性能会比Serial高很多(很多人一起扫房子)
CMS(Concurrent Mark Sweep并行标记清除):我们发现,很多人一起扫,也扫不过来
CMS是很重要的垃圾回收器,促进了垃圾回收器从分代到不分代的转变过程
核心思想是将STW打散,让一部分GC线程与用户线程并发执行,整个GC过程分为4个阶段
1:初始标记阶段:STW只标记出根对象直接引用的对象
2:并发标记:继续标记其他对象
3:重新标记:STW对并发执行阶段的对象进行重新标记
4:并发清除:并行,将产生的垃圾清除。清除过程中应用程序又会不断产生新的垃圾,这些垃圾叫浮动垃圾,留到下一次GC过程中清除
G1:Garbage First:垃圾优先
对于这个垃圾回收器,堆内存不再分新生代和老年代,而是把堆内存分为一个一个的小块,每一个小块叫做Region。逻辑上不分代,实际上是分代的。每个Region可以隶属于不同的年代
GC分为四个阶段
1:初始标记,标记出GC Root直接引用的对象。STW
2:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region
RSet:Region中的一个小的数据结构,在每一个Region当中会有一个RSet,记录与当前Region有引用关系的Region。 是并发的,没有STW
3:并发标记阶段:跟CMS差不多,不过遍历的范围不再是整个OLD区,只需要标记第2步标记出来的Region
4:重新标记:跟CMS的重新标记差不多,方法不同。把并发标记过程中产生变化了的对象重新标记一下
5:垃圾清理:采用复制算法,直接将整个Region的信息复制到一个新的Region,这个阶段G1只选则垃圾较多的Region进行清理,而不是全部清理
ZGC和shennandoah逗孩子啊优化之中,是未来的垃圾回收器
三色标记是CMS中用来标记的机制,是CMS的核心算法
是一种逻辑上的抽象,将每个内存对象分成三种颜色,黑色
表示自己和成员变量都已经标记完毕,灰色
表示自己标记完了,成员变量还没有标记完,白色
表示自己未标记完
错标记:
漏标记
因为内存逐渐变大
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程序的运行速度
Java面试200道系统教程助力拿下年薪60万!并发网络通信JVM面试缓存/微服务/spring/MySQL
图解Java 垃圾回收机制
携程面试官问我怎么划分 Java 虚拟机内存区域,相见恨晚!
携程面试官竟然问我 Java 虚拟机栈!
Java虚拟机(JVM)面试题(2020最新版)
JVM相关问题整理
Java垃圾回收、引用计数法、根可达算法
java垃圾回收机制–可达性算法
常见JVM面试题及答案整理
JVM面试题总结
用最直接的大白话来聊一聊Java对象的GC垃圾回收以及Java对象在内存中的那些事
深入理解 JVM 垃圾回收机制及其实现原理
JVM架构和GC垃圾回收机制(JVM面试不用愁)
Java虚拟机(JVM)你只要看这一篇就够了!
垃圾回收和GC调优
深入理解JVM的垃圾回收机制