2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Les objets en Java sont-ils alloués dans la mémoire tas ?
D'accord, c'est trop abstrait. Soyons plus précis. Voyons où est allouée la mémoire de l'objet suivant ?
- public void test() {
- Object object = new Object();
- }
- 这个方法中的object对象,是在堆中分配内存么?
Dites le résultat : l'objet peut allouer de la mémoire sur la pile ou sur le tas.
Voici le point clé : dans la mise en œuvre de la JVM, afin d'améliorer les performances de la JVM et d'économiser de l'espace mémoire, la JVM fournit une fonctionnalité appelée « analyse d'évasion » L'analyse d'évasion est une technologie d'optimisation relativement avant-gardiste à l'heure actuelle. Machine virtuelle Java, et c'est aussi un JIT Une technique d'optimisation très importante. jdk6 a seulement commencé à introduire cette technologie, jdk7 a commencé à activer l'analyse d'échappement par défaut, jdk8 a commencé à améliorer l'analyse d'échappement et l'a activée par défaut. Jusqu'au JDK 9, l'analyse d'échappement sera utilisée comme méthode d'optimisation par défaut et aucun paramètre de compilation spécial. sont requis.
Comprenez maintenant la phrase "l'objet peut allouer de la mémoire sur la pile ou allouer de la mémoire sur le tas". Avant jdk7, l'objet ici doit allouer de la mémoire sur le tas dans jdk7 et 8, il est possible d'allouer de la mémoire sur la pile, car jdk7 ; seule l'analyse d'échappement a commencé à être prise en charge ; jdk9 est très probablement alloué sur la pile (l'objet ici est très petit), car jdk9 ne prend en charge et n'active réellement que l'analyse d'échappement par défaut.
Avec le développement des compilateurs JIT (compilateurs juste à temps) et la maturité progressive de la technologie d'analyse d'échappement, la technologie d'allocation de pile et d'optimisation du remplacement scalaire fera que « tous les objets seront alloués sur le tas » deviendra moins absolu en Java. Dans la machine virtuelle, les objets se voient allouer de la mémoire dans le tas, mais il existe un cas particulier, c'est-à-dire que si après analyse d'échappement, il s'avère qu'un objet ne s'échappe pas de la méthode, il peut être optimisé pour être alloué sur la pile. Lorsque la méthode est exécutée Une fois terminée, le cadre de pile est sauté et l'objet est libéré. Cela élimine le besoin d'allouer de la mémoire sur le tas et de subir un garbage collection (.Hotspot ne le fait pas actuellement). Il s’agit également de la technologie de stockage hors tas la plus courante.
Après JDK 6u23 (version majeure mémorisable JDK7), l'analyse d'échappement est activée par défaut dans Hotspot. Si vous utilisez une version antérieure, vous pouvez afficher l'analyse d'échappement via l'option "-XX:+DoEscapeAnalysis". Afficher les résultats du filtre pour l’analyse des évasions.
Hotspot implémente le remplacement scalaire via une analyse d'échappement (les objets non échappés sont remplacés par des scalaires et des agrégats, ce qui peut améliorer l'efficacité du code), mais les objets non échappés alloueront toujours de la mémoire sur le tas, on peut donc toujours dire que tous les objets sont alloués. mémoire sur le tas.
De plus, profondément personnalisé basé sur Open JDKMachine virtuelle TaoBao, parmi lesquels la technologie innovante GCIH (GC invisible heap) implémente le déplacement d'objets hors tas avec de longs cycles de vie du tas vers l'extérieur du tas, et GC ne gère pas les objets Java à l'intérieur de GCIH, réduisant ainsi la fréquence de recyclage du GC et améliorant l'objectif. de l'efficacité de la récupération du GC.
Empiler: Lorsque chaque méthode est exécutée, un cadre de pile sera créé en même temps pour stocker des informations telles que des tables de variables locales, des piles d'opérations, des connexions dynamiques, des sorties de méthode, etc. Le processus depuis chaque méthode appelée jusqu'à la fin de l'exécution correspond au processus d'un cadre de pile depuis son insertion dans la pile jusqu'à sa sortie de la pile dans la pile de la machine virtuelle.
tas:Lorsqu'un objet est instancié, l'objet est alloué sur le tas et une référence au tas est poussée sur la pile.
s'échapper:Lorsqu'un pointeur vers un objet est référencé par plusieurs méthodes ou threads, nous disons que le pointeur s'échappe. Généralement, les objets renvoyés et les variables globales s'échappent généralement.
Analyse d'évasion :La méthode utilisée pour analyser ce phénomène d’évasion est appelée analyse d’évasion.
Optimisation de l'analyse des évasions - allocation sur la pile :L'allocation sur la pile signifie que l'instance générée par la variable locale dans la méthode (aucun échappement ne se produit) est allouée sur la pile et n'a pas besoin d'être allouée dans le tas. Une fois l'allocation terminée, l'exécution continue dans la pile d'appels. Enfin, le thread se termine, l'espace de pile est recyclé et les objets variables locaux sont également recyclés.
Réponse : Pas nécessairement.
Si les conditions d'analyse d'échappement sont remplies, un objet peut être alloué sur la pile.Réduisez l’allocation de mémoire tas et la pression du GC.La mémoire de la pile étant limitée, si l'objet remplit les conditions de remplacement scalaire,Une autre opération est effectuée sur le sujet pour le diviser en plusieurs parties.La méthode spécifique de remplacement scalaire est la suivante : la JVM divisera davantage l'objet et le décomposera en plusieurs variables membres utilisées par cette méthode.Ainsi, l'objectif d'une meilleure utilisation de la mémoire de pile et des registres est atteint.
Il s'agit d'un algorithme d'analyse du flux de données global interfonctionnel qui peut réduire efficacement la charge de synchronisation et la pression d'allocation du tas de mémoire dans les programmes Java. Grâce à l'analyse d'échappement, le compilateur Java Hotspot peut analyser la plage d'utilisation de la référence d'un nouvel objet et décider s'il doit allouer cet objet au tas.
Le comportement de base de l'analyse d'échappement consiste à analyser la portée dynamique des objets :
Dans le principe d'optimisation du compilateur de langage informatique, l'analyse d'échappement fait référence à la méthode d'analyse de la plage dynamique des pointeurs. Elle est liée à l'analyse du pointeur et à l'analyse de forme du principe d'optimisation du compilateur. Lorsqu'une variable (ou un objet) est allouée dans une méthode, son pointeur peut être renvoyé ou référencé globalement, qui sera référencé par d'autres méthodes ou threads. Ce phénomène est appelé échappement de pointeur (ou référence). En termes simples, si le pointeur d'un objet est référencé par plusieurs méthodes ou threads, alors nous appelons le pointeur (ou l'objet) de l'objet Escape (car à ce moment, l'objet s'échappe de la portée locale de la méthode ou du thread).
Brève description : « Analyse d'échappement : une analyse statique qui détermine la plage dynamique des pointeurs. Elle peut analyser où dans le programme le pointeur est accessible. » Dans le contexte de la compilation juste à temps de JVM, l'analyse d'échappement déterminera si le pointeur est accessible. l'objet nouvellement créé s'échappe.
La base de la compilation juste à temps pour déterminer si un objet s'échappe : l'une est de savoir si l'objet est stocké dans le tas (champ statique ou champ d'instance de l'objet dans le tas), et l'autre est de savoir si l'objet est passé dans code inconnu.
Escape Analysis est actuellement une technologie d'optimisation relativement avant-gardiste dans les machines virtuelles Java, comme l'analyse des relations d'héritage de types, ce n'est pas un moyen d'optimiser directement le code, mais une technologie d'analyse qui fournit une base à d'autres moyens d'optimisation.
Analyse d'évasion : il s'agit d'une technologie d'optimisation JIT très importante, utilisée pour déterminer si l'objet sera accédé en dehors de la méthode, c'est-à-dire pour échapper à la portée de la méthode. L'analyse d'échappement est une étape du compilateur JIT. Grâce à JIT, nous pouvons déterminer quels objets peuvent être limités à l'utilisation à l'intérieur de la méthode et ne s'échapperont pas vers l'extérieur, par exemple en les allouant sur la pile au lieu du tas. Ou effectuez un remplacement scalaire pour diviser un objet en plusieurs types de base pour le stockage. Il s'agit d'un algorithme d'analyse du flux de données global interfonctionnel qui peut réduire efficacement la charge de synchronisation, l'allocation du tas de mémoire et la pression du garbage collection dans les programmes Java. Grâce à l'analyse d'échappement, le compilateur Java Hotspot peut analyser la plage d'utilisation de la référence d'un nouvel objet et décider s'il doit allouer cet objet au tas.
L'analyse d'échappement se concentre principalement sur les variables locales pour déterminer si les objets alloués sur le tas ont échappé au champ d'application de la méthode. Il est associé à l'analyse des pointeurs et à l'analyse de la forme des principes d'optimisation du compilateur. Lorsqu'une variable (ou un objet) est allouée dans une méthode, son pointeur peut être renvoyé ou référencé globalement, qui sera référencé par d'autres méthodes ou threads. Ce phénomène est appelé échappement de pointeur (ou référence). En termes simples, si le pointeur d'un objet est référencé par plusieurs méthodes ou threads, alors nous disons que le pointeur de l'objet s'est échappé. Une conception correcte de la structure du code et de l'utilisation des données permet de mieux utiliser l'analyse d'échappement pour optimiser les performances du programme. Nous pouvons également réduire la surcharge liée à l'allocation d'objets sur le tas et améliorer l'utilisation de la mémoire grâce à l'analyse d'échappement.
L'analyse d'évasion est une technique utilisée pour déterminer si un objet s'est échappé en dehors du champ d'application d'une méthode au cours de sa durée de vie. Dans le développement Java, l'analyse d'échappement est utilisée pour déterminer le cycle de vie et la portée des objets afin d'effectuer l'optimisation correspondante et d'améliorer les performances du programme et l'efficacité de l'utilisation de la mémoire.
Lorsqu'un objet est créé, il peut être utilisé dans une méthode, ou il peut être transmis à d'autres méthodes ou threads et continuer à exister en dehors de la méthode. Si l'objet n'échappe pas à la portée de la méthode, la JVM peut l'allouer sur la pile au lieu du tas, évitant ainsi la surcharge liée à l'allocation de mémoire du tas et au garbage collection.
- 关于逃逸分析的论文在1999年就已经发表,但直到Sun JDK 1.6才实现了逃逸分析,而且直到现在这项优化尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。如果要完全准确的判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。这是一个相对高耗时的过程,如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。
- 在测试结果中,实施逃逸分析后的程序在MicroBenchmarks中往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即使编译的收益)有所下降,所以在很长的一段时间里,即使是Server Compiler,也默认不开启逃逸分析(在JDK 1.6 Update 23的Server Compiler中才开始默认开启了逃逸分析),甚至在某些版本(如JDK 1.6 Update 18)中还曾经短暂的完全禁止了这项优化。
- 如果有需要,并且确认对程序运行有益,用户可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析,开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可以使用参数-XX:EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消除,使用参数-XX:PrintEliminateAllocations查看标量的替换情况。
- 尽管目前逃逸分析的技术仍不是十分成熟,但是他却是即时编译器优化技术的一个重要的方向,在今后的虚拟机中,逃逸分析技术肯定会支撑起一系列使用有效的优化技术。
Le principe de base de l'analyse d'échappement JVM est de déterminer la situation d'évasion de l'objet grâce à deux méthodes d'analyse : statique et dynamique.
Dans le système de compilation Java, le processus de transformation d'un fichier de code source Java en une instruction machine exécutable par ordinateur nécessite deux étapes de compilation :
La première section de compilation fait référence au compilateur frontalFichier .javaconverti enfichier .classe (fichier de bytecode). Les produits du compilateur frontal peuvent être Javac du JDK ou le compilateur incrémentiel d'Eclipse JDT.
Lors de la deuxième étape de compilation, la JVM interprète le bytecode et le traduit en instructions machine correspondantes, lit le bytecode un par un, puis l'interprète et le traduit en code machine un par un.
Évidemment, en raison du processus intermédiaire d’interprétation, sa vitesse d’exécution sera inévitablement beaucoup plus lente que celle d’un programme de bytecode binaire exécutable. C'est la fonction de l'interpréteur JVM traditionnel (Interpreter).
Afin de résoudre ce problème d’efficacité, la technologie JIT (Just In Time Compiler) a été introduite.
Après l'introduction de la technologie JIT, les programmes Java sont toujours interprétés et exécutés via l'interpréteur, c'est-à-dire que le corps principal est toujours interprété et exécuté, mais les liens intermédiaires sont partiellement supprimés.
Compilateur JIT (Just-in-timeCompiler) compilation juste à temps. La première solution d'implémentation Java consistait en un ensemble de traducteurs (interprètes) qui traduisaient chaque instruction Java en une instruction de microprocesseur équivalente et les exécutaient séquentiellement selon l'ordre des instructions traduites, car une instruction Java pouvait être traduite en une douzaine ou des dizaines de instructions équivalentes du microprocesseur, ce mode s’exécute très lentement.
Lorsque la JVM constate qu'une certaine méthode ou un certain bloc de code s'exécute particulièrement fréquemment, elle le considère comme un « Hot Spot Code ». Ensuite, JIT traduira une partie du « code chaud » en code machine lié à la machine locale, l'optimisera, puis mettra en cache le code machine traduit pour la prochaine utilisation.
Où mettre en cache le code machine traduit ? Ce cache est appelé Code Cache. On peut constater que les méthodes permettant d'obtenir une concurrence élevée entre les applications JVM et WEB sont similaires et qu'elles utilisent toujours une architecture de cache.
Lorsque la JVM rencontre le même code chaud la prochaine fois, elle ignore le lien intermédiaire d'interprétation, charge le code machine directement à partir du cache de code et l'exécute directement sans recompiler.
Par conséquent, l'objectif global de JIT est de découvrir le code chaud, et le code chaud est devenu la clé pour améliorer les performances. C'est ainsi qu'est né le nom hotspot JVM. C'est une quête permanente pour identifier le code chaud et l'écrire sur le nom.
Par conséquent, la stratégie globale de JVM est la suivante :
Pour la plupart des codes peu courants, nous n'avons pas besoin de passer du temps à les compiler en code machine, mais de les exécuter par interprétation et exécution ;
En revanche, pour le code chaud qui n’occupe qu’une petite partie, nous pouvons le compiler en code machine pour atteindre la vitesse d’exécution idéale.
L'émergence du JIT (just in time compilation) et la différence entre les interprètes
(1) L'interprète interprète le bytecode en code machine Même s'il rencontre le même bytecode la prochaine fois, il effectuera toujours une interprétation répétée.
(2) JIT compile certains bytecodes en codes machine et les stocke dans le cache de code la prochaine fois qu'il rencontrera le même code, il sera exécuté directement sans recompiler.
(3) L'interpréteur interprète le bytecode en code machine commun à toutes les plates-formes.
(4) JIT générera un code machine spécifique à la plate-forme en fonction du type de plate-forme.
JVM contient plusieurs compilateurs juste à temps, principalement C1 et C2, et Graal (expérimental).
Plusieurs compilateurs juste à temps optimiseront le bytecode et généreront du code machine
JVM divise l'état d'exécution en 5 niveaux :
Niveau 0, Interprète
Niveau 1, compilé et exécuté à l'aide du compilateur juste-à-temps C1 (sans profilage)
Couche 2, compilée et exécutée à l'aide du compilateur juste-à-temps C1 (avec profilage de base)
Couche 3, compilée et exécutée à l'aide du compilateur juste-à-temps C1 (avec profilage complet)
Niveau 4, compilé et exécuté à l'aide du compilateur juste-à-temps C2
La JVM n'activera pas directement C2. Au lieu de cela, elle collecte d'abord l'état d'exécution du programme via la compilation C1, puis détermine s'il convient d'activer C2 en fonction des résultats de l'analyse.
En mode de compilation en couches, l'état d'exécution de la machine virtuelle est divisé en cinq couches, du simple au complexe, du rapide au lent.
Lors de la compilation, en plus de mettre en cache le code chaud pour accélérer le processus, JIT effectuera également de nombreuses optimisations sur le code.
Le but de certaines optimisations est deRéduire la pression d’allocation du tas de mémoire , l'une des techniques importantes de l'optimisation JIT est appelée analyse d'échappement. Selon l'analyse d'échappement, le compilateur juste à temps optimisera le code comme suit pendant le processus de compilation :
Il vérifie la structure statique du code pour déterminer si l'objet peut s'échapper. Par exemple, lorsqu'un objet est affecté à une variable membre d'une classe ou renvoyé à une méthode externe, il peut être déterminé que l'objet s'échappe.
Il détermine si un objet s'échappe en observant le comportement des appels de méthode et des références d'objet. Par exemple, lorsqu'un objet est référencé par plusieurs threads, l'objet peut être considéré comme s'étant échappé.
L'analyse d'échappement effectue une analyse approfondie du code pour déterminer si l'objet s'est échappé en dehors de la portée de la méthode pendant la durée de vie de la méthode. Si l'objet ne s'échappe pas, la JVM peut l'allouer sur la pile au lieu du tas.
Un objet a trois états d’échappement : échappement global, échappement de paramètres et aucun échappement.
évasion globale(GlobalEscape) : c'est-à-dire que la portée d'un objet s'échappe de la méthode actuelle ou du thread actuel.
Généralement, il existe les scénarios suivants :
① L'objet est une variable statique
② L'objet est un objet qui s'est échappé
③ L'objet est utilisé comme valeur de retour de la méthode actuelle
Échappement des paramètres(ArgEscape) : c'est-à-dire qu'un objet est passé en tant que paramètre de méthode ou référencé par un paramètre, mais aucun échappement global ne se produit pendant le processus d'appel. Cet état est déterminé par le bytecode de la méthode appelée.
pas de fuite: Autrement dit, l'objet dans la méthode ne s'échappe pas.
L’exemple de code d’état d’échappement est le suivant :
- public class EscapeAnalysisTest {
-
- public static Object globalVariableObject;
-
- public Object instanceObject;
-
- public void globalVariableEscape(){
- globalVariableObject = new Object(); // 静态变量,外部线程可见,发生逃逸
- }
-
- public void instanceObjectEscape(){
- instanceObject = new Object(); // 赋值给堆中实例字段,外部线程可见,发生逃逸
- }
-
- public Object returnObjectEscape(){
- return new Object(); // 返回实例,外部线程可见,发生逃逸
- }
-
- public void noEscape(){
- Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸
- }
-
- }
1. Échappement de méthode : dans le corps d'une méthode, définissez une variable locale, qui peut être référencée par une méthode externe, par exemple transmise à une méthode en tant que paramètre d'appel, ou renvoyée directement en tant qu'objet. Ou bien, on peut comprendre que l'objet sort de la méthode.
Les échappements de méthode incluent :
- 我们可以用下面的代码来表示这个现象。
-
- //StringBuffer对象发生了方法逃逸
- public static StringBuffer createStringBuffer(String s1, String s2) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- return sb;
- }
- 上面的例子中,StringBuffer 对象通过return语句返回。
-
- StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
-
- 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
-
- 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
-
- 具体的代码如下:
-
- // 非方法逃逸
- public static String createString(String s1, String s2) {
- StringBuffer sb = new StringBuffer();
- sb.append(s1);
- sb.append(s2);
- return sb.toString();
- }
- 可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。
Comment déterminer rapidement si une analyse d'échappement a eu lieu ? Voyons si la nouvelle entité objet est appelée en dehors de la méthode.
- public class EscapeAnalysis {
-
- public EscapeAnalysis obj;
-
- /**
- * 方法返回EscapeAnalysis对象,发生逃逸
- * @return
- */
- public EscapeAnalysis getInstance() {
- return obj == null ? new EscapeAnalysis():obj;
- }
-
- /**
- * 为成员属性赋值,发生逃逸
- */
- public void setObj() {
- this.obj = new EscapeAnalysis();
- }
-
- /**
- * 对象的作用于仅在当前方法中有效,没有发生逃逸
- */
- public void useEscapeAnalysis() {
- EscapeAnalysis e = new EscapeAnalysis();
- }
-
- /**
- * 引用成员变量的值,发生逃逸
- */
- public void useEscapeAnalysis2() {
- EscapeAnalysis e = getInstance();
- }
- }
2. Échappement de thread : cet objet est accessible par d'autres threads, par exemple attribué à une variable d'instance et accessible par d'autres threads. L'objet a échappé au thread actuel.
L'analyse d'échappement peut apporter les stratégies d'optimisation suivantes aux programmes Java : allocation sur la pile, élimination de la synchronisation, remplacement scalaire et inlining de méthode ;
Paramètres liés à l’analyse des évasions :
- -XX:+DoEscapeAnalysis 开启逃逸分析
- -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
- -XX:+EliminateAllocations 开启标量替换
- -XX:+EliminateLocks 开启同步消除
- -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。
L'analyse d'échappement peut déterminer quels objets n'échapperont pas à la portée de la méthode et allouer ces objets sur la pile au lieu du tas. Les objets alloués sur la pile sont créés et détruits au cours du cycle de vie de l'appel de méthode sans garbage collection, améliorant ainsi l'efficacité de l'exécution du programme.
Dans des circonstances normales, les objets qui ne peuvent pas s'échapper occupent un espace relativement grand. Si l'espace sur la pile peut être utilisé, un grand nombre d'objets seront détruits à la fin de la méthode, réduisant ainsi la pression du GC.
Idées d'allocation sur la pileL'allocation sur la pile est une technologie d'optimisation fournie par la JVM.
L'idée est :
Problème : étant donné que la mémoire de la pile est relativement petite, les objets volumineux ne peuvent pas et ne conviennent pas à l'allocation sur la pile.
Activer l'allocation sur la pile
L'allocation sur la pile est basée sur l'analyse d'échappement et le remplacement scalaire, donc l'analyse d'échappement et le remplacement scalaire doivent être activés. Bien entendu, JDK1.8 est activé par défaut.
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
-
-
- 开启标量替换:-XX:+EliminateAllocations
- 关闭标量替换:-XX:-EliminateAllocations
- 显示标量替换详情:-XX:+PrintEliminateAllocations
Exemple d'allocation sur pile :
- 示例1
- import java.lang.management.ManagementFactory;
- import java.util.List;
- /**
- * 逃逸分析优化-栈上分配
- * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
- * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
- *虚拟机参数设置如下,表示做了逃逸分析 消耗时间在10毫秒以下
- * -server -Xmx10M -Xms10M
- -XX:+DoEscapeAnalysis -XX:+PrintGC
- *
- *虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
- * -server -Xmx10m -Xms10m
- -XX: -DoEscapeAnalysis -XX:+PrintGC
- * @author 734621
- *
- */
-
- public class OnStack{
- public static void alloc(){
- byte[] b=new byte[2];
- b[0]=1;
- }
- public static void main(String [] args){
- long b=System.currentTimeMillis();
- for(int i=0;i<100000000;i++){
- alloc();
- }
- long e=System.currentTimeMillis();
- System.out.println("消耗时间为:" + (e - b));
- List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
- for(String p : paramters){
- System.out.println(p);
- }
- }
- }
-
-
- 加逃逸分析的结果
- [GC (Allocation Failure) 2816K->484K(9984K), 0.0013117 secs]
- 消耗时间为:7
- -Xmx10m
- -Xms10m
- -XX:+DoEscapeAnalysis
- -XX:+PrintGC
-
-
-
- 没有加逃逸分析的结果如下:
- [GC (Allocation Failure) 3320K->504K(9984K), 0.0003174 secs]
- [GC (Allocation Failure) 3320K->504K(9984K), 0.0002524 secs]
- 消耗时间为:1150
- -Xmx10m
- -Xms10m
- -XX:-DoEscapeAnalysis
- -XX:+PrintGC
-
-
- 以上测试可以看出,栈上分配可以明显提高效率: 效率是不开启的1150/7= 160倍
-
-
- 示例2
- 我们通过举例来说明 开启逃逸分析 和 未开启逃逸分析时候的情况
-
- class User {
- private String name;
- private String age;
- private String gender;
- private String phone;
- }
- public class StackAllocation {
- public static void main(String[] args) throws InterruptedException {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 100000000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- System.out.println("花费的时间为:" + (end - start) + " ms");
-
- // 为了方便查看堆内存中对象个数,线程sleep
- Thread.sleep(10000000);
- }
-
- private static void alloc() {
- // 未发生逃逸
- User user = new User();
- }
- }
- 设置JVM参数,表示未开启逃逸分析
- -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
- 花费的时间为:664 ms
- 然后查看内存的情况,发现有大量的User存储在堆中
-
- 开启逃逸分析
- -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
- 然后查看运行时间,我们能够发现花费的时间快速减少,同时不会发生GC操作
- 花费的时间为:5 ms
- 在看内存情况,我们发现只有很少的User对象,说明User未发生逃逸,因为它存储在栈中,随着栈的销毁而消失。
Par comparaison, on voit
L'analyse d'échappement peut détecter que certains objets ne sont accessibles que par un seul thread et ne s'échappent pas vers d'autres threads. Par conséquent, les opérations de synchronisation inutiles peuvent être éliminées et la surcharge d'exécution des programmes multithread est réduite.
Les verrous de synchronisation consomment beaucoup de performances, donc lorsque le compilateur détermine qu'un objet ne s'est pas échappé, il supprime le verrou de synchronisation de l'objet. JDK1.8 active les verrous de synchronisation par défaut, mais il est basé sur l'activation de l'analyse d'échappement.
- -XX:+EliminateLocks #开启同步锁消除(JVM默认状态)
- -XX:-EliminateLocks #关闭同步锁消除
- 通过示例: 明显可以看到“逃逸分析和锁消除” 对性能的提升
-
- public void testLock(){
- long t1 = System.currentTimeMillis();
- for (int i = 0; i < 100_000_000; i++) {
- locketMethod();
- }
- long t2 = System.currentTimeMillis();
- System.out.println("耗时:"+(t2-t1));
- }
-
- public static void locketMethod(){
- EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
- synchronized(escapeAnalysis) {
- escapeAnalysis.obj2="abcdefg";
- }
- }
-
- 设置JVM参数,开启逃逸分析, 耗时:
- java -Xmx64m -Xms64m -XX:+DoEscapeAnalysis
-
- 设置JVM参数,关闭逃逸分析, 耗时:
- java -Xmx64m -Xms64m -XX:-DoEscapeAnalysis
-
- 设置JVM参数,关闭锁消除,再次运行
- java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
-
- 设置JVM参数,开启锁消除,再次运行
- java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks
-
-
Le coût de la synchronisation des threads est assez élevé et la conséquence de la synchronisation est une concurrence et des performances réduites.
Lors de la compilation dynamique d'un bloc synchronisé, le compilateur JIT peut utiliser l'analyse d'échappement pour déterminer si l'objet verrou utilisé par le bloc synchronisé n'est accessible que par un seul thread et n'a pas été libéré vers d'autres threads. Dans le cas contraire, le compilateur JIT désynchronisera cette partie du code lors de la compilation de ce bloc synchronisé. Cela peut grandement améliorer la simultanéité et les performances. Ce processus d'annulation de synchronisation est appelé omission de synchronisation, également appelé élimination de verrouillage.
- 例如下面的代码
-
- public void f() {
- Object hellis = new Object();
- synchronized(hellis) {
- System.out.println(hellis);
- }
- }
- 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
-
- public void f() {
- Object hellis = new Object();
- System.out.println(hellis);
- }
- 我们将其转换成字节码,此处发现,还是有同步锁的身影,是因为优化是在编译阶段的,在加载进内存后发生。
L'analyse d'échappement peut diviser un objet en plusieurs scalaires, tels que des types primitifs ou d'autres objets, et les attribuer à différents emplacements. Cela peut réduire la fragmentation de la mémoire et la surcharge d'accès aux objets, et améliorer l'efficacité de l'utilisation de la mémoire.
Tout d’abord, nous devons comprendre les scalaires et les agrégats. Les références aux types et objets de base peuvent être comprises comme des scalaires, et elles ne peuvent pas être décomposées davantage. La quantité qui peut être décomposée davantage est la quantité globale, telle que : objet.
L'objet est une quantité globale, qui peut être décomposée en scalaires et ses variables membres en variables discrètes. C'est ce qu'on appelle le remplacement scalaire.
De cette façon, si un objet ne s'échappe pas, il n'est pas du tout nécessaire de le créer. Seuls les scalaires membres qu'il utilise seront créés sur la pile ou le registre, ce qui économise de l'espace mémoire et améliore les performances de l'application.
La substitution scalaire est également activée par défaut dans JDK1.8, mais elle doit également être basée sur l'activation de l'analyse d'échappement.
Un scalaire est une donnée qui ne peut pas être décomposée en données plus petites. Le type de données primitif en Java est scalaire.
En revanche, les données qui peuvent être décomposées sont appelées un agrégat. Un objet en Java est un agrégat car il peut être décomposé en d'autres agrégats et scalaires.
- public static void main(String args[]) {
- alloc();
- }
- class Point {
- private int x;
- private int y;
- }
- private static void alloc() {
- Point point = new Point(1,2);
- System.out.println("point.x" + point.x + ";point.y" + point.y);
- }
- 以上代码,经过标量替换后,就会变成
-
- private static void alloc() {
- int x = 1;
- int y = 2;
- System.out.println("point.x = " + x + "; point.y=" + y);
- }
Au stade JIT, s'il s'avère grâce à l'analyse d'échappement qu'un objet ne sera pas accessible par le monde extérieur, alors après l'optimisation JIT, l'objet sera désassemblé en plusieurs variables membres qu'il contient et remplacé. Ce processus est un remplacement scalaire.
On peut voir qu'après analyse d'évasion, il a été constaté que la quantité globale Point ne s'est pas échappée, elle a donc été remplacée par deux scalaires. Alors, quels sont les avantages de la substitution scalaire ? Autrement dit, cela peut réduire considérablement l’utilisation de la mémoire tas. Parce qu’une fois qu’il n’est plus nécessaire de créer des objets, il n’est plus nécessaire d’allouer de la mémoire tas. La substitution scalaire fournit une bonne base pour l'allocation sur la pile.
Tests d'analyse d'évasion
- 逃逸分析测试
- 代码如下,大致思路就是 for 循环 1 亿次,循环体内调用外部的 allot() 方法,而 allot() 方法的作用就是简单创建一个对象,但是这个对象是内部的,所以是未逃逸的,所以理论上 JVM 是会进行优化的,我们拭目以待。并且我们会对比开启和关闭逃逸分析之后各自程序的运行时间:
-
- /**
- * @ClassName: EscapeAnalysisTest
- * @Description: http://www.jetchen.cn 逃逸分析 demo
- * @Author: Jet.Chen
- * @Date: 2020/11/23 14:26
- * @Version: 1.0
- **/
- public class EscapeAnalysisTest {
-
- public static void main(String[] args) {
- long t1 = System.currentTimeMillis();
- for (int i = 0; i < 100000000; i++) {
- allot();
- }
- long t2 = System.currentTimeMillis();
- System.out.println(t2-t1);
- }
-
- private static void allot() {
- Jet jet = new Jet();
- }
-
- static class Jet {
- public String name;
- }
-
- }
- 上面就是我们进行逃逸分析测试的代码, mian() 方法末尾有一个线程暂停,目的是为了观察此时 JVM 中的内存情况。
-
- Step 1:测试开启逃逸
- 由于环境是 jdk1.8,默认开启了逃逸分析,所以直接运行,得到结果如下,程序耗时 3 毫秒:
-
-
- 此时线程是处于睡眠状态的,我们观察下内存情况,发现堆内存中一共新建了 11 万个 Jet 对象。
-
-
-
- Step 2:测试关闭逃逸
- 我们关闭逃逸分析再来运行一次(使用 java -XX:-DoEscapeAnalysis EscapeAnalysisTest 来运行代码即可),得到结果如下,程序耗时 400 毫秒:
-
-
- 此时我们观察下内存情况,发现堆内存中一共新建了 3 千多万个 Jet 对象。
-
-
- 所以,无论是从代码的执行时间(3 毫秒 VS 400 毫秒),还是从堆内存中对象的数量(11 万个 VS 3 千万个)来分析,在上述场景下,开启逃逸分析是有正向益的。
-
- Step 3:测试标量替换
- 我们测试下开启和关闭 标量替换,如下图:
-
-
- 由上图我们可以看出,在上述极端场景下,开启和关闭标量替换对于性能的影响也是满巨大的,另外,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。
-
- Step 4:测试锁消除
- 测试锁消除,我们需要简单调整下代码,即给 allot() 方法中的内容加锁处理,如下:
-
- private static void allot() {
- Jet jet = new Jet();
- synchronized (jet) {
- jet.name = "jet Chen";
- }
- }
- 然后我们运行测试代码,测试结果也很明显,在上述场景下,开启和关闭锁消除对程序性能的影响也是巨大的。
-
-
- /**
- * 进行两种测试
- * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
- * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 开启逃逸分析 jdk8默认开启
- * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- *
- * 执行main方法后
- * jps 查看进程
- * jmap -histo 进程ID
- *
- */
- @Slf4j
- public class EscapeTest {
-
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- for (int i = 0; i < 500000; i++) {
- alloc();
- }
- long end = System.currentTimeMillis();
- log.info("执行时间:" + (end - start) + " ms");
- try {
- Thread.sleep(Integer.MAX_VALUE);
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- }
-
-
- /**
- * JIT编译时会对代码进行逃逸分析
- * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
- * Ponit没有逃逸
- */
- private static String alloc() {
- Point point = new Point();
- return point.toString();
- }
-
- /**
- *同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
- */
- public void append(String str1, String str2) {
- StringBuffer stringBuffer = new StringBuffer();
- stringBuffer.append(str1).append(str2);
- }
-
- /**
- * 标量替换
- *
- */
- private static void test2() {
- Point point = new Point(1,2);
- System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
-
- // int x=1;
- // int y=2;
- // System.out.println("point.x="+x+"; point.y="+y);
- }
-
-
- }
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- class Point{
- private int x;
- private int y;
- }
L'analyse d'échappement peut déterminer que certains appels de méthode n'échapperont pas à la portée de la méthode actuelle. Par conséquent, ces méthodes peuvent être optimisées en ligne pour réduire le coût des appels de méthode et améliorer l’efficacité d’exécution du programme.
Grâce à ces stratégies d'optimisation, l'analyse des échappements peut aider la JVM à mieux optimiser le code, à réduire la surcharge du garbage collection, à améliorer l'efficacité et la réactivité de l'exécution du programme et à réduire l'utilisation de la mémoire.
L'analyse d'échappement propose un large éventail de scénarios d'application dans les applications Java réelles. Voici quelques scénarios d'application courants :
L'article sur l'analyse des évasions a été publié en 1999, mais il n'a été implémenté qu'avec le JDK1.6, et cette technologie n'est pas encore très mature.
La raison fondamentale est qu'il n'y a aucune garantie que la consommation de performances de l'analyse d'échappement sera supérieure à sa consommation. Bien que l'analyse d'échappement puisse effectuer une substitution scalaire, une allocation de pile et une élimination de verrous. Cependant, l’analyse des évasions elle-même nécessite également une série d’analyses complexes, ce qui constitue en réalité un processus relativement long.
Un exemple extrême est qu’après analyse d’évasion, on constate qu’aucun objet ne s’échappe. Le processus d’analyse des évasions est alors inutile.
Bien que cette technologie ne soit pas très mature, elle constitue également un moyen très important dans la technologie d'optimisation des compilateurs juste à temps. J'ai remarqué qu'il existe certaines opinions selon lesquelles, grâce à l'analyse d'échappement, la JVM allouera des objets sur la pile qui ne s'échapperont pas. Ceci est théoriquement possible, mais cela dépend du choix du concepteur JvM. Pour autant que je sache, Oracle Hotspot JVM ne fait pas cela. Cela a été expliqué dans les documents relatifs à l'analyse d'échappement, il est donc clair que toutes les instances d'objet sont créées sur le tas.
À l'heure actuelle, de nombreux livres sont encore basés sur des versions antérieures au JDK7. Le cache des chaînes internes et des variables statiques était autrefois alloué à la génération permanente, et la génération permanente a été remplacée par la zone de métadonnées. Cependant, le cache de chaînes interne et les variables statiques ne sont pas transférés vers la zone de métadonnées, mais sont alloués directement sur le tas, cela est donc également cohérent avec la conclusion du point précédent : les instances d'objet sont allouées sur le tas. L'exemple ci-dessus est accéléré en raison de la substitution scalaire.
Si un objet ne s'échappe pas dans le corps de la méthode ou dans le thread (ou s'il est déterminé qu'il n'a pas réussi à s'échapper après l'analyse de l'échappement), les optimisations suivantes peuvent être effectuées :
Dans des circonstances normales, les objets qui ne peuvent pas s'échapper occupent un espace relativement grand. Si l'espace sur la pile peut être utilisé, un grand nombre d'objets seront détruits à la fin de la méthode, réduisant ainsi la pression du GC.
S'il existe un verrou de synchronisation sur la méthode de la classe que vous définissez, mais qu'un seul thread y accède au moment de l'exécution, le code machine après l'analyse d'échappement s'exécutera sans le verrou de synchronisation.
Les types de données primitifs de la machine virtuelle Java (types numériques tels que les types int, long et référence, etc.) ne peuvent pas être décomposés davantage et peuvent être appelés scalaires. En revanche, si une donnée peut continuer à être décomposée, elle est appelée un agrégat. L'agrégat le plus typique en Java est un objet. Si l'analyse d'échappement prouve qu'un objet ne sera pas accessible de l'extérieur et que l'objet est décomposable, l'objet ne peut pas être créé lorsque le programme est réellement exécuté, mais plutôt créer directement plusieurs de ses variables membres utilisées par cette méthode pour le remplacer. Les variables désassemblées peuvent être analysées et optimisées séparément. Une fois les attributs aplatis, il n'est pas nécessaire d'établir des relations via des pointeurs de référence. Elles peuvent être stockées de manière continue et compacte, ce qui est plus convivial pour divers stockages et permet d'économiser beaucoup de traitement de données. exécution entraînant une perte de performances. Dans le même temps, vous pouvez également allouer de l'espace sur le cadre de pile ou le registre respectivement, de sorte que l'objet d'origine n'ait pas besoin d'allouer de l'espace dans son ensemble.
La jeune génération est la zone où les objets naissent, grandissent et meurent. Un objet est généré et utilisé ici, et est finalement collecté par le ramasse-miettes et termine sa vie.
Les objets à long cycle de vie placés dans l'ancienne génération sont généralement des objets Java copiés de la zone survivante. Bien sûr, il y a aussi des cas particuliers. On sait que les objets ordinaires seront alloués sur TLAB ; si l'objet est gros, la JVM essaiera de l'allouer directement à d'autres emplacements dans Eden ; si l'objet est trop gros, elle ne le fera pas ; Pour pouvoir trouver un espace libre continu suffisamment long dans l'espace de nouvelle génération, la JVM l'attribuera directement à l'ancienne génération. Lorsque le GC ne se produit que dans la jeune génération, l’acte de recyclage des objets de la jeune génération est appelé MinorGc.
Lorsque le GC apparaît dans l’ancienne génération, on l’appelle MajorGc ou FullGC. Généralement, la fréquence d'apparition de MinorGc est beaucoup plus élevée que celle de MajorGC, c'est-à-dire que la fréquence de collecte des ordures dans l'ancienne génération sera bien inférieure à celle de la jeune génération.
L'analyse d'échappement JVM utilise deux méthodes d'analyse, statique et dynamique, pour déterminer si un objet peut échapper à la portée d'une méthode. Il peut aider la JVM à optimiser le code et à améliorer les performances et l'efficacité de l'utilisation de la mémoire des programmes Java.
Les stratégies d'optimisation pour l'analyse d'échappement incluent l'allocation sur la pile, l'élimination de la synchronisation, la substitution scalaire et l'inlining de méthode. Ces stratégies d'optimisation peuvent réduire la surcharge de garbage collection, améliorer l'efficacité et la réactivité de l'exécution du programme et réduire l'utilisation de la mémoire.
faire référence à:
https://zhuanlan.zhihu.com/p/693382698
JVM-Heap-Escape Analysis-08-CSDN Blog
JIT Memory Escape Analysis_Java désactive le remplacement scalaire-CSDN Blog
java -XX:+PrintFlagsFinal #输出打印所有参数jvm参数