做网站哪个效果好,网站建设与管理的未来规划方案,wordpress站点标题图片,公众号文章制作模板0x01. 内存模型以及分区#xff0c;需要详细到每个区放什么。
栈区#xff1a;
栈分为java虚拟机栈和本地方法栈
重点是Java虚拟机栈#xff0c;它是线程私有的#xff0c;生命周期与线程相同。
每个方法执行都会创建一个栈帧#xff0c;用于存放局部变量表#xff0…0x01. 内存模型以及分区需要详细到每个区放什么。
栈区
栈分为java虚拟机栈和本地方法栈
重点是Java虚拟机栈它是线程私有的生命周期与线程相同。
每个方法执行都会创建一个栈帧用于存放局部变量表操作栈动态链接方法出口等。每个方法从被调用直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
通常说的栈就是指局部变量表部分存放编译期间可知的8种基本数据类型及对象引用和指令地址。局部变量表是在编译期间完成分配当进入一个方法时这个栈中的局部变量分配内存大小是确定的。
会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误虚拟机栈动态扩展当扩展无法申请到足够的内存空间时候抛出OutOfMemoneyError。
本地方法栈为虚拟机使用到本地方法服务native
堆区
堆被所有线程共享区域在虚拟机启动时创建唯一目的存放对象实例。
堆区是gc的主要区域通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区最要放新创建对象From survivor 和 To survivor 保存gc后幸存下的对象默认情况下各自占比 8:1:1。
不过很多文章介绍分为3个区块把方法区算着为永久代。这大概是基于Hotspot虚拟机划分然后比如IBM j9就不存在永久代概论。不管怎么分区都是存放对象实例。
会有异常OutOfMemoneyError
方法区
被所有线程共享区域用于存放已被虚拟机加载的类信息常量静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代permanment generation
垃圾回收很少光顾这个区域不过也是需要回收的主要针对常量池回收类型卸载。
常量池用于存放编译期生成的各种字节码和符号引用常量池具有一定的动态性里面可以存放编译期生成的常量运行期间的常量也可以添加进入常量池中比如string的intern()方法。
程序计数器
当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令比如循环分支跳转异常处理线程恢复等都是依赖计数器来完成。
Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置每条线程都需要一个独立的程序计数器所以它是线程私有的。
唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。
0x02. 堆里面的分区Edensurvivalfrom to老年代各自的特点
1.JVM中堆空间可以分成三个大区新生代、老年代、永久代。
2.新生代可以划分为三个区Eden区两个幸存区。
在JVM运行时可以通过配置以下参数改变整个JVM堆的配置比例
JVM运行时堆的大小
-Xms堆的最小值
-Xmx堆空间的最大值
复制
新生代堆空间大小调整 -XX:NewSize新生代的最小值-XX:MaxNewSize新生代的最大值-XX:NewRatio设置新生代与老年代在堆空间的大小-XX:SurvivorRatio新生代中Eden所占区域的大小
复制
永久代大小调整 -XX:MaxPermSize
复制
其他
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收但是仍然没有被回收
复制
复制Copying算法
将内存平均分成A、B两块算法过程
新生对象被分配到A块中未使用的内存当中。当A块的内存用完了 把A块的存活对象对象复制到B块。清理A块所有对象。新生对象被分配的B块中未使用的内存当中。当B块的内存用完了 把B块的存活对象对象复制到A块。清理B块所有对象。goto 1。
优点简单高效。
缺点内存代价高有效内存为占用内存的一半。
对复制算法进一步优化使用Eden/S0/S1三个分区
平均分成A/B块太浪费内存采用Eden/S0/S1三个区更合理空间比例为Eden:S0:S18:1:1有效内存即可分配新生对象的内存是总内存的9/10。
算法过程
EdenS0可分配新生对象对EdenS0进行垃圾收集存活对象复制到S1。清理EdenS0。一次新生代GC结束。EdenS1可分配新生对象对EdenS1进行垃圾收集存活对象复制到S0。清理EdenS1。二次新生代GC结束。goto 1。
默认Eden:S0:S18:1:1,因此新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问为什么不直接分成两个区一个区占9/10,另一个区占1/10
这样做的原因大概有以下几种
1.S0与S1的区间明显较小有效新生代空间为EdenS0/S1因此有效空间就大增加了内存使用率
2.有利于对象代的计算当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后会将其分到老年代中设想一下如果没有S0/S1,直接分成两个区该如何计算对象经过了多少次GC还没被释放,你可能会说在对象里加一个计数器记录经过的GC次数或者存在一张映射表记录对象和GC次数的关系是的可以但是这样的话会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了~~~
0x03. 对象创建方法对象的内存分配对象的访问定位。
创建
1. 类加载检查
JVM遇到一条new指令时首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已被加载、解析和初始化过。
如果没有那必须先执行相应的类的加载过程。
2. 对象分配内存
对象所需内存的大小在类加载完成后便完全确定对象内存布局为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
根据Java堆中是否规整有两种内存的分配方式Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
指针碰撞(Bump the pointer)
Java堆中的内存是规整的所有用过的内存都放在一边空闲的内存放在另一边中间放着一个指针作为分界点的指示器分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如Serial、ParNew等收集器。
空闲列表(Free List)
Java堆中的内存不是规整的已使用的内存和空闲的内存相互交错就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表记录哪些内存块是可用的在分配的时候从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录。例如CMS这种基于Mark-Sweep算法的收集器。
3. 并发处理
对象创建在虚拟机中时非常频繁的行为即使是仅仅修改一个指针指向的位置在并发情况下也并不是线程安全的可能出现正在给对象A分配内存指针还没来得及修改对象B又同时使用了原来的指针来分配内存的情况。
同步
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
本地线程分配缓冲Thread Local Allocation Buffer, TLAB
把内存分配的动作按照线程划分为在不同的空间之中进行即每个线程在Java堆中预先分配一小块内存TLAB。哪个线程要分配内存就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时才需要同步锁定。
4. 内存空间初始化
虚拟机将分配到的内存空间都初始化为零值不包括对象头,如果使用了TLAB这一工作过程也可以提前至TLAB分配时进行。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用程序能访问到这些字段的数据类型所对应的零值。
注意类的成员变量可以不显示地初始化Java虚拟机都会先自动给它初始化为默认值。方法中的局部变量如果只负责接收一个表达式的值可以不初始化但是参与运算和直接输出等其它情况的局部变量需要初始化。
5. 对象设置
虚拟机对对象进行必要的设置例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
6. 执行init()
在上面的工作都完成之后从虚拟机的角度看一个新的对象已经产生了。但是从Java程序的角度看对象的创建才刚刚开始init()方法还没有执行所有的字段都还是零。
所以一般来说由字节码中是否跟随invokespecial指令所决定执行new指令之后会接着执行init()方法把对象按照程序员的意愿进行初始化这样一个真正可用的对象才算产生出来。
访问定位句柄或者直接指针。
0x04. GC的两种判定方法引用计数与引用链。
1.在JDK1.2之前使用的是引用计数器算法即当这个类被加载到内存之后就会产生方法区堆栈、程序计数
器等一系列信息当创建对象的时候为这个对象在堆栈空间中分配对象同时会产生一个引用计数器同时引
用计数器1当有新的引用时引用计数器继续1而当其中一个引用销毁时引用计数器-1当引用计数器减
为0的时候标志着这个对象已经没有引用了可以回收了但是这样会有一个问题
当我们的代码出现这样的情况时
a)ObjA.objObjB
b)ObjB.objObjA
这样的代码会产生如下引用情形objA指向objB而ObjB又指向objA这样当其他所有的引用都消失了之后objA
和objB还有一个相互的引用也就是说两个对象的引用计数器各为1而实际上这两个对象都已经没有额外的引用已经是垃圾了。
2.根搜索算法
根搜索算法是从离散数学中的图论引入的程序把所有的引用关系看做一张图从一个节点GC Root开始寻找对
应的引用节点找到这个节点之后继续寻找这个节点的引用节点当所有的引用节点寻找完毕之后剩余的节
点则被认为是没有被饮用到的节点即无用的节点。
目前Java中可作为GC Root的对象有
1.虚拟机栈中引用的对象本地变量表
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)。
java中存在的四种引用
1强引用
只要引用存在垃圾回收器永远不会回收。
2软引用
非必须引用内存溢出之前进行回收可以通过以下代码实现
Object objnew Object();
SoftReferenceObject sfnewSoftRerenceObject(obj);
objnull;
sf.get();//有时会返回null
这时候sf是对obj的一个软引用通过sf.get()方法可以取到这个对象当然这个对象被标记为需要回收的对象时
则返回null
软引用主要用于用户实现类似缓存的功能在内存不足的情况下直接通过软引用取值无需从繁忙的真实来源查
询数据提升速度当内存不足时自动删除这部分缓存数据从真实的来源查询这些数据。
3弱引用
第二次垃圾回收时回收可以通过如下代码实现
Object objnew Object();
WeakReferenceObject wfnewWeakReferenceObject(obj);
objnull;
wf.get();//有时会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收短时间内通过弱引用取对应的数据可以取到当执行过第二次垃圾回收时
将返回null。弱引用主要用于监控对象是否已经被标记为即将回收的垃圾可以通过弱引用的isEnQueues方法
返回对象是否被垃圾回收器标记。
4虚引用
垃圾回收时回收无法通过引用取到对象值可以通过如下代码实现
Object objnew Object();
PhantomReferenceObject pfnewPhantomReferenceObject(obj);
objnull;
pf.get();//永远是返回null
pf.isEnQueued();//返回从内从中已经删除。
虚引用是每次垃圾回收的时候都会被回收通过虚引用的get方法永远获取到的数据为null。
0x05. GC的三种收集方法标记清除、标记整理、复制算法的原理与特点分别用在什么地方如果让你优化收集方法有什么思路
垃圾回收算法
标记-清除算法Mark-Sweep
从根节点开始标记所有可达对象其余没有标记的即为垃圾对象执行清除。但回收后的空间是不连续的。
标记-清除算法采用从根集合进行扫描对存活的对象标记标记完毕后在扫描整个空间中未被标记的对象进
行回收。
标记-清除算法不需要进行对象的移动并且仅对不存活的对象进行处理在存活对象比较多的情况下极为高效
但由于标记-清除算法直接回收不存活的对象因此会造成内存碎片。
复制算法
复制算法采用从根集合扫描并将存活对象复制到一块新的没有使用过的空间中这种算法当控件存活的对象
比较少时极为高效但是带来的成本是需要一块内存交换空间进行对象的移动。也就是s0s1等空间。
标记-整理法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记但在清除时在回收不存活的对象占用的空间后
会将所有的存活对象网左端空闲空间移动并更新相应的指针。标记-整理算法是在标记-清除算法的基础上
又进行了对象的移动因此成本更高但是却解决了内存碎片的问题。
0x06. GC收集器有哪些CMS收集器与G1收集器的特点。
串行垃圾回收器Serial Garbage Collector
并行垃圾回收器Parallel Garbage Collector
并发标记扫描垃圾回收器CMS Garbage Collector
G1垃圾回收器G1 GarbageCollector
并发标记垃圾回收使用多线程扫描堆内存标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
当标记的引用对象在tenured区域
在进行垃圾回收的时候堆内存的数据被并发的改变。
相比并行垃圾回收器并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:USeParNewGC 打开并发标记扫描垃圾回收器。
G1垃圾回收器适用于堆内存很大的情况他将堆内存分割成不同的区域并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数–XX:UseG1GC 使用G1垃圾回收器
0x07. Minor GC与Full GC分别在什么时候发生
从年轻代空间包括 Eden 和 Survivor 区域回收内存被称为 Minor GC。
这一定义既清晰又易于理解。但是当发生Minor GC事件的时候有一些有趣的地方需要注意到
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC比如当 Eden 区满了。所以分配率越高越频繁执行 Minor GC。
内存池被填满的时候其中的内容全部会被复制指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时不会影响到永久代。从永久代到年轻代的引用被当成 GC roots从年轻代到永久代的引用在标记阶段被直接忽略掉。
质疑常规的认知所有的 Minor GC 都会触发“全世界的暂停stop-the-world”停止应用程序的线程。对于大部分应用程序停顿导致的延迟都是可以忽略不计的。
其中的真相就是大部分 Eden 区中的对象都能被认为是垃圾永远也不会被复制到 Survivor 区或者老年代空间。
如果正好相反Eden 区大部分新生对象不符合 GC 条件Minor GC 执行时暂停的时间将会长很多。
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。
大家应该注意到目前这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的
Minor GC 清理年轻带内存应该被设计得简单
Major GC 是清理永久代。
Full GC 是清理整个堆空间—包括年轻代和永久代。
很不幸实际上它还有点复杂且令人困惑。首先许多 Major GC 是由 Minor GC 触发的所以很多情况下将这两种 GC 分离是不太可能的。另一方面许多现代垃圾收集机制会清理部分永久代空间所以使用“cleaning”一词只是部分正确。
这使得我们不用去关心到底是叫 Major GC 还是 Full GC大家应该关注当前的 GC 是否停止了所有应用程序的线程还是能够并发的处理而不用停掉应用程序的线程。
0x08. 几种常用的内存调试工具jmap、jstack、jconsole
0x09. 类加载的五个过程加载、验证、准备、解析、初始化
加载
在加载阶段虚拟机主要完成三件事
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象作为方法区域数据的访问入口。
验证
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范不会给JVM造成危害。如果验证失败就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段
1.文件格式验证验证字节流文件是否符合Class文件格式的规范并且能被当前虚拟机正确的处理。
2.元数据验证是对字节码描述的信息进行语义分析以保证其描述的信息符合Java语言的规范。
3.字节码验证主要是进行数据流和控制流的分析保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证符号引用验证发生在虚拟机将符号引用转化为直接引用的时候这个转化动作将在解析阶段中发生。
准备
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量static修饰的变量而不包括类的实例变量。对已非final的变量JVM会将其设置成“零值”而不是其赋值语句的值
pirvate static int size 12;
那么在这个阶段size的值为0而不是12。final修饰的类变量将会赋值成真实的值。
解析
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化
在准备阶段类变量已经经过一次初始化了在这个阶段则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块构造函数父类的初始化等。
至于使用和卸载阶段阶段这里不再过多说明使用过程就是根据程序定义的行为执行卸载由GC完成。
0x10. 双亲委派模型Bootstrap ClassLoader、ExtensionClassLoader、ApplicationClassLoader。
类加载器按照层次从顶层到底层分为以下三种
1启动类加载器BootstrapClassLoader
这个类加载器负责将存放在JAVA_HOME/lib下的或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
2扩展类加载器ExtensionClassLoader
这个加载器负责加载JAVA_HOME/lib/ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库开发者可以直接使用扩展类加载器
3应用程序类加载器ApplicationClassLoader
这个加载器是ClassLoader中getSystemClassLoader()方法的返回值所以一般也称它为系统类加载器。它负责加载用户类路径Classpath上所指定的类库可直接使用这个加载器如果应用程序没有自定义自己的类加载器一般情况下这个就是程序中默认的类加载
类加载的双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现而是都使用组合关系来复用父加载器的代码
工作过程
如果一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此因此所有的加载请求最终都应该传递到顶层的启动类加载器中
只有当父类加载器反馈自己无法完成这个请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载。
好处
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object它放在rt.jar中无论哪一个类加载器要加载这个类最终都是委派给启动类加载器进行加载因此Object类在程序的各种类加载器环境中都是同一个类。
判断两个类是否相同是通过classloader.class这种方式进行的所以哪怕是同一个class文件如果被两个classloader加载那么他们也是不同的类。
实现自己的加载器
只需要继承ClassLoader并覆盖findClass方法。
在调用loadClass方法时会先根据委派模型在父加载器中加载如果加载失败则会调用自己的findClass方法来完成加载。
0x11. 分派静态分派与动态分派
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派其典型应用是方法重载根据参数的静态类型来定位目标方法。
静态分派发生在编译阶段因此确定静态分派的动作实际上不是由虚拟机执行的。
动态分派
在运行期根据实际类型确定方法执行版本。